@whisperr/wizard 0.1.4 → 0.1.6

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 +141 -50
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -113,7 +113,7 @@ var target2 = {
113
113
  id: "nextjs",
114
114
  displayName: "Next.js",
115
115
  language: "typescript",
116
- availability: "planned"
116
+ availability: "available"
117
117
  };
118
118
  async function detect2(ctx) {
119
119
  if (!await ctx.exists("package.json")) return null;
@@ -140,34 +140,37 @@ async function detect2(ctx) {
140
140
  return { target: target2, confidence: Math.min(confidence, 1), evidence };
141
141
  }
142
142
  var systemPrompt2 = `
143
- ## SDK: Whisperr for Web in Next.js \u2014 package \`@whisperr/web\`
143
+ ## SDK: Whisperr for Next.js \u2014 package \`@whisperr/next\` (+ \`@whisperr/web\`)
144
144
 
145
- Same SDK as plain web, with Next-specific wiring.
145
+ 1) Install: \`<pm> add @whisperr/next @whisperr/web\`. Put the key in
146
+ NEXT_PUBLIC_WHISPERR_KEY in .env.local (and add it to .env.example).
146
147
 
147
- 1) Install \`@whisperr/web\`. Put the key in NEXT_PUBLIC_WHISPERR_KEY in
148
- .env.local (and add it to .env.example).
148
+ 2) Mount the provider in app/layout.tsx. It is already a 'use client' boundary,
149
+ so it drops straight into the server-component layout \u2014 no wrapper file needed:
150
+ import { WhisperrProvider } from '@whisperr/next';
151
+ // inside <body>:
152
+ <WhisperrProvider apiKey={process.env.NEXT_PUBLIC_WHISPERR_KEY!}>{children}</WhisperrProvider>
153
+ Pages Router: wrap the tree in pages/_app.tsx with <WhisperrProvider apiKey=...>.
149
154
 
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
+ 3) identify() after auth resolves on the client (in your session hook/effect);
156
+ reset() on sign-out. Use the useWhisperr() hook inside 'use client' components.
155
157
 
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.
158
+ 4) track() in client components / event handlers / mutation callbacks:
159
+ 'use client';
160
+ import { useWhisperr } from '@whisperr/next';
161
+ const whisperr = useWhisperr();
162
+ whisperr.track('event_type_from_manifest', { ... });
163
+ Do NOT call track from server components, route handlers, or getServerSideProps \u2014
164
+ it is a browser client. Copy event_type verbatim (snake_case) from the manifest.
162
165
 
163
166
  Notes:
164
- - Guard against double-init under React Strict Mode (init is idempotent, but
165
- wire it so it only runs once).
167
+ - Pageviews are auto-captured via the History API (covers Next client navigation).
168
+ - Provider init is idempotent (safe under React Strict Mode).
166
169
  `.trim();
167
170
  var nextjsPlaybook = {
168
171
  target: target2,
169
172
  detect: detect2,
170
- packageRef: "@whisperr/web (npm)",
173
+ packageRef: "@whisperr/next (npm)",
171
174
  systemPrompt: systemPrompt2
172
175
  };
173
176
 
@@ -176,8 +179,7 @@ var target3 = {
176
179
  id: "web-js",
177
180
  displayName: "Web (JavaScript / TypeScript)",
178
181
  language: "typescript",
179
- // Flip to "available" once @whisperr/web ships (Phase 2).
180
- availability: "planned"
182
+ availability: "available"
181
183
  };
182
184
  function dependsOn(pkgJson, names) {
183
185
  try {
@@ -209,41 +211,41 @@ async function detect3(ctx) {
209
211
  return { target: target3, confidence: Math.min(confidence, 0.95), evidence };
210
212
  }
211
213
  var systemPrompt3 = `
212
- ## SDK: Whisperr for Web (TypeScript/JavaScript) \u2014 package \`@whisperr/web\`
214
+ ## SDK: Whisperr for Web \u2014 package \`@whisperr/web\` (+ \`@whisperr/react\` for React)
213
215
 
214
- 1) Dependency. Install with the repo's package manager (detect pnpm/yarn/npm
215
- from the lockfile): \`<pm> add @whisperr/web\`.
216
+ 1) Install (detect pnpm/yarn/npm from the lockfile):
217
+ - Vanilla / Vue / Svelte / Angular / plain TS: \`<pm> add @whisperr/web\`
218
+ - React (not Next.js): \`<pm> add @whisperr/react @whisperr/web\`
216
219
 
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.
220
+ 2) Initialize once at app entry, using the app's PUBLIC env var (VITE_*,
221
+ REACT_APP_*, etc.). Write the var into .env / .env.example.
222
+ - Core (any framework): create a singleton in e.g. src/whisperr.ts:
223
+ import { Whisperr } from '@whisperr/web';
224
+ export const whisperr = Whisperr.init({
225
+ apiKey: import.meta.env.VITE_WHISPERR_KEY,
226
+ baseUrl: '<INGESTION_BASE_URL from manifest>', // omit if it's the default
227
+ });
228
+ - React: wrap the app root instead of a bare singleton:
229
+ import { WhisperrProvider } from '@whisperr/react';
230
+ <WhisperrProvider apiKey={import.meta.env.VITE_WHISPERR_KEY}>\u2026</WhisperrProvider>
226
231
 
227
232
  3) identify(). After auth resolves (login success, and on app load if a session
228
233
  is restored):
229
- whisperr.identify(user.id, {
230
- traits: { /* manifest traits */ },
231
- email: user.email, phone: user.phone,
232
- });
234
+ whisperr.identify(user.id, { traits: { /* manifest traits */ }, email: user.email, phone: user.phone });
235
+ React: const whisperr = useWhisperr(); then whisperr.identify(...).
233
236
  Call whisperr.reset() on logout.
234
237
 
235
238
  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
239
+ whisperr.track('event_type_from_manifest', { /* properties */ });
240
+ In React this belongs in event handlers / effects / mutation callbacks, never
238
241
  in render. Copy event_type verbatim (snake_case) from the manifest.
239
242
 
240
243
  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.
244
+ - The SDK batches + flushes automatically (timer + on page hide). Fire-and-forget
245
+ is fine; do NOT await track().
246
+ - It auto-captures pageviews \u2014 you only add the named business events.
247
+ - Only PUBLIC env vars (VITE_/REACT_APP_/NEXT_PUBLIC_) reach the browser; the
248
+ ingestion key is meant to be public + rate-limited, so that's expected.
247
249
  `.trim();
248
250
  var webPlaybook = {
249
251
  target: target3,
@@ -522,10 +524,11 @@ function sleep(ms) {
522
524
  }
523
525
 
524
526
  // src/core/manifest.ts
525
- async function fetchManifest(config, session, targetId) {
527
+ async function fetchManifest(config, session, targetId, repoFingerprint2) {
526
528
  if (config.offline) return mockManifest(session.appId, config);
527
529
  const url = new URL(`${config.apiBaseUrl}/wizard/manifest`);
528
530
  if (targetId) url.searchParams.set("target", targetId);
531
+ if (repoFingerprint2) url.searchParams.set("repo", repoFingerprint2);
529
532
  const res = await fetch(url, {
530
533
  headers: { authorization: `Bearer ${session.token}` }
531
534
  });
@@ -680,10 +683,17 @@ function renderEventsBrief(m) {
680
683
  lines.push(
681
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.`
682
685
  );
686
+ if (events.some((e) => e.coverage?.length)) {
687
+ lines.push("");
688
+ lines.push(
689
+ "Some events are already instrumented elsewhere (noted inline). Skip any marked [already wired here]; for [wired on <surface>], only add it if this codebase ALSO genuinely triggers it."
690
+ );
691
+ }
683
692
  for (const e of events) {
684
693
  lines.push("");
685
694
  const why = e.interventions?.length ? ` \xB7 drives: ${e.interventions.map((i) => i.label ?? i.code).join(", ")}` : "";
686
- lines.push(`\u25A0 ${e.eventType}${e.label ? ` (${e.label})` : ""}${why}`);
695
+ const cov = coverageNote(e.coverage);
696
+ lines.push(`\u25A0 ${e.eventType}${e.label ? ` (${e.label})` : ""}${why}${cov}`);
687
697
  if (e.description) lines.push(` ${e.description}`);
688
698
  if (e.properties?.length) {
689
699
  lines.push(" properties to capture if available:");
@@ -696,6 +706,16 @@ function renderEventsBrief(m) {
696
706
  }
697
707
  return lines.join("\n");
698
708
  }
709
+ function coverageNote(coverage) {
710
+ if (!coverage?.length) return "";
711
+ const here = coverage.find((c) => c.sameSurface && c.status === "wired");
712
+ if (here) return " [already wired here]";
713
+ const elsewhere = coverage.filter((c) => c.status === "wired");
714
+ if (elsewhere.length) {
715
+ return ` [wired on ${elsewhere.map((c) => c.target).join(", ")}]`;
716
+ }
717
+ return "";
718
+ }
699
719
 
700
720
  // src/core/agent.ts
701
721
  async function runIntegrationAgent(opts) {
@@ -867,6 +887,9 @@ function short(s, max = 60) {
867
887
 
868
888
  // src/core/git.ts
869
889
  import { spawn } from "child_process";
890
+ import { createHash } from "crypto";
891
+ import { readFile as readFile2 } from "fs/promises";
892
+ import { basename, join as join2 } from "path";
870
893
  async function takeCheckpoint(repoPath) {
871
894
  const isRepo = (await run(repoPath, ["rev-parse", "--is-inside-work-tree"])).ok;
872
895
  if (!isRepo) return { isRepo: false };
@@ -897,6 +920,38 @@ function revertHint(checkpoint) {
897
920
  if (!checkpoint.isRepo || !checkpoint.baseRef) return void 0;
898
921
  return `git restore . && git clean -fd (back to ${checkpoint.baseRef.slice(0, 7)})`;
899
922
  }
923
+ async function repoFingerprint(repoPath) {
924
+ const remote = await run(repoPath, ["remote", "get-url", "origin"]);
925
+ const seed = remote.ok && remote.stdout.trim() ? normalizeRemote(remote.stdout.trim()) : `dir:${basename(repoPath)}`;
926
+ return createHash("sha256").update(seed).digest("hex").slice(0, 16);
927
+ }
928
+ function normalizeRemote(url) {
929
+ return url.replace(/^git@([^:]+):/, "$1/").replace(/^https?:\/\//, "").replace(/\.git$/, "").replace(/\/+$/, "").toLowerCase();
930
+ }
931
+ async function scanWiredEvents(repoPath, files, eventTypes) {
932
+ const wired = /* @__PURE__ */ new Map();
933
+ const patterns = eventTypes.map((e) => ({
934
+ eventType: e,
935
+ re: new RegExp(`track\\s*\\(\\s*['"\`]${escapeRegExp(e)}['"\`]`)
936
+ }));
937
+ for (const file of files) {
938
+ let content = "";
939
+ try {
940
+ content = await readFile2(join2(repoPath, file), "utf8");
941
+ } catch {
942
+ continue;
943
+ }
944
+ for (const { eventType, re } of patterns) {
945
+ if (!wired.has(eventType) && re.test(content)) {
946
+ wired.set(eventType, file);
947
+ }
948
+ }
949
+ }
950
+ return wired;
951
+ }
952
+ function escapeRegExp(s) {
953
+ return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
954
+ }
900
955
  function run(cwd, args) {
901
956
  return new Promise((resolve2) => {
902
957
  const child = spawn("git", args, { cwd });
@@ -934,6 +989,22 @@ async function pollFirstEvent(config, session, opts = {}) {
934
989
  return { received: false };
935
990
  }
936
991
 
992
+ // src/core/report.ts
993
+ async function postRunReport(config, session, report) {
994
+ if (config.offline) return;
995
+ try {
996
+ await fetch(`${config.apiBaseUrl}/wizard/report`, {
997
+ method: "POST",
998
+ headers: {
999
+ "Content-Type": "application/json",
1000
+ Authorization: `Bearer ${session.token}`
1001
+ },
1002
+ body: JSON.stringify(report)
1003
+ });
1004
+ } catch {
1005
+ }
1006
+ }
1007
+
937
1008
  // src/ui/banner.ts
938
1009
  function banner() {
939
1010
  const wave = theme.signal("\u223F\u223F\u223F");
@@ -997,9 +1068,10 @@ Flutter is live today; ${theme.bright(
997
1068
  p.cancel(theme.alert(err.message));
998
1069
  return 1;
999
1070
  }
1071
+ const fingerprint = await repoFingerprint(repoPath);
1000
1072
  const manifest = await withSpinner(
1001
1073
  "Loading your onboarding context",
1002
- () => fetchManifest(config, session, chosen.playbook.target.id)
1074
+ () => fetchManifest(config, session, chosen.playbook.target.id, fingerprint)
1003
1075
  );
1004
1076
  p.note(
1005
1077
  summarizeManifest(manifest),
@@ -1055,9 +1127,28 @@ Flutter is live today; ${theme.bright(
1055
1127
  if (outcome.summary.trim()) {
1056
1128
  p.log.message(outcome.summary.trim());
1057
1129
  }
1130
+ const wiredMap = await scanWiredEvents(
1131
+ repoPath,
1132
+ files,
1133
+ manifest.events.map((e) => e.eventType)
1134
+ );
1135
+ const reportEvents = manifest.events.map((e) => ({
1136
+ event_type: e.eventType,
1137
+ status: wiredMap.has(e.eventType) ? "wired" : "skipped",
1138
+ file: wiredMap.get(e.eventType)
1139
+ }));
1140
+ await postRunReport(config, session, {
1141
+ target: chosen.playbook.target.id,
1142
+ repo_fingerprint: fingerprint,
1143
+ identify_wired: outcome.coreOk,
1144
+ cost_usd: outcome.costUsd,
1145
+ duration_ms: outcome.durationMs,
1146
+ summary: outcome.summary.slice(0, 4e3),
1147
+ events: reportEvents
1148
+ });
1058
1149
  p.log.info(
1059
1150
  theme.muted(
1060
- `${files.length} file${files.length === 1 ? "" : "s"} changed \xB7 ~$${outcome.costUsd.toFixed(2)} \xB7 ${Math.round(outcome.durationMs / 1e3)}s`
1151
+ `${wiredMap.size}/${manifest.events.length} events wired \xB7 ${files.length} file${files.length === 1 ? "" : "s"} changed \xB7 ~$${outcome.costUsd.toFixed(2)} \xB7 ${Math.round(outcome.durationMs / 1e3)}s`
1061
1152
  )
1062
1153
  );
1063
1154
  if (!config.offline) {
@@ -1226,7 +1317,7 @@ async function main() {
1226
1317
  return;
1227
1318
  }
1228
1319
  if ("version" in parsed) {
1229
- console.log("0.1.4");
1320
+ console.log("0.1.6");
1230
1321
  return;
1231
1322
  }
1232
1323
  try {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@whisperr/wizard",
3
- "version": "0.1.4",
3
+ "version": "0.1.6",
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,