@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.
- package/dist/index.js +141 -50
- 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: "
|
|
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
|
|
143
|
+
## SDK: Whisperr for Next.js \u2014 package \`@whisperr/next\` (+ \`@whisperr/web\`)
|
|
144
144
|
|
|
145
|
-
|
|
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
|
-
|
|
148
|
-
|
|
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
|
-
|
|
151
|
-
-
|
|
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
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
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
|
-
-
|
|
165
|
-
|
|
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/
|
|
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
|
-
|
|
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
|
|
214
|
+
## SDK: Whisperr for Web \u2014 package \`@whisperr/web\` (+ \`@whisperr/react\` for React)
|
|
213
215
|
|
|
214
|
-
1)
|
|
215
|
-
|
|
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
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
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
|
-
|
|
230
|
-
|
|
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
|
-
|
|
237
|
-
In React
|
|
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
|
|
242
|
-
|
|
243
|
-
-
|
|
244
|
-
|
|
245
|
-
|
|
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
|
-
|
|
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.
|
|
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.
|
|
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,
|