@whisperr/wizard 0.1.14 → 0.2.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.
Files changed (2) hide show
  1. package/dist/index.js +211 -52
  2. package/package.json +7 -2
package/dist/index.js CHANGED
@@ -11,7 +11,7 @@ import * as p from "@clack/prompts";
11
11
 
12
12
  // src/core/config.ts
13
13
  var DEFAULT_API_BASE = "https://api.whisperr.net";
14
- var DEFAULT_MODEL = "claude-sonnet-4-6";
14
+ var DEFAULT_MODEL = "claude-sonnet-5";
15
15
  var DEFAULT_EFFORT = "high";
16
16
  var EFFORT_LEVELS = ["low", "medium", "high", "xhigh", "max"];
17
17
  function resolveEffort() {
@@ -647,51 +647,135 @@ var phpPlaybook = {
647
647
  // src/core/playbooks/react-native.ts
648
648
  var target7 = {
649
649
  id: "react-native",
650
- displayName: "React Native",
650
+ displayName: "React Native / Expo",
651
651
  language: "typescript",
652
- availability: "planned"
652
+ availability: "available"
653
653
  };
654
654
  async function detect7(ctx) {
655
655
  if (!await ctx.exists("package.json")) return null;
656
656
  const pkgJson = await ctx.read("package.json");
657
- let match = false;
657
+ let hasRN = false;
658
+ let hasExpo = false;
658
659
  try {
659
660
  const pkg = JSON.parse(pkgJson);
660
661
  const all = { ...pkg.dependencies ?? {}, ...pkg.devDependencies ?? {} };
661
- match = "react-native" in all || "expo" in all;
662
+ hasRN = "react-native" in all;
663
+ hasExpo = "expo" in all;
662
664
  } catch {
663
- match = false;
664
665
  }
665
- if (!match) return null;
666
- const evidence = ["react-native / expo dependency"];
666
+ if (!hasRN && !hasExpo) return null;
667
+ const evidence = [];
668
+ if (hasExpo) evidence.push("expo dependency");
669
+ if (hasRN) evidence.push("react-native dependency");
667
670
  let confidence = 0.85;
668
671
  if (await ctx.exists("app.json")) {
669
672
  evidence.push("app.json");
670
673
  confidence += 0.1;
674
+ } else if (await ctx.exists("app.config.js") || await ctx.exists("app.config.ts")) {
675
+ evidence.push("app.config");
676
+ confidence += 0.1;
671
677
  }
672
678
  return { target: target7, confidence: Math.min(confidence, 1), evidence };
673
679
  }
674
680
  var systemPrompt7 = `
675
681
  ## SDK: Whisperr for React Native \u2014 package \`@whisperr/react-native\`
676
682
 
677
- 1) Install \`@whisperr/react-native\` with the repo's package manager. If it has
678
- native modules, run pod install for iOS (mention it; don't attempt to build).
683
+ Pure TypeScript, ZERO native code and zero dependencies of its own: it works in
684
+ Expo Go, bare React Native, and dev clients with no pod install, no config
685
+ plugin, no prebuild. Do not add or modify anything under ios/ or android/ for
686
+ the SDK itself.
687
+
688
+ 0) Expo or bare? Decide first \u2014 it changes storage install + key handling.
689
+ Expo = \`expo\` in package.json dependencies (usually with app.json /
690
+ app.config.* containing an "expo" key).
691
+
692
+ 1) Install with the repo's package manager (pick by lockfile: pnpm-lock.yaml \u2192
693
+ pnpm, yarn.lock \u2192 yarn, bun.lockb \u2192 bun, else npm):
694
+ <pm> add @whisperr/react-native
695
+
696
+ 2) Durable storage \u2014 IMPORTANT. The SDK's offline queue is MEMORY-ONLY unless
697
+ you inject an AsyncStorage-compatible adapter (\`getItem\`/\`setItem\`/
698
+ \`removeItem\`); the SDK never imports native modules itself. In priority
699
+ order:
700
+ - \`@react-native-async-storage/async-storage\` already a dependency \u2192 import
701
+ it and pass as \`storage\`.
702
+ - The app already uses \`react-native-mmkv\` \u2192 reuse its existing instance
703
+ with a 3-line adapter (do NOT add a new storage dep):
704
+ const storage = {
705
+ getItem: (k: string) => mmkv.getString(k) ?? null,
706
+ setItem: (k: string, v: string) => { mmkv.set(k, v); },
707
+ removeItem: (k: string) => { mmkv.delete(k); },
708
+ };
709
+ - Neither \u2192 add async-storage. Expo: run \`npx expo install
710
+ @react-native-async-storage/async-storage\` via bash (installs the
711
+ SDK-matched version; works in Expo Go \u2014 it ships in the Go client). Bare
712
+ RN: \`<pm> add @react-native-async-storage/async-storage\` and note
713
+ "run npx pod-install before the next iOS build" as a follow-up (it IS a
714
+ native module; do not run pod install yourself).
715
+ Never silently ship without storage \u2014 if you genuinely can't add it, say so
716
+ in the summary.
717
+
718
+ 3) Initialize ONCE via a module singleton, e.g. src/whisperr.ts (match the
719
+ repo's source layout \u2014 src/, lib/, app/lib/\u2026):
720
+ import AsyncStorage from '@react-native-async-storage/async-storage';
721
+ import { Whisperr } from '@whisperr/react-native';
679
722
 
680
- 2) Initialize once at the app root (App.tsx / app/_layout.tsx for Expo Router):
681
- import { Whisperr } from '@whisperr/react-native';
682
- Whisperr.init({ apiKey: <key from manifest/env>, baseUrl: <manifest baseUrl> });
683
-
684
- 3) identify() after auth resolves / on session restore; reset() on logout.
685
- For channels: email/phone where known, and the push token (expo-notifications
686
- or @react-native-firebase/messaging) if the app has push set up.
687
-
688
- 4) track() in event handlers / business logic, never in render. event_type
689
- verbatim from the manifest.
723
+ export const whisperr = Whisperr.init({
724
+ apiKey: process.env.EXPO_PUBLIC_WHISPERR_KEY!,
725
+ storage: AsyncStorage,
726
+ baseUrl: '<INGESTION_BASE_URL from manifest>', // omit if it's the SDK default
727
+ });
728
+ Import the module for its side effect from the app entry: Expo Router \u2192
729
+ app/_layout.tsx (root layout); bare / React Navigation \u2192 App.tsx (or
730
+ index.js). Prefer the singleton over <WhisperrProvider> \u2014 auth listeners,
731
+ API clients, and non-component code can import it directly. If the codebase
732
+ is strongly hook-oriented you may ADDITIONALLY wrap the root with
733
+ <WhisperrProvider client={whisperr}> and use useWhisperr() in components \u2014
734
+ but never create a second client.
735
+ Whisperr.init() is an idempotent singleton \u2014 safe under Fast Refresh and
736
+ React StrictMode.
737
+
738
+ Key handling (the key is a publishable ingestion key \u2014 shipping it in the
739
+ bundle is expected):
740
+ - Expo SDK 49+: put EXPO_PUBLIC_WHISPERR_KEY=<key> in .env (create or
741
+ append) and mirror it in .env.example; reference via
742
+ process.env.EXPO_PUBLIC_WHISPERR_KEY.
743
+ - Bare RN with react-native-config: add WHISPERR_KEY to .env and read
744
+ Config.WHISPERR_KEY.
745
+ - Otherwise: a small constants file with the literal key is acceptable.
746
+
747
+ 4) identify() right after the end-user is known \u2014 on login/signup success AND
748
+ on session restore at app startup:
749
+ whisperr.identify(user.id, {
750
+ traits: { /* manifest traits sourced from the user object */ },
751
+ email: user.email, // shortcut \u2192 opted-in email channel
752
+ phone: user.phone, // shortcut \u2192 sms channel
753
+ pushToken: expoPushToken, // shortcut \u2192 push channel, if the app
754
+ // registers for push (expo-notifications
755
+ // or @react-native-firebase/messaging)
756
+ });
757
+ Call whisperr.reset() on logout (already-queued events keep their user).
758
+ ANONYMOUS EVENTS ARE FINE: track() before identify() buffers on-device and
759
+ attributes to the user retroactively on identify() \u2014 do NOT gate track()
760
+ calls behind auth state, and do NOT skip pre-login manifest events.
761
+
762
+ 5) track() in event handlers / business logic (onPress, mutation onSuccess,
763
+ store/saga/thunk actions) \u2014 never in render:
764
+ whisperr.track('event_type_from_manifest', { /* properties in scope */ });
765
+ Synchronous fire-and-forget: it returns void \u2014 do NOT await it. Batching,
766
+ retries, background flush are all internal. event_type verbatim snake_case
767
+ from the manifest \u2014 the SDK client-side drops invalid names.
690
768
 
691
- Notes:
692
- - Persisted offline queue is built in (AsyncStorage); fire-and-forget is fine.
693
- - Prefer an env/config mechanism (react-native-config, app.config) for the key
694
- if one exists; otherwise a constants file is acceptable.
769
+ Notes / gotchas:
770
+ - Every call is sync void except flush(); the only sensible manual flush is
771
+ \`await whisperr.flush()\` right before something destroys the process.
772
+ - The SDK auto-flushes on an interval, at 20 queued events, and when the app
773
+ backgrounds. Don't sprinkle flush() calls.
774
+ - whisperr.screen(name) records a screen_viewed event \u2014 wire it to the
775
+ navigation container's state-change callback ONLY if the manifest includes a
776
+ screen/page-view style event; otherwise leave navigation alone.
777
+ - Works on the New Architecture and in Expo Go by construction (no native
778
+ code). Never suggest expo prebuild / dev-client for this SDK.
695
779
  `.trim();
696
780
  var reactNativePlaybook = {
697
781
  target: target7,
@@ -703,9 +787,9 @@ var reactNativePlaybook = {
703
787
  // src/core/playbooks/swift.ts
704
788
  var target8 = {
705
789
  id: "swift",
706
- displayName: "Swift (iOS)",
790
+ displayName: "Swift (iOS / Apple)",
707
791
  language: "swift",
708
- availability: "planned"
792
+ availability: "available"
709
793
  };
710
794
  async function detect8(ctx) {
711
795
  const evidence = [];
@@ -727,40 +811,115 @@ async function detect8(ctx) {
727
811
  evidence.push("Podfile");
728
812
  confidence += 0.2;
729
813
  }
814
+ if (await ctx.exists("Project.swift")) {
815
+ evidence.push("Project.swift (Tuist)");
816
+ confidence += 0.4;
817
+ }
818
+ if (await ctx.exists("project.yml")) {
819
+ evidence.push("project.yml (XcodeGen)");
820
+ confidence += 0.3;
821
+ }
730
822
  if (confidence === 0) return null;
731
823
  return { target: target8, confidence: Math.min(confidence, 1), evidence };
732
824
  }
733
825
  var systemPrompt8 = `
734
- ## SDK: Whisperr for Swift (iOS) \u2014 Swift Package \`Whisperr\`
735
-
736
- 1) Add the Swift Package dependency (github.com/WhisperrAI/whisperr-swift). If the
737
- project uses Package.swift, add it there; if it's an .xcodeproj/.xcworkspace,
738
- you cannot edit the project graph reliably from the shell \u2014 instead leave a
739
- clearly commented setup note and the exact import + init code, and report it
740
- as a manual follow-up.
826
+ ## SDK: Whisperr for Swift \u2014 Swift Package \`Whisperr\` (github.com/WhisperrAI/whisperr-swift)
827
+
828
+ Platforms: iOS 13+, macOS 12+, tvOS 13+, watchOS 6+. The client is a Swift
829
+ ACTOR: every call is \`await\`ed, and identify()/track() are throwing. Getting
830
+ the concurrency and JSONValue details below right is what makes the diff
831
+ compile first try.
832
+
833
+ 1) Dependency \u2014 pick by how the project is defined:
834
+ - Package.swift: add to the package's dependencies
835
+ .package(url: "https://github.com/WhisperrAI/whisperr-swift.git", from: "0.1.0")
836
+ and \`"Whisperr"\` to the app target's dependencies array.
837
+ - Tuist (Project.swift) or XcodeGen (project.yml): these manifests are plain
838
+ text \u2014 add the SPM package + product there following the file's existing
839
+ style.
840
+ - Raw .xcodeproj / .xcworkspace only: do NOT hand-edit project.pbxproj to
841
+ add a package reference (fragile, breaks the project on a bad GUID).
842
+ Write ALL the integration code anyway (imports, init, identify, track),
843
+ add a short WHISPERR_SETUP.md with the one manual step \u2014 Xcode \u2192 File \u2192
844
+ Add Package Dependencies\u2026 \u2192 \`https://github.com/WhisperrAI/whisperr-swift\`
845
+ \u2192 add "Whisperr" to the app target \u2014 and report that as the single manual
846
+ follow-up in your summary.
847
+
848
+ 2) Initialize once at launch.
849
+ SwiftUI:
850
+ import Whisperr
851
+
852
+ @main
853
+ struct MyApp: App {
854
+ init() {
855
+ Task { await Whisperr.initialize(apiKey: "<INGESTION_API_KEY>") }
856
+ }
857
+ // ... existing body
858
+ }
859
+ UIKit: same Task { } inside application(_:didFinishLaunchingOptions:).
860
+ Only pass a base URL if the manifest's differs from the SDK default:
861
+ await Whisperr.initialize(apiKey: "...", baseURL: URL(string: "<INGESTION_BASE_URL>")!)
862
+ Note baseURL takes a URL, not a String. Prefer reading the key from
863
+ Info.plist / an .xcconfig if the project already uses that pattern;
864
+ otherwise a literal is acceptable (publishable ingestion key).
865
+
866
+ 3) identify() right after the end-user is known \u2014 login/signup success AND
867
+ session restore. All calls go through the async optional singleton
868
+ \`await Whisperr.shared\`; in synchronous contexts wrap in Task { }:
869
+ Task {
870
+ try? await Whisperr.shared?.identify(
871
+ user.id, // stable external id \u2014 never a device id
872
+ traits: ["plan": .string(user.plan)],
873
+ email: user.email, // shortcut \u2192 opted-in email channel
874
+ phone: user.phone, // shortcut \u2192 sms channel
875
+ pushToken: apnsTokenHex // if the app registers for remote notifications
876
+ )
877
+ }
878
+ Logout:
879
+ Task { await Whisperr.shared?.reset() } // flushes, then clears the user
880
+ For explicit channel consent/verification use WhisperrChannel:
881
+ channels: [.email("a@b.com", verified: true), .sms("+1555\u2026", optedIn: false)]
882
+
883
+ 4) track() in action handlers / view-model methods \u2014 never in a View body:
884
+ Task {
885
+ try? await Whisperr.shared?.track(
886
+ "event_type_from_manifest",
887
+ properties: ["amount_cents": .number(Double(amountCents)),
888
+ "plan": .string(plan)]
889
+ )
890
+ }
891
+ Awaiting only ENQUEUES (delivery batches in the background) \u2014 \`try? await\`
892
+ inside Task { } is the correct fire-and-forget in UI code. In an already-
893
+ async throwing context, plain \`try await\` is fine. event_type verbatim
894
+ snake_case from the manifest.
895
+
896
+ CRITICAL \u2014 JSONValue typing for properties/traits:
897
+ Values are \`JSONValue\`, not \`Any\`. LITERALS convert automatically \u2014
898
+ \`["plan": "pro", "amount": 42, "active": true]\` compiles as-is \u2014 but
899
+ VARIABLES must be wrapped: \`.string(user.plan)\`, \`.number(Double(count))\`,
900
+ \`.bool(flag)\`, \`.array(...)\`, \`.object(...)\`. Mixing literals and wrapped
901
+ variables in one dictionary is fine. Forgetting the wrapper is the #1
902
+ compile error \u2014 check every non-literal value you pass.
903
+
904
+ CRITICAL \u2014 no anonymous buffering in this SDK:
905
+ track() THROWS WhisperrClientError.missingUserID until identify() has run in
906
+ this process (or when you pass \`userID:\` explicitly). Session restore must
907
+ therefore call identify() early at startup. Do not place track() on
908
+ pre-auth surfaces (onboarding, login screen) unless you pass
909
+ \`userID: "<id>"\` \u2014 if a manifest event genuinely happens before the user
910
+ exists, skip it and note it in the summary.
741
911
 
742
- 2) Initialize once at launch in the App struct (SwiftUI) or
743
- application(_:didFinishLaunchingWithOptions:) (UIKit):
744
- import Whisperr
745
- Whisperr.initialize(apiKey: "<key>", baseUrl: "<manifest baseUrl>")
746
-
747
- 3) identify() after the user is known / on session restore:
748
- Whisperr.shared.identify("<external user id>", traits: [...], email: ..., phone: ...)
749
- reset() on logout. Push channel = APNs device token if the app registers for
750
- remote notifications.
751
-
752
- 4) track() in action handlers / view-model methods, not in view body:
753
- Whisperr.shared.track("event_type_from_manifest", properties: [ ... ])
754
- event_type verbatim (snake_case) from the manifest.
755
-
756
- Notes:
757
- - Prefer reading the key from Info.plist / an xcconfig if the project uses one.
758
- - Do not attempt to run xcodebuild; verification is the human's step here.
912
+ Notes / gotchas:
913
+ - \`Whisperr.shared\` is \`async\` and optional \u2014 the call shape is always
914
+ \`await Whisperr.shared?.method(...)\`. Never store it in a non-async global.
915
+ - The queue persists via UserDefaults and survives restarts; retries, backoff,
916
+ and auth-pause semantics are built in. No manual flush() needed.
917
+ - Do not run xcodebuild, swift build, or tests \u2014 the human verifies in Xcode.
759
918
  `.trim();
760
919
  var swiftPlaybook = {
761
920
  target: target8,
762
921
  detect: detect8,
763
- packageRef: "Whisperr (Swift Package)",
922
+ packageRef: "whisperr-swift (Swift Package)",
764
923
  systemPrompt: systemPrompt8
765
924
  };
766
925
 
@@ -1331,7 +1490,7 @@ async function runPass(opts) {
1331
1490
  cwd: repoPath,
1332
1491
  systemPrompt: systemPrompt9,
1333
1492
  // Adaptive thinking (Claude decides depth per step) + an explicit effort
1334
- // level. Sonnet 4.6 supports both; we set them rather than rely on SDK
1493
+ // level. Sonnet 5 supports both; we set them rather than rely on SDK
1335
1494
  // defaults so the behavior is pinned regardless of SDK version.
1336
1495
  thinking: { type: "adaptive" },
1337
1496
  effort,
package/package.json CHANGED
@@ -1,12 +1,17 @@
1
1
  {
2
2
  "name": "@whisperr/wizard",
3
- "version": "0.1.14",
3
+ "version": "0.2.0",
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
+ "repository": {
6
+ "type": "git",
7
+ "url": "git+https://github.com/WhisperrAI/whisperr-wizard.git"
8
+ },
9
+ "homepage": "https://github.com/WhisperrAI/whisperr-wizard#readme",
5
10
  "license": "UNLICENSED",
6
11
  "private": false,
7
12
  "type": "module",
8
13
  "bin": {
9
- "whisperr": "./dist/index.js"
14
+ "whisperr": "dist/index.js"
10
15
  },
11
16
  "files": [
12
17
  "dist"