@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.
- package/dist/index.js +211 -52
- 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-
|
|
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: "
|
|
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
|
|
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
|
-
|
|
662
|
+
hasRN = "react-native" in all;
|
|
663
|
+
hasExpo = "expo" in all;
|
|
662
664
|
} catch {
|
|
663
|
-
match = false;
|
|
664
665
|
}
|
|
665
|
-
if (!
|
|
666
|
-
const evidence = [
|
|
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
|
-
|
|
678
|
-
|
|
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
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
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
|
-
-
|
|
693
|
-
|
|
694
|
-
|
|
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: "
|
|
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
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
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
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
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: "
|
|
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
|
|
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.
|
|
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": "
|
|
14
|
+
"whisperr": "dist/index.js"
|
|
10
15
|
},
|
|
11
16
|
"files": [
|
|
12
17
|
"dist"
|