@whisperr/wizard 0.1.6 → 0.1.7

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 (2) hide show
  1. package/dist/index.js +466 -22
  2. 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/react-native.ts
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 detect4(ctx) {
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: target4, confidence: Math.min(confidence, 1), evidence };
654
+ return { target: target7, confidence: Math.min(confidence, 1), evidence };
284
655
  }
285
- var systemPrompt4 = `
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: target4,
309
- detect: detect4,
679
+ target: target7,
680
+ detect: detect7,
310
681
  packageRef: "@whisperr/react-native (npm)",
311
- systemPrompt: systemPrompt4
682
+ systemPrompt: systemPrompt7
312
683
  };
313
684
 
314
685
  // src/core/playbooks/swift.ts
315
- var target5 = {
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 detect5(ctx) {
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: target5, confidence: Math.min(confidence, 1), evidence };
713
+ return { target: target8, confidence: Math.min(confidence, 1), evidence };
343
714
  }
344
- var systemPrompt5 = `
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: target5,
373
- detect: detect5,
743
+ target: target8,
744
+ detect: detect8,
374
745
  packageRef: "Whisperr (Swift Package)",
375
- systemPrompt: systemPrompt5
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) {
@@ -681,7 +1055,7 @@ function renderEventsBrief(m) {
681
1055
  lines.push("");
682
1056
  }
683
1057
  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 client-side trigger.`
1058
+ `${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
1059
  );
686
1060
  if (events.some((e) => e.coverage?.length)) {
687
1061
  lines.push("");
@@ -721,7 +1095,7 @@ function coverageNote(coverage) {
721
1095
  async function runIntegrationAgent(opts) {
722
1096
  const { repoPath, config, session, playbook, manifest, progress } = opts;
723
1097
  applyModelAuthEnv(config, session);
724
- const systemPrompt6 = [BASE_WIZARD_PROMPT, playbook.systemPrompt].join("\n\n");
1098
+ const systemPrompt9 = [BASE_WIZARD_PROMPT, playbook.systemPrompt].join("\n\n");
725
1099
  const started = Date.now();
726
1100
  let costUsd = 0;
727
1101
  const summaries = [];
@@ -750,7 +1124,7 @@ async function runIntegrationAgent(opts) {
750
1124
  ].join("\n");
751
1125
  const core = await runPass({
752
1126
  prompt: corePrompt,
753
- systemPrompt: systemPrompt6,
1127
+ systemPrompt: systemPrompt9,
754
1128
  repoPath,
755
1129
  model: config.model,
756
1130
  maxTurns: 35,
@@ -779,7 +1153,7 @@ ${core.summary}`);
779
1153
  ].join("\n");
780
1154
  const events = await runPass({
781
1155
  prompt: eventsPrompt,
782
- systemPrompt: systemPrompt6,
1156
+ systemPrompt: systemPrompt9,
783
1157
  repoPath,
784
1158
  model: config.model,
785
1159
  maxTurns: config.maxTurns,
@@ -799,7 +1173,7 @@ ${events.summary}`);
799
1173
  };
800
1174
  }
801
1175
  async function runPass(opts) {
802
- const { prompt, systemPrompt: systemPrompt6, repoPath, model, maxTurns, progress } = opts;
1176
+ const { prompt, systemPrompt: systemPrompt9, repoPath, model, maxTurns, progress } = opts;
803
1177
  let summary = "";
804
1178
  let costUsd = 0;
805
1179
  let ok = false;
@@ -809,7 +1183,7 @@ async function runPass(opts) {
809
1183
  options: {
810
1184
  model,
811
1185
  cwd: repoPath,
812
- systemPrompt: systemPrompt6,
1186
+ systemPrompt: systemPrompt9,
813
1187
  allowedTools: ["Read", "Edit", "Write", "Bash", "Glob", "Grep"],
814
1188
  permissionMode: "bypassPermissions",
815
1189
  maxTurns,
@@ -920,6 +1294,21 @@ function revertHint(checkpoint) {
920
1294
  if (!checkpoint.isRepo || !checkpoint.baseRef) return void 0;
921
1295
  return `git restore . && git clean -fd (back to ${checkpoint.baseRef.slice(0, 7)})`;
922
1296
  }
1297
+ async function isWorkingTreeClean(repoPath) {
1298
+ const status = await run(repoPath, ["status", "--porcelain"]);
1299
+ return status.ok && status.stdout.trim() === "";
1300
+ }
1301
+ async function revertToCheckpoint(repoPath, checkpoint) {
1302
+ if (!checkpoint.isRepo) return false;
1303
+ if (checkpoint.baseRef) {
1304
+ const reset = await run(repoPath, ["reset", "--hard", checkpoint.baseRef]);
1305
+ const clean2 = await run(repoPath, ["clean", "-fd"]);
1306
+ return reset.ok && clean2.ok;
1307
+ }
1308
+ const restore = await run(repoPath, ["restore", "."]);
1309
+ const clean = await run(repoPath, ["clean", "-fd"]);
1310
+ return restore.ok && clean.ok;
1311
+ }
923
1312
  async function repoFingerprint(repoPath) {
924
1313
  const remote = await run(repoPath, ["remote", "get-url", "origin"]);
925
1314
  const seed = remote.ok && remote.stdout.trim() ? normalizeRemote(remote.stdout.trim()) : `dir:${basename(repoPath)}`;
@@ -1079,8 +1468,24 @@ Flutter is live today; ${theme.bright(
1079
1468
  );
1080
1469
  const checkpoint = await takeCheckpoint(repoPath);
1081
1470
  if (!checkpoint.isRepo) {
1471
+ if (!options.force) {
1472
+ p.cancel(
1473
+ 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.")
1474
+ );
1475
+ return 1;
1476
+ }
1082
1477
  p.log.warn(
1083
- theme.warn("not a git repo") + theme.muted(" \u2014 edits will apply with no automatic undo.")
1478
+ theme.warn("not a git repo") + theme.muted(" \u2014 proceeding with --force; there is no automatic undo.")
1479
+ );
1480
+ } else if (!await isWorkingTreeClean(repoPath)) {
1481
+ if (!options.force) {
1482
+ p.cancel(
1483
+ 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.")
1484
+ );
1485
+ return 1;
1486
+ }
1487
+ p.log.warn(
1488
+ theme.warn("uncommitted changes present") + theme.muted(" \u2014 proceeding with --force; a revert would also drop your own changes.")
1084
1489
  );
1085
1490
  }
1086
1491
  const spin = p.spinner();
@@ -1116,6 +1521,7 @@ Flutter is live today; ${theme.bright(
1116
1521
  clearInterval(tick);
1117
1522
  if (!outcome) {
1118
1523
  spin.stop(theme.alert("Integration stopped"));
1524
+ await maybeRevert(repoPath, checkpoint, "The run stopped before finishing.");
1119
1525
  return 1;
1120
1526
  }
1121
1527
  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 +1533,17 @@ Flutter is live today; ${theme.bright(
1127
1533
  if (outcome.summary.trim()) {
1128
1534
  p.log.message(outcome.summary.trim());
1129
1535
  }
1536
+ if (!outcome.coreOk) {
1537
+ const reverted = await maybeRevert(
1538
+ repoPath,
1539
+ checkpoint,
1540
+ "The core setup didn't complete, so these changes may be incomplete."
1541
+ );
1542
+ if (reverted) {
1543
+ p.outro(theme.muted("Reverted \u2014 nothing was left in your working tree."));
1544
+ return 1;
1545
+ }
1546
+ }
1130
1547
  const wiredMap = await scanWiredEvents(
1131
1548
  repoPath,
1132
1549
  files,
@@ -1183,6 +1600,29 @@ Flutter is live today; ${theme.bright(
1183
1600
  p.outro(theme.signal("\u2301 ") + theme.bright("Whisperr is wired in."));
1184
1601
  return 0;
1185
1602
  }
1603
+ async function maybeRevert(repoPath, checkpoint, reason) {
1604
+ if (!checkpoint.isRepo) return false;
1605
+ const files = await changedFiles(repoPath, checkpoint);
1606
+ if (files.length === 0) return false;
1607
+ const doRevert = await p.confirm({
1608
+ message: `${reason} Revert all of the wizard's changes now?`,
1609
+ initialValue: true
1610
+ });
1611
+ if (p.isCancel(doRevert) || !doRevert) {
1612
+ const hint = revertHint(checkpoint);
1613
+ if (hint) p.log.info(theme.muted(`Left in your working tree. Undo anytime: ${hint}`));
1614
+ return false;
1615
+ }
1616
+ const ok = await revertToCheckpoint(repoPath, checkpoint);
1617
+ if (ok) {
1618
+ p.log.success(theme.success("Reverted to your pre-wizard state."));
1619
+ } else {
1620
+ p.log.warn(
1621
+ theme.warn("Couldn't revert automatically \u2014 undo manually: ") + (revertHint(checkpoint) ?? "git reset --hard")
1622
+ );
1623
+ }
1624
+ return ok;
1625
+ }
1186
1626
  async function chooseTarget(detections) {
1187
1627
  const top = detections[0];
1188
1628
  if (top && (detections.length === 1 || top.confidence >= 0.9)) {
@@ -1274,6 +1714,9 @@ function parseArgs(argv) {
1274
1714
  case "--offline":
1275
1715
  opts.offline = true;
1276
1716
  break;
1717
+ case "--force":
1718
+ opts.force = true;
1719
+ break;
1277
1720
  case "--api":
1278
1721
  opts.apiBaseUrl = args[++i];
1279
1722
  break;
@@ -1299,6 +1742,7 @@ function printHelp() {
1299
1742
  "",
1300
1743
  ` ${theme.bright("Options")}`,
1301
1744
  " --offline Use a demo manifest, no account/browser needed",
1745
+ " --force Proceed without a clean git tree (no safe undo)",
1302
1746
  " --api <url> Override the Whisperr API base URL",
1303
1747
  " --model <id> Override the coding-agent model",
1304
1748
  " -h, --help Show this help",
@@ -1317,7 +1761,7 @@ async function main() {
1317
1761
  return;
1318
1762
  }
1319
1763
  if ("version" in parsed) {
1320
- console.log("0.1.6");
1764
+ console.log("0.1.7");
1321
1765
  return;
1322
1766
  }
1323
1767
  try {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@whisperr/wizard",
3
- "version": "0.1.6",
3
+ "version": "0.1.7",
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,