@whisperr/wizard 0.1.3 → 0.1.5
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 +150 -44
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -522,10 +522,11 @@ function sleep(ms) {
|
|
|
522
522
|
}
|
|
523
523
|
|
|
524
524
|
// src/core/manifest.ts
|
|
525
|
-
async function fetchManifest(config, session, targetId) {
|
|
525
|
+
async function fetchManifest(config, session, targetId, repoFingerprint2) {
|
|
526
526
|
if (config.offline) return mockManifest(session.appId, config);
|
|
527
527
|
const url = new URL(`${config.apiBaseUrl}/wizard/manifest`);
|
|
528
528
|
if (targetId) url.searchParams.set("target", targetId);
|
|
529
|
+
if (repoFingerprint2) url.searchParams.set("repo", repoFingerprint2);
|
|
529
530
|
const res = await fetch(url, {
|
|
530
531
|
headers: { authorization: `Bearer ${session.token}` }
|
|
531
532
|
});
|
|
@@ -597,41 +598,53 @@ import { query } from "@anthropic-ai/claude-agent-sdk";
|
|
|
597
598
|
// src/core/playbooks/shared-prompt.ts
|
|
598
599
|
var BASE_WIZARD_PROMPT = `
|
|
599
600
|
You are the Whisperr Wizard \u2014 an expert integration agent running INSIDE a
|
|
600
|
-
customer's code repository.
|
|
601
|
-
sends the right events to Whisperr, then stop. Whisperr is a churn-prevention
|
|
602
|
-
engine that needs identify() (who the user is) and track() (what they do).
|
|
601
|
+
customer's code repository.
|
|
603
602
|
|
|
604
|
-
|
|
605
|
-
|
|
603
|
+
MISSION:
|
|
604
|
+
Whisperr predicts churn and fires retention interventions from product events.
|
|
605
|
+
Two things make that work: (1) identify() tying events to a real user, and
|
|
606
|
+
(2) track() events fired at the right moment WITH the properties that give them
|
|
607
|
+
meaning. A well-placed event carrying its real properties is gold; a bare or
|
|
608
|
+
mis-placed event is noise that corrupts the customer's churn data. Your mission:
|
|
609
|
+
wire up everything in the plan that genuinely exists in this app, and for each
|
|
610
|
+
event you place, capture the properties that are actually available right there
|
|
611
|
+
in the code. You will NOT find everything \u2014 some events live server-side or
|
|
612
|
+
aren't in this codebase at all. That is expected and fine. Get what is here, get
|
|
613
|
+
it right, report the rest. Know exactly what you're doing; never hunt blindly.
|
|
614
|
+
|
|
615
|
+
You are billed per step and the user is watching. Work FAST and DECISIVELY.
|
|
606
616
|
|
|
607
617
|
EFFICIENCY \u2014 non-negotiable:
|
|
608
618
|
- Use Grep and Glob to find code. Do NOT read whole files; read the smallest
|
|
609
619
|
slice you need.
|
|
610
620
|
- NEVER re-read a file you just edited. Trust your edit.
|
|
611
|
-
- Do not run the app, build, or
|
|
612
|
-
|
|
621
|
+
- Do not run the app, build, or the analyzer/test suites. The human reviews the
|
|
622
|
+
diff.
|
|
613
623
|
- Make several related edits in a row before pausing to think.
|
|
614
624
|
|
|
615
625
|
DECISIVENESS:
|
|
616
626
|
- When you find the right spot, edit it and move on immediately.
|
|
617
|
-
- If you
|
|
618
|
-
|
|
619
|
-
|
|
627
|
+
- If you can't find a clear place for something within ~2 searches, STOP looking.
|
|
628
|
+
Leave a one-line \`// TODO(whisperr): ...\` only if an obvious spot exists,
|
|
629
|
+
otherwise just note it as a follow-up. Do not keep hunting.
|
|
620
630
|
|
|
621
|
-
|
|
622
|
-
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
631
|
+
PROPERTY CAPTURE (this is the craft):
|
|
632
|
+
- For each event you place, look at what's in scope AT THAT CALL SITE \u2014 function
|
|
633
|
+
arguments, the object/model/state you're inside, nearby local variables \u2014 and
|
|
634
|
+
populate every listed property you can source from those values.
|
|
635
|
+
- Omit a property only if it genuinely isn't available there. Do NOT fetch,
|
|
636
|
+
compute, or invent data to fill a property, and do NOT wander off to find it.
|
|
637
|
+
- If a property marked (required) can't be sourced at your chosen site, that's a
|
|
638
|
+
strong hint you've picked the wrong site \u2014 reconsider; if it's still the best
|
|
639
|
+
spot, add the track() call and note the missing property.
|
|
626
640
|
|
|
627
641
|
CORRECTNESS:
|
|
628
642
|
- Match the app's existing conventions (imports, state management, file layout).
|
|
629
643
|
- Use the EXACT snake_case event names given. Never invent or rename events.
|
|
630
|
-
-
|
|
631
|
-
Only place an event where you are confident the user action truly happens.
|
|
644
|
+
- Only place an event where you're confident the user action truly happens.
|
|
632
645
|
|
|
633
|
-
End every task with a short summary: what you changed,
|
|
634
|
-
|
|
646
|
+
End every task with a short summary: what you changed, which events you wired
|
|
647
|
+
(and the properties you attached), and the follow-ups you intentionally left.
|
|
635
648
|
`.trim();
|
|
636
649
|
function renderIdentifyBrief(m) {
|
|
637
650
|
const lines = [];
|
|
@@ -640,14 +653,14 @@ function renderIdentifyBrief(m) {
|
|
|
640
653
|
lines.push(`Ingestion API key: ${m.ingestionApiKey}`);
|
|
641
654
|
if (m.businessContext) {
|
|
642
655
|
lines.push("");
|
|
643
|
-
lines.push(`
|
|
656
|
+
lines.push(`Business context: ${m.businessContext}`);
|
|
644
657
|
}
|
|
645
658
|
lines.push("");
|
|
646
659
|
lines.push("identify():");
|
|
647
660
|
if (m.identify.traits?.length) {
|
|
648
|
-
lines.push(" Send these traits when available:");
|
|
661
|
+
lines.push(" Send these traits when readily available:");
|
|
649
662
|
for (const t of m.identify.traits) {
|
|
650
|
-
lines.push(` - ${t.name}${t.description ? `
|
|
663
|
+
lines.push(` - ${t.name}${t.description ? ` \u2014 ${t.description}` : ""}`);
|
|
651
664
|
}
|
|
652
665
|
}
|
|
653
666
|
if (m.identify.channels?.length) {
|
|
@@ -661,22 +674,46 @@ function renderEventsBrief(m) {
|
|
|
661
674
|
(a, b) => (b.importance ?? 0) - (a.importance ?? 0)
|
|
662
675
|
);
|
|
663
676
|
const lines = [];
|
|
677
|
+
if (m.businessContext) {
|
|
678
|
+
lines.push(`Business context: ${m.businessContext}`);
|
|
679
|
+
lines.push("");
|
|
680
|
+
}
|
|
664
681
|
lines.push(
|
|
665
|
-
`${events.length} candidate events (highest-impact first).
|
|
682
|
+
`${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.`
|
|
666
683
|
);
|
|
667
|
-
|
|
684
|
+
if (events.some((e) => e.coverage?.length)) {
|
|
685
|
+
lines.push("");
|
|
686
|
+
lines.push(
|
|
687
|
+
"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."
|
|
688
|
+
);
|
|
689
|
+
}
|
|
668
690
|
for (const e of events) {
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
691
|
+
lines.push("");
|
|
692
|
+
const why = e.interventions?.length ? ` \xB7 drives: ${e.interventions.map((i) => i.label ?? i.code).join(", ")}` : "";
|
|
693
|
+
const cov = coverageNote(e.coverage);
|
|
694
|
+
lines.push(`\u25A0 ${e.eventType}${e.label ? ` (${e.label})` : ""}${why}${cov}`);
|
|
695
|
+
if (e.description) lines.push(` ${e.description}`);
|
|
672
696
|
if (e.properties?.length) {
|
|
673
|
-
lines.push(
|
|
674
|
-
|
|
675
|
-
|
|
697
|
+
lines.push(" properties to capture if available:");
|
|
698
|
+
for (const p2 of e.properties) {
|
|
699
|
+
const req = p2.required ? " (required)" : "";
|
|
700
|
+
const desc = p2.description ? ` \u2014 ${p2.description}` : "";
|
|
701
|
+
lines.push(` \xB7 ${p2.name}${req}${desc}`);
|
|
702
|
+
}
|
|
676
703
|
}
|
|
677
704
|
}
|
|
678
705
|
return lines.join("\n");
|
|
679
706
|
}
|
|
707
|
+
function coverageNote(coverage) {
|
|
708
|
+
if (!coverage?.length) return "";
|
|
709
|
+
const here = coverage.find((c) => c.sameSurface && c.status === "wired");
|
|
710
|
+
if (here) return " [already wired here]";
|
|
711
|
+
const elsewhere = coverage.filter((c) => c.status === "wired");
|
|
712
|
+
if (elsewhere.length) {
|
|
713
|
+
return ` [wired on ${elsewhere.map((c) => c.target).join(", ")}]`;
|
|
714
|
+
}
|
|
715
|
+
return "";
|
|
716
|
+
}
|
|
680
717
|
|
|
681
718
|
// src/core/agent.ts
|
|
682
719
|
async function runIntegrationAgent(opts) {
|
|
@@ -724,17 +761,15 @@ ${core.summary}`);
|
|
|
724
761
|
if (manifest.events.length > 0) {
|
|
725
762
|
progress?.onPhase?.("Instrumenting your events");
|
|
726
763
|
const eventsPrompt = [
|
|
727
|
-
`The Whisperr ${playbook.target.displayName} SDK is already installed
|
|
728
|
-
"initialized, and identify() is wired. Now
|
|
729
|
-
"
|
|
764
|
+
`The Whisperr ${playbook.target.displayName} SDK is already installed and`,
|
|
765
|
+
"initialized, and identify() is wired. Now instrument the product events",
|
|
766
|
+
"below with track().",
|
|
730
767
|
"",
|
|
731
|
-
"
|
|
732
|
-
"
|
|
733
|
-
"-
|
|
734
|
-
"
|
|
735
|
-
"
|
|
736
|
-
"- You have a limited number of steps; spend them placing events, not",
|
|
737
|
-
" exploring. Stop once the events that clearly exist in this app are wired.",
|
|
768
|
+
"Work in importance order. Place each event at its real call site and",
|
|
769
|
+
"attach the properties you can source from what's in scope there (see the",
|
|
770
|
+
"property-capture rule in your instructions). Skip fast when an event has",
|
|
771
|
+
"no clear client-side trigger \u2014 spend steps placing events, not exploring.",
|
|
772
|
+
"Stop once the events that clearly exist in this app are wired.",
|
|
738
773
|
"",
|
|
739
774
|
"----- EVENTS -----",
|
|
740
775
|
renderEventsBrief(manifest),
|
|
@@ -850,6 +885,9 @@ function short(s, max = 60) {
|
|
|
850
885
|
|
|
851
886
|
// src/core/git.ts
|
|
852
887
|
import { spawn } from "child_process";
|
|
888
|
+
import { createHash } from "crypto";
|
|
889
|
+
import { readFile as readFile2 } from "fs/promises";
|
|
890
|
+
import { basename, join as join2 } from "path";
|
|
853
891
|
async function takeCheckpoint(repoPath) {
|
|
854
892
|
const isRepo = (await run(repoPath, ["rev-parse", "--is-inside-work-tree"])).ok;
|
|
855
893
|
if (!isRepo) return { isRepo: false };
|
|
@@ -880,6 +918,38 @@ function revertHint(checkpoint) {
|
|
|
880
918
|
if (!checkpoint.isRepo || !checkpoint.baseRef) return void 0;
|
|
881
919
|
return `git restore . && git clean -fd (back to ${checkpoint.baseRef.slice(0, 7)})`;
|
|
882
920
|
}
|
|
921
|
+
async function repoFingerprint(repoPath) {
|
|
922
|
+
const remote = await run(repoPath, ["remote", "get-url", "origin"]);
|
|
923
|
+
const seed = remote.ok && remote.stdout.trim() ? normalizeRemote(remote.stdout.trim()) : `dir:${basename(repoPath)}`;
|
|
924
|
+
return createHash("sha256").update(seed).digest("hex").slice(0, 16);
|
|
925
|
+
}
|
|
926
|
+
function normalizeRemote(url) {
|
|
927
|
+
return url.replace(/^git@([^:]+):/, "$1/").replace(/^https?:\/\//, "").replace(/\.git$/, "").replace(/\/+$/, "").toLowerCase();
|
|
928
|
+
}
|
|
929
|
+
async function scanWiredEvents(repoPath, files, eventTypes) {
|
|
930
|
+
const wired = /* @__PURE__ */ new Map();
|
|
931
|
+
const patterns = eventTypes.map((e) => ({
|
|
932
|
+
eventType: e,
|
|
933
|
+
re: new RegExp(`track\\s*\\(\\s*['"\`]${escapeRegExp(e)}['"\`]`)
|
|
934
|
+
}));
|
|
935
|
+
for (const file of files) {
|
|
936
|
+
let content = "";
|
|
937
|
+
try {
|
|
938
|
+
content = await readFile2(join2(repoPath, file), "utf8");
|
|
939
|
+
} catch {
|
|
940
|
+
continue;
|
|
941
|
+
}
|
|
942
|
+
for (const { eventType, re } of patterns) {
|
|
943
|
+
if (!wired.has(eventType) && re.test(content)) {
|
|
944
|
+
wired.set(eventType, file);
|
|
945
|
+
}
|
|
946
|
+
}
|
|
947
|
+
}
|
|
948
|
+
return wired;
|
|
949
|
+
}
|
|
950
|
+
function escapeRegExp(s) {
|
|
951
|
+
return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
952
|
+
}
|
|
883
953
|
function run(cwd, args) {
|
|
884
954
|
return new Promise((resolve2) => {
|
|
885
955
|
const child = spawn("git", args, { cwd });
|
|
@@ -917,6 +987,22 @@ async function pollFirstEvent(config, session, opts = {}) {
|
|
|
917
987
|
return { received: false };
|
|
918
988
|
}
|
|
919
989
|
|
|
990
|
+
// src/core/report.ts
|
|
991
|
+
async function postRunReport(config, session, report) {
|
|
992
|
+
if (config.offline) return;
|
|
993
|
+
try {
|
|
994
|
+
await fetch(`${config.apiBaseUrl}/wizard/report`, {
|
|
995
|
+
method: "POST",
|
|
996
|
+
headers: {
|
|
997
|
+
"Content-Type": "application/json",
|
|
998
|
+
Authorization: `Bearer ${session.token}`
|
|
999
|
+
},
|
|
1000
|
+
body: JSON.stringify(report)
|
|
1001
|
+
});
|
|
1002
|
+
} catch {
|
|
1003
|
+
}
|
|
1004
|
+
}
|
|
1005
|
+
|
|
920
1006
|
// src/ui/banner.ts
|
|
921
1007
|
function banner() {
|
|
922
1008
|
const wave = theme.signal("\u223F\u223F\u223F");
|
|
@@ -980,9 +1066,10 @@ Flutter is live today; ${theme.bright(
|
|
|
980
1066
|
p.cancel(theme.alert(err.message));
|
|
981
1067
|
return 1;
|
|
982
1068
|
}
|
|
1069
|
+
const fingerprint = await repoFingerprint(repoPath);
|
|
983
1070
|
const manifest = await withSpinner(
|
|
984
1071
|
"Loading your onboarding context",
|
|
985
|
-
() => fetchManifest(config, session, chosen.playbook.target.id)
|
|
1072
|
+
() => fetchManifest(config, session, chosen.playbook.target.id, fingerprint)
|
|
986
1073
|
);
|
|
987
1074
|
p.note(
|
|
988
1075
|
summarizeManifest(manifest),
|
|
@@ -1038,9 +1125,28 @@ Flutter is live today; ${theme.bright(
|
|
|
1038
1125
|
if (outcome.summary.trim()) {
|
|
1039
1126
|
p.log.message(outcome.summary.trim());
|
|
1040
1127
|
}
|
|
1128
|
+
const wiredMap = await scanWiredEvents(
|
|
1129
|
+
repoPath,
|
|
1130
|
+
files,
|
|
1131
|
+
manifest.events.map((e) => e.eventType)
|
|
1132
|
+
);
|
|
1133
|
+
const reportEvents = manifest.events.map((e) => ({
|
|
1134
|
+
event_type: e.eventType,
|
|
1135
|
+
status: wiredMap.has(e.eventType) ? "wired" : "skipped",
|
|
1136
|
+
file: wiredMap.get(e.eventType)
|
|
1137
|
+
}));
|
|
1138
|
+
await postRunReport(config, session, {
|
|
1139
|
+
target: chosen.playbook.target.id,
|
|
1140
|
+
repo_fingerprint: fingerprint,
|
|
1141
|
+
identify_wired: outcome.coreOk,
|
|
1142
|
+
cost_usd: outcome.costUsd,
|
|
1143
|
+
duration_ms: outcome.durationMs,
|
|
1144
|
+
summary: outcome.summary.slice(0, 4e3),
|
|
1145
|
+
events: reportEvents
|
|
1146
|
+
});
|
|
1041
1147
|
p.log.info(
|
|
1042
1148
|
theme.muted(
|
|
1043
|
-
`${files.length} file${files.length === 1 ? "" : "s"} changed \xB7 ~$${outcome.costUsd.toFixed(2)} \xB7 ${Math.round(outcome.durationMs / 1e3)}s`
|
|
1149
|
+
`${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`
|
|
1044
1150
|
)
|
|
1045
1151
|
);
|
|
1046
1152
|
if (!config.offline) {
|
|
@@ -1209,7 +1315,7 @@ async function main() {
|
|
|
1209
1315
|
return;
|
|
1210
1316
|
}
|
|
1211
1317
|
if ("version" in parsed) {
|
|
1212
|
-
console.log("0.1.
|
|
1318
|
+
console.log("0.1.5");
|
|
1213
1319
|
return;
|
|
1214
1320
|
}
|
|
1215
1321
|
try {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@whisperr/wizard",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.5",
|
|
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,
|