@whisperr/wizard 0.1.0

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 ADDED
@@ -0,0 +1,1149 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/cli.ts
4
+ import { resolve } from "path";
5
+ import * as p from "@clack/prompts";
6
+
7
+ // src/core/config.ts
8
+ var DEFAULT_API_BASE = "https://api.whisperr.net";
9
+ var DEFAULT_MODEL = "claude-opus-4-8";
10
+ function resolveConfig(flags = {}) {
11
+ const apiBaseUrl = (flags.apiBaseUrl ?? process.env.WHISPERR_WIZARD_API_BASE ?? DEFAULT_API_BASE).replace(/\/+$/, "");
12
+ const llmBaseUrl = (process.env.WHISPERR_WIZARD_LLM_BASE ?? `${apiBaseUrl}/wizard/llm`).replace(/\/+$/, "");
13
+ const directAnthropicKey = process.env.WHISPERR_WIZARD_DIRECT_ANTHROPIC_KEY ?? process.env.ANTHROPIC_API_KEY;
14
+ const offline = flags.offline ?? ["1", "true", "yes"].includes(
15
+ (process.env.WHISPERR_WIZARD_OFFLINE ?? "").toLowerCase()
16
+ );
17
+ return {
18
+ apiBaseUrl,
19
+ llmBaseUrl,
20
+ model: flags.model ?? process.env.WHISPERR_WIZARD_MODEL ?? DEFAULT_MODEL,
21
+ maxTurns: Number(process.env.WHISPERR_WIZARD_MAX_TURNS ?? 40),
22
+ directAnthropicKey,
23
+ offline
24
+ };
25
+ }
26
+
27
+ // src/core/detect.ts
28
+ import { readFile, stat, readdir } from "fs/promises";
29
+ import { join } from "path";
30
+
31
+ // src/core/playbooks/flutter.ts
32
+ var target = {
33
+ id: "flutter",
34
+ displayName: "Flutter",
35
+ language: "dart",
36
+ availability: "available"
37
+ };
38
+ async function detect(ctx) {
39
+ const evidence = [];
40
+ let confidence = 0;
41
+ if (await ctx.exists("pubspec.yaml")) {
42
+ evidence.push("pubspec.yaml");
43
+ confidence += 0.6;
44
+ const pubspec = await ctx.read("pubspec.yaml");
45
+ if (/\bflutter\s*:/.test(pubspec) || /sdk:\s*flutter/.test(pubspec)) {
46
+ evidence.push("flutter sdk in pubspec");
47
+ confidence += 0.3;
48
+ }
49
+ }
50
+ if (await ctx.exists("lib/main.dart")) {
51
+ evidence.push("lib/main.dart");
52
+ confidence += 0.2;
53
+ }
54
+ if (confidence === 0) return null;
55
+ return { target, confidence: Math.min(confidence, 1), evidence };
56
+ }
57
+ var systemPrompt = `
58
+ ## SDK: Whisperr for Flutter (Dart) \u2014 package \`whisperr\`
59
+
60
+ This SDK is published on pub.dev. Integrate it as follows.
61
+
62
+ 1) Dependency. Add to pubspec.yaml under dependencies:
63
+ whisperr: ^0.2.0
64
+ Then the SDK expects \`flutter pub get\` to be run (run it via bash).
65
+
66
+ 2) Initialize once, early, before runApp(), in lib/main.dart:
67
+ import 'package:whisperr/whisperr.dart';
68
+ await Whisperr.initialize(
69
+ apiKey: '<INGESTION_API_KEY from manifest>',
70
+ baseUrl: '<INGESTION_BASE_URL from manifest>', // omit if it equals the SDK default
71
+ );
72
+ If main() is not async, make it async and \`await\` the initialize call, or use
73
+ WidgetsFlutterBinding.ensureInitialized() first.
74
+
75
+ 3) identify(). Call right after the end-user is known \u2014 after successful
76
+ login/signup and on session restore at startup:
77
+ await Whisperr.instance.identify(
78
+ '<stable external user id>',
79
+ traits: { /* manifest traits, e.g. 'plan': user.plan */ },
80
+ email: user.email, // optional shortcut -> opted-in email channel
81
+ phone: user.phone, // optional shortcut -> SMS channel
82
+ pushToken: fcmToken, // optional shortcut -> push channel
83
+ );
84
+ Use the app's real stable id (auth uid / customer id), never a device id.
85
+ On logout call \`await Whisperr.instance.reset();\`.
86
+
87
+ 4) track(). For each manifest event, find where that user action actually
88
+ happens and add:
89
+ await Whisperr.instance.track('event_type_from_manifest', properties: { ... });
90
+ event_type must be snake_case and copied verbatim from the manifest.
91
+
92
+ Notes / gotchas:
93
+ - The SDK queues + batches automatically; you do NOT need to await every track
94
+ for delivery \u2014 awaiting just enqueues. Fire-and-forget is fine in UI handlers.
95
+ - Do not call track() inside build() methods or render loops \u2014 only in event
96
+ handlers / business logic (onPressed, on success of an API call, in a bloc/
97
+ cubit/provider action, etc.).
98
+ - Hardcoding the API key in Dart is acceptable for a client SDK (it is an
99
+ ingestion key, scoped + rate-limited), but prefer --dart-define / a config
100
+ file if the project already uses one.
101
+ - Verify with \`flutter analyze\`.
102
+ `.trim();
103
+ var flutterPlaybook = {
104
+ target,
105
+ detect,
106
+ packageRef: "whisperr (pub.dev)",
107
+ systemPrompt,
108
+ verifyCommand: "flutter analyze"
109
+ };
110
+
111
+ // src/core/playbooks/nextjs.ts
112
+ var target2 = {
113
+ id: "nextjs",
114
+ displayName: "Next.js",
115
+ language: "typescript",
116
+ availability: "planned"
117
+ };
118
+ async function detect2(ctx) {
119
+ if (!await ctx.exists("package.json")) return null;
120
+ const pkgJson = await ctx.read("package.json");
121
+ let isNext = false;
122
+ try {
123
+ const pkg = JSON.parse(pkgJson);
124
+ const all = { ...pkg.dependencies ?? {}, ...pkg.devDependencies ?? {} };
125
+ isNext = "next" in all;
126
+ } catch {
127
+ isNext = false;
128
+ }
129
+ if (!isNext && !await ctx.exists("next.config.js") && !await ctx.exists("next.config.mjs"))
130
+ return null;
131
+ const evidence = ["next dependency"];
132
+ let confidence = 0.85;
133
+ if (await ctx.exists("app")) {
134
+ evidence.push("app/ router");
135
+ confidence += 0.1;
136
+ } else if (await ctx.exists("pages")) {
137
+ evidence.push("pages/ router");
138
+ confidence += 0.05;
139
+ }
140
+ return { target: target2, confidence: Math.min(confidence, 1), evidence };
141
+ }
142
+ var systemPrompt2 = `
143
+ ## SDK: Whisperr for Web in Next.js \u2014 package \`@whisperr/web\`
144
+
145
+ Same SDK as plain web, with Next-specific wiring.
146
+
147
+ 1) Install \`@whisperr/web\`. Put the key in NEXT_PUBLIC_WHISPERR_KEY in
148
+ .env.local (and add it to .env.example).
149
+
150
+ 2) Whisperr runs client-side. Create a client provider:
151
+ - App Router: a 'use client' component (e.g. app/whisperr-provider.tsx) that
152
+ calls Whisperr.init(...) in a useEffect and renders children; mount it in
153
+ app/layout.tsx.
154
+ - Pages Router: init in pages/_app.tsx inside a useEffect.
155
+
156
+ 3) identify() after auth resolves on the client (e.g. in your auth/session
157
+ hook). reset() on sign-out.
158
+
159
+ 4) track() in client components / event handlers / mutation callbacks. Do NOT
160
+ call track from server components, route handlers, or getServerSideProps \u2014
161
+ this SDK is a browser client. Copy event_type verbatim from the manifest.
162
+
163
+ Notes:
164
+ - Guard against double-init under React Strict Mode (init is idempotent, but
165
+ wire it so it only runs once).
166
+ `.trim();
167
+ var nextjsPlaybook = {
168
+ target: target2,
169
+ detect: detect2,
170
+ packageRef: "@whisperr/web (npm)",
171
+ systemPrompt: systemPrompt2
172
+ };
173
+
174
+ // src/core/playbooks/web.ts
175
+ var target3 = {
176
+ id: "web-js",
177
+ displayName: "Web (JavaScript / TypeScript)",
178
+ language: "typescript",
179
+ // Flip to "available" once @whisperr/web ships (Phase 2).
180
+ availability: "planned"
181
+ };
182
+ function dependsOn(pkgJson, names) {
183
+ try {
184
+ const pkg = JSON.parse(pkgJson);
185
+ const all = {
186
+ ...pkg.dependencies ?? {},
187
+ ...pkg.devDependencies ?? {}
188
+ };
189
+ return names.some((n) => n in all);
190
+ } catch {
191
+ return false;
192
+ }
193
+ }
194
+ async function detect3(ctx) {
195
+ if (!await ctx.exists("package.json")) return null;
196
+ const pkgJson = await ctx.read("package.json");
197
+ const evidence = ["package.json"];
198
+ let confidence = 0.4;
199
+ if (dependsOn(pkgJson, ["next"])) return null;
200
+ if (dependsOn(pkgJson, ["react-native", "expo"])) return null;
201
+ if (dependsOn(pkgJson, ["react", "vue", "svelte", "@angular/core"])) {
202
+ evidence.push("frontend framework dependency");
203
+ confidence += 0.4;
204
+ }
205
+ if (await ctx.exists("index.html")) {
206
+ evidence.push("index.html");
207
+ confidence += 0.1;
208
+ }
209
+ return { target: target3, confidence: Math.min(confidence, 0.95), evidence };
210
+ }
211
+ var systemPrompt3 = `
212
+ ## SDK: Whisperr for Web (TypeScript/JavaScript) \u2014 package \`@whisperr/web\`
213
+
214
+ 1) Dependency. Install with the repo's package manager (detect pnpm/yarn/npm
215
+ from the lockfile): \`<pm> add @whisperr/web\`.
216
+
217
+ 2) Initialize once at app entry (e.g. src/main.tsx, src/index.ts, or the root
218
+ layout). Create a singleton client:
219
+ import { Whisperr } from '@whisperr/web';
220
+ export const whisperr = Whisperr.init({
221
+ apiKey: import.meta.env.VITE_WHISPERR_KEY, // or process.env.NEXT_PUBLIC_... etc
222
+ baseUrl: '<INGESTION_BASE_URL from manifest>',
223
+ });
224
+ Put the key in the project's env mechanism (Vite VITE_*, CRA REACT_APP_*,
225
+ plain .env) and write the example var into .env / .env.example.
226
+
227
+ 3) identify(). After auth resolves (login success, and on app load if a session
228
+ is restored):
229
+ whisperr.identify(user.id, {
230
+ traits: { /* manifest traits */ },
231
+ email: user.email, phone: user.phone,
232
+ });
233
+ Call whisperr.reset() on logout.
234
+
235
+ 4) track(). For each manifest event, instrument the real handler:
236
+ whisperr.track('event_type_from_manifest', { /* properties */ });
237
+ In React, this belongs in event handlers / effects / mutation callbacks, not
238
+ in render. Copy event_type verbatim (snake_case) from the manifest.
239
+
240
+ Notes:
241
+ - The SDK batches via /v1/events/batch and flushes on a timer + on
242
+ visibilitychange/unload (sendBeacon). Fire-and-forget is fine.
243
+ - Only the NEXT_PUBLIC_/VITE_-style public env vars are exposed to the browser;
244
+ the ingestion key is a public, rate-limited key so that is expected.
245
+ - If the project uses an analytics wrapper/provider pattern, follow it rather
246
+ than scattering raw whisperr.track calls.
247
+ `.trim();
248
+ var webPlaybook = {
249
+ target: target3,
250
+ detect: detect3,
251
+ packageRef: "@whisperr/web (npm)",
252
+ systemPrompt: systemPrompt3,
253
+ verifyCommand: void 0
254
+ };
255
+
256
+ // src/core/playbooks/react-native.ts
257
+ var target4 = {
258
+ id: "react-native",
259
+ displayName: "React Native",
260
+ language: "typescript",
261
+ availability: "planned"
262
+ };
263
+ async function detect4(ctx) {
264
+ if (!await ctx.exists("package.json")) return null;
265
+ const pkgJson = await ctx.read("package.json");
266
+ let match = false;
267
+ try {
268
+ const pkg = JSON.parse(pkgJson);
269
+ const all = { ...pkg.dependencies ?? {}, ...pkg.devDependencies ?? {} };
270
+ match = "react-native" in all || "expo" in all;
271
+ } catch {
272
+ match = false;
273
+ }
274
+ if (!match) return null;
275
+ const evidence = ["react-native / expo dependency"];
276
+ let confidence = 0.85;
277
+ if (await ctx.exists("app.json")) {
278
+ evidence.push("app.json");
279
+ confidence += 0.1;
280
+ }
281
+ return { target: target4, confidence: Math.min(confidence, 1), evidence };
282
+ }
283
+ var systemPrompt4 = `
284
+ ## SDK: Whisperr for React Native \u2014 package \`@whisperr/react-native\`
285
+
286
+ 1) Install \`@whisperr/react-native\` with the repo's package manager. If it has
287
+ native modules, run pod install for iOS (mention it; don't attempt to build).
288
+
289
+ 2) Initialize once at the app root (App.tsx / app/_layout.tsx for Expo Router):
290
+ import { Whisperr } from '@whisperr/react-native';
291
+ Whisperr.init({ apiKey: <key from manifest/env>, baseUrl: <manifest baseUrl> });
292
+
293
+ 3) identify() after auth resolves / on session restore; reset() on logout.
294
+ For channels: email/phone where known, and the push token (expo-notifications
295
+ or @react-native-firebase/messaging) if the app has push set up.
296
+
297
+ 4) track() in event handlers / business logic, never in render. event_type
298
+ verbatim from the manifest.
299
+
300
+ Notes:
301
+ - Persisted offline queue is built in (AsyncStorage); fire-and-forget is fine.
302
+ - Prefer an env/config mechanism (react-native-config, app.config) for the key
303
+ if one exists; otherwise a constants file is acceptable.
304
+ `.trim();
305
+ var reactNativePlaybook = {
306
+ target: target4,
307
+ detect: detect4,
308
+ packageRef: "@whisperr/react-native (npm)",
309
+ systemPrompt: systemPrompt4
310
+ };
311
+
312
+ // src/core/playbooks/swift.ts
313
+ var target5 = {
314
+ id: "swift",
315
+ displayName: "Swift (iOS)",
316
+ language: "swift",
317
+ availability: "planned"
318
+ };
319
+ async function detect5(ctx) {
320
+ const evidence = [];
321
+ let confidence = 0;
322
+ if (await ctx.exists("Package.swift")) {
323
+ evidence.push("Package.swift");
324
+ confidence += 0.6;
325
+ }
326
+ const root = await ctx.list(".");
327
+ if (root.some((f) => f.endsWith(".xcodeproj"))) {
328
+ evidence.push("xcodeproj");
329
+ confidence += 0.5;
330
+ }
331
+ if (root.some((f) => f.endsWith(".xcworkspace"))) {
332
+ evidence.push("xcworkspace");
333
+ confidence += 0.3;
334
+ }
335
+ if (await ctx.exists("Podfile")) {
336
+ evidence.push("Podfile");
337
+ confidence += 0.2;
338
+ }
339
+ if (confidence === 0) return null;
340
+ return { target: target5, confidence: Math.min(confidence, 1), evidence };
341
+ }
342
+ var systemPrompt5 = `
343
+ ## SDK: Whisperr for Swift (iOS) \u2014 Swift Package \`Whisperr\`
344
+
345
+ 1) Add the Swift Package dependency (github.com/WhisperrAI/whisperr-swift). If the
346
+ project uses Package.swift, add it there; if it's an .xcodeproj/.xcworkspace,
347
+ you cannot edit the project graph reliably from the shell \u2014 instead leave a
348
+ clearly commented setup note and the exact import + init code, and report it
349
+ as a manual follow-up.
350
+
351
+ 2) Initialize once at launch in the App struct (SwiftUI) or
352
+ application(_:didFinishLaunchingWithOptions:) (UIKit):
353
+ import Whisperr
354
+ Whisperr.initialize(apiKey: "<key>", baseUrl: "<manifest baseUrl>")
355
+
356
+ 3) identify() after the user is known / on session restore:
357
+ Whisperr.shared.identify("<external user id>", traits: [...], email: ..., phone: ...)
358
+ reset() on logout. Push channel = APNs device token if the app registers for
359
+ remote notifications.
360
+
361
+ 4) track() in action handlers / view-model methods, not in view body:
362
+ Whisperr.shared.track("event_type_from_manifest", properties: [ ... ])
363
+ event_type verbatim (snake_case) from the manifest.
364
+
365
+ Notes:
366
+ - Prefer reading the key from Info.plist / an xcconfig if the project uses one.
367
+ - Do not attempt to run xcodebuild; verification is the human's step here.
368
+ `.trim();
369
+ var swiftPlaybook = {
370
+ target: target5,
371
+ detect: detect5,
372
+ packageRef: "Whisperr (Swift Package)",
373
+ systemPrompt: systemPrompt5
374
+ };
375
+
376
+ // src/core/playbooks/index.ts
377
+ var ALL_PLAYBOOKS = [
378
+ flutterPlaybook,
379
+ nextjsPlaybook,
380
+ reactNativePlaybook,
381
+ webPlaybook,
382
+ swiftPlaybook
383
+ ];
384
+ function playbookByTargetId(id) {
385
+ return ALL_PLAYBOOKS.find((p2) => p2.target.id === id);
386
+ }
387
+
388
+ // src/core/detect.ts
389
+ function makeDetectContext(repoPath) {
390
+ const cache = /* @__PURE__ */ new Map();
391
+ return {
392
+ repoPath,
393
+ async read(relPath) {
394
+ if (cache.has(relPath)) return cache.get(relPath);
395
+ let content = "";
396
+ try {
397
+ content = await readFile(join(repoPath, relPath), "utf8");
398
+ } catch {
399
+ content = "";
400
+ }
401
+ cache.set(relPath, content);
402
+ return content;
403
+ },
404
+ async exists(relPath) {
405
+ try {
406
+ await stat(join(repoPath, relPath));
407
+ return true;
408
+ } catch {
409
+ return false;
410
+ }
411
+ },
412
+ async list(relDir) {
413
+ try {
414
+ const entries = await readdir(join(repoPath, relDir), {
415
+ withFileTypes: true
416
+ });
417
+ return entries.map((e) => e.name);
418
+ } catch {
419
+ return [];
420
+ }
421
+ }
422
+ };
423
+ }
424
+ async function detectStack(repoPath) {
425
+ const ctx = makeDetectContext(repoPath);
426
+ const results = await Promise.all(
427
+ ALL_PLAYBOOKS.map((p2) => p2.detect(ctx).catch(() => null))
428
+ );
429
+ return results.filter((d) => d !== null).sort((a, b) => b.confidence - a.confidence);
430
+ }
431
+
432
+ // src/core/auth.ts
433
+ import open from "open";
434
+
435
+ // src/ui/theme.ts
436
+ import pc from "picocolors";
437
+ var truecolor = process.env.COLORTERM === "truecolor" || process.env.COLORTERM === "24bit";
438
+ function rgb(r, g, b, text) {
439
+ if (!truecolor || !pc.isColorSupported) return text;
440
+ return `\x1B[38;2;${r};${g};${b}m${text}\x1B[39m`;
441
+ }
442
+ var theme = {
443
+ /** Signal blue — the one accent. */
444
+ signal: (s) => rgb(91, 141, 239, s),
445
+ /** Muted hairline grey for secondary text. */
446
+ muted: (s) => rgb(140, 140, 150, s),
447
+ /** Near-white primary text. */
448
+ bright: (s) => pc.white(pc.bold(s)),
449
+ dim: (s) => pc.dim(s),
450
+ success: (s) => rgb(110, 207, 159, s),
451
+ warn: (s) => rgb(240, 191, 105, s),
452
+ alert: (s) => rgb(248, 113, 113, s),
453
+ bold: (s) => pc.bold(s)
454
+ };
455
+ var wordmark = (() => {
456
+ const mark = theme.signal("\u2301");
457
+ return `${mark} ${theme.bright("whisperr")} ${theme.muted("wizard")}`;
458
+ })();
459
+ var divider = theme.dim("\u2500".repeat(48));
460
+
461
+ // src/core/auth.ts
462
+ async function authenticate(config) {
463
+ if (config.offline) return offlineSession();
464
+ const authorize = await fetchJson(
465
+ `${config.apiBaseUrl}/wizard/device/authorize`,
466
+ { method: "POST", body: JSON.stringify({ client: "whisperr-wizard" }) }
467
+ );
468
+ const url = authorize.verification_uri_complete ?? authorize.verification_uri;
469
+ const intervalMs = (authorize.interval ?? 5) * 1e3;
470
+ const deadline = Date.now() + (authorize.expires_in ?? 600) * 1e3;
471
+ try {
472
+ await open(url);
473
+ } catch {
474
+ }
475
+ let wait = intervalMs;
476
+ while (Date.now() < deadline) {
477
+ await sleep(wait);
478
+ const res = await fetch(
479
+ `${config.apiBaseUrl}/wizard/device/token`,
480
+ {
481
+ method: "POST",
482
+ headers: { "content-type": "application/json" },
483
+ body: JSON.stringify({ device_code: authorize.device_code })
484
+ }
485
+ );
486
+ if (res.ok) {
487
+ const tok = await res.json();
488
+ return { token: tok.token, appId: tok.app_id, expiresAt: tok.expires_at };
489
+ }
490
+ if (res.status === 429) {
491
+ wait += 2e3;
492
+ continue;
493
+ }
494
+ if (res.status === 428) continue;
495
+ throw new Error(
496
+ `Authorization failed (${res.status}). Run the wizard again to retry.`
497
+ );
498
+ }
499
+ throw new Error("Authorization timed out. Run the wizard again.");
500
+ }
501
+ function offlineSession() {
502
+ return {
503
+ token: "offline-dev-token",
504
+ appId: "app_offline_dev",
505
+ expiresAt: Math.floor(Date.now() / 1e3) + 3600
506
+ };
507
+ }
508
+ async function fetchJson(url, init) {
509
+ const res = await fetch(url, {
510
+ ...init,
511
+ headers: { "content-type": "application/json", ...init.headers ?? {} }
512
+ });
513
+ if (!res.ok) {
514
+ throw new Error(
515
+ `${theme.alert("Request failed")}: ${init.method ?? "GET"} ${url} -> ${res.status}`
516
+ );
517
+ }
518
+ return await res.json();
519
+ }
520
+ function sleep(ms) {
521
+ return new Promise((r) => setTimeout(r, ms));
522
+ }
523
+
524
+ // src/core/manifest.ts
525
+ async function fetchManifest(config, session, targetId) {
526
+ if (config.offline) return mockManifest(session.appId, config);
527
+ const url = new URL(`${config.apiBaseUrl}/wizard/manifest`);
528
+ if (targetId) url.searchParams.set("target", targetId);
529
+ const res = await fetch(url, {
530
+ headers: { authorization: `Bearer ${session.token}` }
531
+ });
532
+ if (!res.ok) {
533
+ throw new Error(
534
+ `Could not load your integration manifest (${res.status}). Make sure onboarding is complete for this app.`
535
+ );
536
+ }
537
+ return await res.json();
538
+ }
539
+ function mockManifest(appId, config) {
540
+ return {
541
+ appId,
542
+ appName: "Acme (dev)",
543
+ ingestionApiKey: "wrk_offline_demo_key_do_not_use",
544
+ ingestionBaseUrl: config.apiBaseUrl,
545
+ businessContext: "B2C subscription app. Free trial converts to a paid monthly plan. Churn risk concentrates around trial end and the first failed payment.",
546
+ identify: {
547
+ traits: [
548
+ { name: "plan", description: "free | trial | pro" },
549
+ { name: "signup_date", description: "ISO date the user signed up" }
550
+ ],
551
+ channels: ["email", "push"],
552
+ notes: "Call identify on login and on session restore. Send email always; push token when notifications are enabled."
553
+ },
554
+ events: [
555
+ {
556
+ eventType: "trial_started",
557
+ label: "Trial started",
558
+ description: "User begins their free trial.",
559
+ importance: 0.7,
560
+ properties: [{ name: "plan", description: "trial tier chosen" }],
561
+ interventions: [{ code: "onboarding_nudge", label: "Onboarding nudge" }]
562
+ },
563
+ {
564
+ eventType: "feature_activated",
565
+ label: "Core feature activated",
566
+ description: "User completes the core 'aha' action for the first time. Strong retention signal; fire on first successful completion only.",
567
+ importance: 0.6,
568
+ interventions: [{ code: "activation_boost", label: "Activation boost" }]
569
+ },
570
+ {
571
+ eventType: "payment_failed",
572
+ label: "Payment failed",
573
+ description: "A subscription charge was declined. Fire from the billing webhook handler / failed-charge path.",
574
+ importance: 0.95,
575
+ properties: [
576
+ { name: "amount", description: "charge amount" },
577
+ { name: "reason", description: "decline reason if known" }
578
+ ],
579
+ interventions: [{ code: "dunning_recovery", label: "Dunning recovery", weight: 0.9 }]
580
+ },
581
+ {
582
+ eventType: "subscription_cancelled",
583
+ label: "Subscription cancelled",
584
+ description: "User cancels their paid plan. Fire at the moment of cancel.",
585
+ importance: 1,
586
+ properties: [{ name: "reason", description: "stated cancel reason" }],
587
+ interventions: [{ code: "win_back", label: "Win-back", weight: 1 }]
588
+ }
589
+ ],
590
+ supportedTargets: ["flutter", "web-js", "nextjs", "react-native", "swift"]
591
+ };
592
+ }
593
+
594
+ // src/core/agent.ts
595
+ import { query } from "@anthropic-ai/claude-agent-sdk";
596
+
597
+ // src/core/playbooks/shared-prompt.ts
598
+ var BASE_WIZARD_PROMPT = `
599
+ You are the Whisperr Wizard \u2014 an expert integration agent. You are running
600
+ INSIDE a customer's code repository. Your single job: integrate the Whisperr SDK
601
+ so the product starts sending the right events to Whisperr, then stop.
602
+
603
+ Whisperr is a churn-prevention engine. It needs two things from the app:
604
+ 1. identify() \u2014 tell Whisperr who the end-user is (a stable external user id)
605
+ plus traits and contact channels, called right after the user is known
606
+ (login / session restore / signup).
607
+ 2. track() \u2014 emit the specific business events listed in the integration
608
+ manifest, at the exact places in the code where those user actions happen.
609
+ These named events drive the customer's churn interventions, so placing
610
+ them at the correct call sites matters more than anything else you do.
611
+
612
+ Operating rules:
613
+ - Work autonomously. Make the edits directly; do not ask for confirmation.
614
+ - Be surgical. Add the SDK dependency, initialize it once at app startup, wire
615
+ identify() where the user becomes known, and add track() calls for the
616
+ manifest events. Do not refactor unrelated code, restyle, or "improve" things.
617
+ - Find the RIGHT call site for each named event by reading the code, not by
618
+ guessing. If you genuinely cannot find where an event occurs, add a clearly
619
+ commented TODO with the exact Whisperr call to make, and report it at the end
620
+ rather than firing the event from a wrong place.
621
+ - Never invent event types. Use the exact snake_case event_type strings from the
622
+ manifest. Custom data goes under properties.
623
+ - Keep the API key out of source where the SDK/platform supports env/secrets;
624
+ otherwise place it where the manifest guidance says and flag it.
625
+ - Match the repository's existing conventions (imports, formatting, state
626
+ management, file layout).
627
+
628
+ When done, output a concise summary:
629
+ - files changed
630
+ - where identify() was wired
631
+ - each manifest event -> the call site you instrumented (or TODO + reason)
632
+ - any follow-ups the human must do (env var, push token, etc.)
633
+ `.trim();
634
+ function renderManifestBrief(m) {
635
+ const lines = [];
636
+ lines.push(`# Whisperr integration manifest for app: ${m.appName ?? m.appId}`);
637
+ lines.push("");
638
+ lines.push(`Ingestion base URL: ${m.ingestionBaseUrl}`);
639
+ lines.push(`Ingestion API key: ${m.ingestionApiKey}`);
640
+ if (m.businessContext) {
641
+ lines.push("");
642
+ lines.push("## Business context");
643
+ lines.push(m.businessContext);
644
+ }
645
+ lines.push("");
646
+ lines.push("## identify()");
647
+ if (m.identify.traits?.length) {
648
+ lines.push("Traits to send when available:");
649
+ for (const t of m.identify.traits) {
650
+ lines.push(` - ${t.name}${t.description ? ` \u2014 ${t.description}` : ""}`);
651
+ }
652
+ }
653
+ if (m.identify.channels?.length) {
654
+ lines.push(`Channels in use: ${m.identify.channels.join(", ")}`);
655
+ }
656
+ if (m.identify.notes) lines.push(m.identify.notes);
657
+ lines.push("");
658
+ lines.push("## Events to instrument (use these exact event_type strings)");
659
+ const events = [...m.events].sort(
660
+ (a, b) => (b.importance ?? 0) - (a.importance ?? 0)
661
+ );
662
+ for (const e of events) {
663
+ lines.push("");
664
+ lines.push(`### ${e.eventType}${e.label ? ` (${e.label})` : ""}`);
665
+ if (e.description) lines.push(e.description);
666
+ if (e.properties?.length) {
667
+ lines.push("Properties:");
668
+ for (const p2 of e.properties) {
669
+ lines.push(
670
+ ` - ${p2.name}${p2.required ? " (required)" : ""}${p2.description ? ` \u2014 ${p2.description}` : ""}`
671
+ );
672
+ }
673
+ }
674
+ if (e.interventions?.length) {
675
+ const drv = e.interventions.map((i) => i.label ?? i.code).join(", ");
676
+ lines.push(`Drives: ${drv}`);
677
+ }
678
+ }
679
+ return lines.join("\n");
680
+ }
681
+
682
+ // src/core/agent.ts
683
+ async function runIntegrationAgent(opts) {
684
+ const { repoPath, config, session, playbook, manifest, progress } = opts;
685
+ applyModelAuthEnv(config, session);
686
+ const systemPrompt6 = [BASE_WIZARD_PROMPT, playbook.systemPrompt].join("\n\n");
687
+ const verifyHint = playbook.verifyCommand ? `
688
+
689
+ When finished, run \`${playbook.verifyCommand}\` and fix anything it flags.` : "";
690
+ const prompt = [
691
+ `Integrate the Whisperr ${playbook.target.displayName} SDK into this repository.`,
692
+ `Project root: ${repoPath}`,
693
+ "",
694
+ "Here is the integration manifest derived from this customer's Whisperr",
695
+ "onboarding. Wire identify() and instrument every event below at the correct",
696
+ "call site, following the SDK guide in your system prompt.",
697
+ "",
698
+ "----- MANIFEST -----",
699
+ renderManifestBrief(manifest),
700
+ "----- END MANIFEST -----",
701
+ verifyHint
702
+ ].join("\n");
703
+ const started = Date.now();
704
+ let summary = "";
705
+ let costUsd;
706
+ let ok = false;
707
+ const response = query({
708
+ prompt,
709
+ options: {
710
+ model: config.model,
711
+ cwd: repoPath,
712
+ systemPrompt: systemPrompt6,
713
+ // Full file-system agent toolset.
714
+ allowedTools: ["Read", "Edit", "Write", "Bash", "Glob", "Grep"],
715
+ // Fully autonomous edits (the user opted into auto-apply; the wizard takes
716
+ // a git checkpoint before this runs so revert is one command).
717
+ permissionMode: "bypassPermissions",
718
+ maxTurns: config.maxTurns,
719
+ // Don't pick up the user's own CLAUDE.md / settings — the wizard's prompt
720
+ // is the single source of truth.
721
+ settingSources: []
722
+ }
723
+ });
724
+ for await (const message of response) {
725
+ if (message.type === "assistant") {
726
+ for (const block of message.message?.content ?? []) {
727
+ if (isTextBlock(block) && block.text?.trim()) {
728
+ progress?.onActivity?.(firstLine(block.text));
729
+ } else if (isToolUseBlock(block)) {
730
+ progress?.onActivity?.(describeToolUse(block));
731
+ }
732
+ }
733
+ } else if (message.type === "result") {
734
+ ok = message.subtype === "success";
735
+ costUsd = message.total_cost_usd;
736
+ summary = message.result ?? extractLastText(message) ?? summary;
737
+ }
738
+ }
739
+ progress?.onResult?.(summary, costUsd);
740
+ return { summary, costUsd, ok, durationMs: Date.now() - started };
741
+ }
742
+ function applyModelAuthEnv(config, session) {
743
+ if (config.directAnthropicKey) {
744
+ process.env.ANTHROPIC_API_KEY = config.directAnthropicKey;
745
+ delete process.env.ANTHROPIC_AUTH_TOKEN;
746
+ return;
747
+ }
748
+ process.env.ANTHROPIC_BASE_URL = config.llmBaseUrl;
749
+ process.env.ANTHROPIC_AUTH_TOKEN = session.token;
750
+ delete process.env.ANTHROPIC_API_KEY;
751
+ }
752
+ function isTextBlock(b) {
753
+ return b.type === "text";
754
+ }
755
+ function isToolUseBlock(b) {
756
+ return b.type === "tool_use";
757
+ }
758
+ function describeToolUse(block) {
759
+ const file = block.input?.file_path ?? block.input?.path ?? block.input?.pattern ?? "";
760
+ switch (block.name) {
761
+ case "Edit":
762
+ return `Editing ${short(file)}`;
763
+ case "Write":
764
+ return `Writing ${short(file)}`;
765
+ case "Read":
766
+ return `Reading ${short(file)}`;
767
+ case "Bash":
768
+ return `Running ${short(block.input?.command ?? "command")}`;
769
+ case "Grep":
770
+ return `Searching for ${short(file)}`;
771
+ case "Glob":
772
+ return `Scanning ${short(file)}`;
773
+ default:
774
+ return `${block.name ?? "Working"}\u2026`;
775
+ }
776
+ }
777
+ function extractLastText(message) {
778
+ const blocks = message.message?.content ?? [];
779
+ for (let i = blocks.length - 1; i >= 0; i--) {
780
+ const b = blocks[i];
781
+ if (b?.type === "text" && b.text) return b.text;
782
+ }
783
+ return void 0;
784
+ }
785
+ function firstLine(text) {
786
+ const line = text.split("\n").find((l) => l.trim()) ?? text;
787
+ return short(line.trim(), 100);
788
+ }
789
+ function short(s, max = 60) {
790
+ return s.length > max ? `${s.slice(0, max - 1)}\u2026` : s;
791
+ }
792
+
793
+ // src/core/git.ts
794
+ import { spawn } from "child_process";
795
+ async function takeCheckpoint(repoPath) {
796
+ const isRepo = (await run(repoPath, ["rev-parse", "--is-inside-work-tree"])).ok;
797
+ if (!isRepo) return { isRepo: false };
798
+ const branch = (await run(repoPath, ["rev-parse", "--abbrev-ref", "HEAD"])).stdout.trim();
799
+ const head = await run(repoPath, ["rev-parse", "HEAD"]);
800
+ return {
801
+ isRepo: true,
802
+ baseRef: head.ok ? head.stdout.trim() : void 0,
803
+ branch: branch || void 0
804
+ };
805
+ }
806
+ async function changedFiles(repoPath, checkpoint) {
807
+ if (!checkpoint.isRepo) return [];
808
+ const tracked = await run(repoPath, ["diff", "--name-only"]);
809
+ const untracked = await run(repoPath, [
810
+ "ls-files",
811
+ "--others",
812
+ "--exclude-standard"
813
+ ]);
814
+ const set = /* @__PURE__ */ new Set();
815
+ for (const f of `${tracked.stdout}
816
+ ${untracked.stdout}`.split("\n")) {
817
+ if (f.trim()) set.add(f.trim());
818
+ }
819
+ return [...set];
820
+ }
821
+ function revertHint(checkpoint) {
822
+ if (!checkpoint.isRepo || !checkpoint.baseRef) return void 0;
823
+ return `git restore . && git clean -fd (back to ${checkpoint.baseRef.slice(0, 7)})`;
824
+ }
825
+ function run(cwd, args) {
826
+ return new Promise((resolve2) => {
827
+ const child = spawn("git", args, { cwd });
828
+ let stdout = "";
829
+ let stderr = "";
830
+ child.stdout.on("data", (d) => stdout += d.toString());
831
+ child.stderr.on("data", (d) => stderr += d.toString());
832
+ child.on("close", (code) => resolve2({ ok: code === 0, stdout, stderr }));
833
+ child.on("error", () => resolve2({ ok: false, stdout, stderr }));
834
+ });
835
+ }
836
+
837
+ // src/core/verify.ts
838
+ async function pollFirstEvent(config, session, opts = {}) {
839
+ if (config.offline) return { received: false };
840
+ const timeoutMs = opts.timeoutMs ?? 12e4;
841
+ const intervalMs = opts.intervalMs ?? 3e3;
842
+ const deadline = Date.now() + timeoutMs;
843
+ while (Date.now() < deadline) {
844
+ if (opts.signal?.aborted) return { received: false };
845
+ try {
846
+ const res = await fetch(`${config.apiBaseUrl}/wizard/first-event`, {
847
+ headers: { authorization: `Bearer ${session.token}` }
848
+ });
849
+ if (res.ok) {
850
+ const body = await res.json();
851
+ if (body.received) {
852
+ return { received: true, eventType: body.event_type };
853
+ }
854
+ }
855
+ } catch {
856
+ }
857
+ await new Promise((r) => setTimeout(r, intervalMs));
858
+ }
859
+ return { received: false };
860
+ }
861
+
862
+ // src/ui/banner.ts
863
+ function banner() {
864
+ const wave = theme.signal("\u223F\u223F\u223F");
865
+ const title = `${theme.bright("Whisperr")} ${theme.signal("Wizard")}`;
866
+ const tagline = theme.muted(
867
+ "Integrate Whisperr into your app \u2014 one command, fully wired."
868
+ );
869
+ return [
870
+ "",
871
+ ` ${wave} ${title}`,
872
+ ` ${tagline}`,
873
+ ""
874
+ ].join("\n");
875
+ }
876
+
877
+ // src/cli.ts
878
+ async function run2(options) {
879
+ const repoPath = resolve(options.path ?? process.cwd());
880
+ const config = resolveConfig(options);
881
+ console.log(banner());
882
+ p.intro(theme.signal("Let's wire Whisperr into your app."));
883
+ if (config.offline) {
884
+ p.log.warn(
885
+ theme.warn("offline mode") + theme.muted(" \u2014 using a demo manifest, no account needed.")
886
+ );
887
+ }
888
+ const detections = await withSpinner(
889
+ "Scanning your repository",
890
+ () => detectStack(repoPath)
891
+ );
892
+ const chosen = await chooseTarget(detections);
893
+ if (!chosen) {
894
+ p.cancel("No supported stack selected.");
895
+ return 1;
896
+ }
897
+ if (chosen.playbook.target.availability === "planned") {
898
+ p.note(
899
+ `We can detect ${theme.bright(chosen.playbook.target.displayName)} but the Whisperr SDK for it isn't shipped yet.
900
+ Flutter is live today; ${theme.bright(
901
+ chosen.playbook.target.displayName
902
+ )} is next on the roadmap.`,
903
+ theme.warn("SDK coming soon")
904
+ );
905
+ const cont = await p.confirm({
906
+ message: "Continue anyway and let the agent scaffold against the planned SDK?",
907
+ initialValue: false
908
+ });
909
+ if (p.isCancel(cont) || !cont) {
910
+ p.outro(theme.muted("No problem \u2014 come back when the SDK lands."));
911
+ return 0;
912
+ }
913
+ } else {
914
+ p.log.success(
915
+ `Detected ${theme.bright(chosen.playbook.target.displayName)} ` + theme.muted(`(${chosen.detection?.evidence.join(", ") ?? "selected"})`)
916
+ );
917
+ }
918
+ let session;
919
+ try {
920
+ session = config.offline ? await authenticate(config) : await withBrowserAuth(config);
921
+ } catch (err) {
922
+ p.cancel(theme.alert(err.message));
923
+ return 1;
924
+ }
925
+ const manifest = await withSpinner(
926
+ "Loading your onboarding context",
927
+ () => fetchManifest(config, session, chosen.playbook.target.id)
928
+ );
929
+ p.note(
930
+ summarizeManifest(manifest),
931
+ theme.signal(`Plan for ${manifest.appName ?? manifest.appId}`)
932
+ );
933
+ const checkpoint = await takeCheckpoint(repoPath);
934
+ if (!checkpoint.isRepo) {
935
+ p.log.warn(
936
+ theme.warn("not a git repo") + theme.muted(" \u2014 edits will apply with no automatic undo.")
937
+ );
938
+ }
939
+ const spin = p.spinner();
940
+ spin.start(theme.bright("Integrating") + theme.muted(" \u2014 the agent is working in your code"));
941
+ let lastLine = "";
942
+ const outcome = await runIntegrationAgent({
943
+ repoPath,
944
+ config,
945
+ session,
946
+ playbook: chosen.playbook,
947
+ manifest,
948
+ progress: {
949
+ onActivity(line) {
950
+ lastLine = line;
951
+ spin.message(theme.bright("Integrating") + theme.muted(` \u2014 ${line}`));
952
+ }
953
+ }
954
+ }).catch((err) => {
955
+ spin.stop(theme.alert("Integration failed"));
956
+ p.log.error(err.message);
957
+ return null;
958
+ });
959
+ if (!outcome) return 1;
960
+ spin.stop(
961
+ outcome.ok ? theme.success("Integration complete") : theme.warn("Integration finished with notes")
962
+ );
963
+ const files = await changedFiles(repoPath, checkpoint);
964
+ if (files.length) {
965
+ p.note(files.map((f) => theme.muted("\u2022 ") + f).join("\n"), "Files changed");
966
+ }
967
+ if (outcome.summary.trim()) {
968
+ p.log.message(outcome.summary.trim());
969
+ }
970
+ if (!config.offline) {
971
+ p.log.step(
972
+ theme.bright("Run your app once") + theme.muted(" and trigger any tracked action \u2014 I'll watch for the first event.")
973
+ );
974
+ const verifySpin = p.spinner();
975
+ verifySpin.start("Waiting for the first event from your app");
976
+ const first = await pollFirstEvent(config, session, { timeoutMs: 12e4 });
977
+ if (first.received) {
978
+ verifySpin.stop(
979
+ theme.success(
980
+ `Whisperr is receiving events \u2713` + (first.eventType ? theme.muted(` (first: ${first.eventType})`) : "")
981
+ )
982
+ );
983
+ } else {
984
+ verifySpin.stop(
985
+ theme.warn(
986
+ "No events yet \u2014 that's fine. They'll flow once you run the app with the changes."
987
+ )
988
+ );
989
+ }
990
+ }
991
+ const undo = revertHint(checkpoint);
992
+ const nextSteps = [
993
+ `${theme.signal("1.")} Review the diff above.`,
994
+ `${theme.signal("2.")} Run / rebuild your app to start sending events.`,
995
+ `${theme.signal("3.")} Commit & deploy \u2014 Whisperr starts working on real users.`,
996
+ undo ? theme.muted(`Undo everything: ${undo}`) : ""
997
+ ].filter(Boolean).join("\n");
998
+ p.note(nextSteps, theme.bright("Next"));
999
+ p.outro(theme.signal("\u2301 ") + theme.bright("Whisperr is wired in."));
1000
+ void lastLine;
1001
+ return 0;
1002
+ }
1003
+ async function chooseTarget(detections) {
1004
+ const top = detections[0];
1005
+ if (top && (detections.length === 1 || top.confidence >= 0.9)) {
1006
+ const playbook2 = playbookByTargetId(top.target.id);
1007
+ if (playbook2) return { playbook: playbook2, detection: top };
1008
+ }
1009
+ const seen = /* @__PURE__ */ new Set();
1010
+ const options = [];
1011
+ for (const d of detections) {
1012
+ if (seen.has(d.target.id)) continue;
1013
+ seen.add(d.target.id);
1014
+ options.push({
1015
+ value: d.target.id,
1016
+ label: d.target.displayName,
1017
+ hint: (d.target.availability === "available" ? "" : "coming soon \xB7 ") + `${Math.round(d.confidence * 100)}% match`
1018
+ });
1019
+ }
1020
+ for (const pb of ALL_PLAYBOOKS) {
1021
+ if (seen.has(pb.target.id)) continue;
1022
+ options.push({
1023
+ value: pb.target.id,
1024
+ label: pb.target.displayName,
1025
+ hint: pb.target.availability === "available" ? void 0 : "coming soon"
1026
+ });
1027
+ }
1028
+ const answer = await p.select({
1029
+ message: detections.length ? "I found more than one possible stack \u2014 which should I integrate?" : "I couldn't auto-detect your stack. Which are you using?",
1030
+ options
1031
+ });
1032
+ if (p.isCancel(answer)) return null;
1033
+ const playbook = playbookByTargetId(answer);
1034
+ if (!playbook) return null;
1035
+ return {
1036
+ playbook,
1037
+ detection: detections.find((d) => d.target.id === answer)
1038
+ };
1039
+ }
1040
+ async function withBrowserAuth(config) {
1041
+ p.log.step(
1042
+ theme.bright("Authenticate") + theme.muted(" \u2014 opening your browser to approve this device\u2026")
1043
+ );
1044
+ const spin = p.spinner();
1045
+ spin.start("Waiting for you to approve in the browser");
1046
+ try {
1047
+ const session = await authenticate(config);
1048
+ spin.stop(theme.success("Authenticated \u2713"));
1049
+ return session;
1050
+ } catch (err) {
1051
+ spin.stop(theme.alert("Authentication failed"));
1052
+ throw err;
1053
+ }
1054
+ }
1055
+ async function withSpinner(label, fn) {
1056
+ const spin = p.spinner();
1057
+ spin.start(label);
1058
+ try {
1059
+ const result = await fn();
1060
+ spin.stop(theme.muted(label) + " " + theme.success("\u2713"));
1061
+ return result;
1062
+ } catch (err) {
1063
+ spin.stop(theme.alert(`${label} \u2014 failed`));
1064
+ throw err;
1065
+ }
1066
+ }
1067
+ function summarizeManifest(m) {
1068
+ const events = [...m.events].sort((a, b) => (b.importance ?? 0) - (a.importance ?? 0)).map((e) => theme.muted("\u2022 ") + e.eventType);
1069
+ const channels = m.identify.channels?.length ? theme.muted(`channels: ${m.identify.channels.join(", ")}`) : "";
1070
+ return [
1071
+ theme.bright("identify()") + theme.muted(" + ") + theme.bright(`${m.events.length} events`),
1072
+ ...events,
1073
+ channels
1074
+ ].filter(Boolean).join("\n");
1075
+ }
1076
+
1077
+ // src/index.ts
1078
+ function parseArgs(argv) {
1079
+ const args = [...argv];
1080
+ const opts = {};
1081
+ if (args[0] === "init") args.shift();
1082
+ for (let i = 0; i < args.length; i++) {
1083
+ const a = args[i];
1084
+ switch (a) {
1085
+ case "-h":
1086
+ case "--help":
1087
+ return { help: true };
1088
+ case "-v":
1089
+ case "--version":
1090
+ return { version: true };
1091
+ case "--offline":
1092
+ opts.offline = true;
1093
+ break;
1094
+ case "--api":
1095
+ opts.apiBaseUrl = args[++i];
1096
+ break;
1097
+ case "--model":
1098
+ opts.model = args[++i];
1099
+ break;
1100
+ default:
1101
+ if (a && !a.startsWith("-") && !opts.path) {
1102
+ opts.path = a;
1103
+ }
1104
+ }
1105
+ }
1106
+ return opts;
1107
+ }
1108
+ function printHelp() {
1109
+ console.log(
1110
+ [
1111
+ "",
1112
+ ` ${theme.bright("Whisperr Wizard")} \u2014 integrate Whisperr into your app.`,
1113
+ "",
1114
+ ` ${theme.bright("Usage")}`,
1115
+ " npx @whisperr/wizard [init] [path] [options]",
1116
+ "",
1117
+ ` ${theme.bright("Options")}`,
1118
+ " --offline Use a demo manifest, no account/browser needed",
1119
+ " --api <url> Override the Whisperr API base URL",
1120
+ " --model <id> Override the coding-agent model",
1121
+ " -h, --help Show this help",
1122
+ " -v, --version Show version",
1123
+ "",
1124
+ ` ${theme.muted("The wizard detects your stack, authenticates with your")}`,
1125
+ ` ${theme.muted("onboarded account, and wires up identify() + your events.")}`,
1126
+ ""
1127
+ ].join("\n")
1128
+ );
1129
+ }
1130
+ async function main() {
1131
+ const parsed = parseArgs(process.argv.slice(2));
1132
+ if ("help" in parsed) {
1133
+ printHelp();
1134
+ return;
1135
+ }
1136
+ if ("version" in parsed) {
1137
+ console.log("0.1.0");
1138
+ return;
1139
+ }
1140
+ try {
1141
+ const code = await run2(parsed);
1142
+ process.exit(code);
1143
+ } catch (err) {
1144
+ console.error("\n" + theme.alert("Unexpected error: ") + err.message);
1145
+ process.exit(1);
1146
+ }
1147
+ }
1148
+ void main();
1149
+ //# sourceMappingURL=index.js.map