@whisperr/wizard 0.1.1 → 0.1.3
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 +166 -91
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -6,7 +6,7 @@ import * as p from "@clack/prompts";
|
|
|
6
6
|
|
|
7
7
|
// src/core/config.ts
|
|
8
8
|
var DEFAULT_API_BASE = "https://api.whisperr.net";
|
|
9
|
-
var DEFAULT_MODEL = "claude-
|
|
9
|
+
var DEFAULT_MODEL = "claude-sonnet-4-6";
|
|
10
10
|
function resolveConfig(flags = {}) {
|
|
11
11
|
const apiBaseUrl = (flags.apiBaseUrl ?? process.env.WHISPERR_WIZARD_API_BASE ?? DEFAULT_API_BASE).replace(/\/+$/, "");
|
|
12
12
|
const llmBaseUrl = (process.env.WHISPERR_WIZARD_LLM_BASE ?? `${apiBaseUrl}/wizard/llm`).replace(/\/+$/, "");
|
|
@@ -18,7 +18,7 @@ function resolveConfig(flags = {}) {
|
|
|
18
18
|
apiBaseUrl,
|
|
19
19
|
llmBaseUrl,
|
|
20
20
|
model: flags.model ?? process.env.WHISPERR_WIZARD_MODEL ?? DEFAULT_MODEL,
|
|
21
|
-
maxTurns: Number(process.env.WHISPERR_WIZARD_MAX_TURNS ??
|
|
21
|
+
maxTurns: Number(process.env.WHISPERR_WIZARD_MAX_TURNS ?? 55),
|
|
22
22
|
directAnthropicKey,
|
|
23
23
|
offline
|
|
24
24
|
};
|
|
@@ -596,84 +596,83 @@ import { query } from "@anthropic-ai/claude-agent-sdk";
|
|
|
596
596
|
|
|
597
597
|
// src/core/playbooks/shared-prompt.ts
|
|
598
598
|
var BASE_WIZARD_PROMPT = `
|
|
599
|
-
You are the Whisperr Wizard \u2014 an expert integration agent
|
|
600
|
-
|
|
601
|
-
|
|
599
|
+
You are the Whisperr Wizard \u2014 an expert integration agent running INSIDE a
|
|
600
|
+
customer's code repository. Your job: wire up the Whisperr SDK so the product
|
|
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).
|
|
602
603
|
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
plus traits and contact channels, called right after the user is known
|
|
606
|
-
(login / session restore / signup).
|
|
607
|
-
2. track() \u2014 emit the specific business events listed in the integration
|
|
608
|
-
manifest, at the exact places in the code where those user actions happen.
|
|
609
|
-
These named events drive the customer's churn interventions, so placing
|
|
610
|
-
them at the correct call sites matters more than anything else you do.
|
|
604
|
+
You are billed per step and the user is watching the clock. Work FAST and
|
|
605
|
+
DECISIVELY. Most failures come from over-exploring \u2014 do not do that.
|
|
611
606
|
|
|
612
|
-
|
|
613
|
-
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
commented TODO with the exact Whisperr call to make, and report it at the end
|
|
620
|
-
rather than firing the event from a wrong place.
|
|
621
|
-
- Never invent event types. Use the exact snake_case event_type strings from the
|
|
622
|
-
manifest. Custom data goes under properties.
|
|
623
|
-
- Keep the API key out of source where the SDK/platform supports env/secrets;
|
|
624
|
-
otherwise place it where the manifest guidance says and flag it.
|
|
625
|
-
- Match the repository's existing conventions (imports, formatting, state
|
|
626
|
-
management, file layout).
|
|
607
|
+
EFFICIENCY \u2014 non-negotiable:
|
|
608
|
+
- Use Grep and Glob to find code. Do NOT read whole files; read the smallest
|
|
609
|
+
slice you need.
|
|
610
|
+
- NEVER re-read a file you just edited. Trust your edit.
|
|
611
|
+
- Do not run the app, build, or heavy test suites. At most one quick static
|
|
612
|
+
check, and only if the SDK guide tells you to.
|
|
613
|
+
- Make several related edits in a row before pausing to think.
|
|
627
614
|
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
615
|
+
DECISIVENESS:
|
|
616
|
+
- When you find the right spot, edit it and move on immediately.
|
|
617
|
+
- If you cannot find a clear place for something within ~2 searches, STOP
|
|
618
|
+
looking for it. Either drop a single-line \`// TODO(whisperr): track(...)\`
|
|
619
|
+
if an obvious spot exists, or just note it as a follow-up. Do not keep hunting.
|
|
620
|
+
|
|
621
|
+
SCOPE REALISM:
|
|
622
|
+
- Not every event exists in THIS app. Many are server-side (billing webhooks,
|
|
623
|
+
delivery callbacks) or simply absent. That is expected. Instrument what
|
|
624
|
+
clearly exists on the client; list the rest as follow-ups. Do not treat a
|
|
625
|
+
missing call site as a problem to solve.
|
|
626
|
+
|
|
627
|
+
CORRECTNESS:
|
|
628
|
+
- Match the app's existing conventions (imports, state management, file layout).
|
|
629
|
+
- Use the EXACT snake_case event names given. Never invent or rename events.
|
|
630
|
+
- A wrongly-placed business event corrupts churn data \u2014 worse than a missing one.
|
|
631
|
+
Only place an event where you are confident the user action truly happens.
|
|
632
|
+
|
|
633
|
+
End every task with a short summary: what you changed, and any follow-ups the
|
|
634
|
+
human should handle (events you intentionally skipped + why).
|
|
633
635
|
`.trim();
|
|
634
|
-
function
|
|
636
|
+
function renderIdentifyBrief(m) {
|
|
635
637
|
const lines = [];
|
|
636
|
-
lines.push(
|
|
637
|
-
lines.push("");
|
|
638
|
+
lines.push(`App: ${m.appName ?? m.appId}`);
|
|
638
639
|
lines.push(`Ingestion base URL: ${m.ingestionBaseUrl}`);
|
|
639
640
|
lines.push(`Ingestion API key: ${m.ingestionApiKey}`);
|
|
640
641
|
if (m.businessContext) {
|
|
641
642
|
lines.push("");
|
|
642
|
-
lines.push(
|
|
643
|
-
lines.push(m.businessContext);
|
|
643
|
+
lines.push(`Context: ${m.businessContext}`);
|
|
644
644
|
}
|
|
645
645
|
lines.push("");
|
|
646
|
-
lines.push("
|
|
646
|
+
lines.push("identify():");
|
|
647
647
|
if (m.identify.traits?.length) {
|
|
648
|
-
lines.push("
|
|
648
|
+
lines.push(" Send these traits when available:");
|
|
649
649
|
for (const t of m.identify.traits) {
|
|
650
|
-
lines.push(`
|
|
650
|
+
lines.push(` - ${t.name}${t.description ? ` (${t.description})` : ""}`);
|
|
651
651
|
}
|
|
652
652
|
}
|
|
653
653
|
if (m.identify.channels?.length) {
|
|
654
|
-
lines.push(`Channels in use: ${m.identify.channels.join(", ")}`);
|
|
654
|
+
lines.push(` Channels in use: ${m.identify.channels.join(", ")}`);
|
|
655
655
|
}
|
|
656
|
-
if (m.identify.notes) lines.push(m.identify.notes);
|
|
657
|
-
lines.
|
|
658
|
-
|
|
656
|
+
if (m.identify.notes) lines.push(` ${m.identify.notes}`);
|
|
657
|
+
return lines.join("\n");
|
|
658
|
+
}
|
|
659
|
+
function renderEventsBrief(m) {
|
|
659
660
|
const events = [...m.events].sort(
|
|
660
661
|
(a, b) => (b.importance ?? 0) - (a.importance ?? 0)
|
|
661
662
|
);
|
|
663
|
+
const lines = [];
|
|
664
|
+
lines.push(
|
|
665
|
+
`${events.length} candidate events (highest-impact first). Emit each with its EXACT name via track('<event_type>', { ...properties }).`
|
|
666
|
+
);
|
|
667
|
+
lines.push("");
|
|
662
668
|
for (const e of events) {
|
|
663
|
-
|
|
664
|
-
lines.push(
|
|
665
|
-
if (e.description) lines.push(e.description);
|
|
669
|
+
const drivers = e.interventions?.length ? ` \u2014 drives: ${e.interventions.map((i) => i.label ?? i.code).join(", ")}` : "";
|
|
670
|
+
lines.push(`- ${e.eventType}${e.label ? ` (${e.label})` : ""}${drivers}`);
|
|
671
|
+
if (e.description) lines.push(` ${e.description}`);
|
|
666
672
|
if (e.properties?.length) {
|
|
667
|
-
lines.push(
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
` - ${p2.name}${p2.required ? " (required)" : ""}${p2.description ? ` \u2014 ${p2.description}` : ""}`
|
|
671
|
-
);
|
|
672
|
-
}
|
|
673
|
-
}
|
|
674
|
-
if (e.interventions?.length) {
|
|
675
|
-
const drv = e.interventions.map((i) => i.label ?? i.code).join(", ");
|
|
676
|
-
lines.push(`Drives: ${drv}`);
|
|
673
|
+
lines.push(
|
|
674
|
+
` properties: ${e.properties.map((p2) => p2.name).join(", ")}`
|
|
675
|
+
);
|
|
677
676
|
}
|
|
678
677
|
}
|
|
679
678
|
return lines.join("\n");
|
|
@@ -684,40 +683,99 @@ async function runIntegrationAgent(opts) {
|
|
|
684
683
|
const { repoPath, config, session, playbook, manifest, progress } = opts;
|
|
685
684
|
applyModelAuthEnv(config, session);
|
|
686
685
|
const systemPrompt6 = [BASE_WIZARD_PROMPT, playbook.systemPrompt].join("\n\n");
|
|
687
|
-
const
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
686
|
+
const started = Date.now();
|
|
687
|
+
let costUsd = 0;
|
|
688
|
+
const summaries = [];
|
|
689
|
+
progress?.onPhase?.("Installing the SDK & wiring identify()");
|
|
690
|
+
const corePrompt = [
|
|
691
|
+
`Integrate the Whisperr ${playbook.target.displayName} SDK \u2014 CORE SETUP ONLY.`,
|
|
692
692
|
`Project root: ${repoPath}`,
|
|
693
693
|
"",
|
|
694
|
-
"
|
|
695
|
-
|
|
696
|
-
"
|
|
694
|
+
"Do exactly these, then stop:",
|
|
695
|
+
`1. Add the Whisperr SDK dependency and install it (${playbook.packageRef}).`,
|
|
696
|
+
"2. Initialize it once at app startup with the key + base URL below.",
|
|
697
|
+
"3. Wire identify() at the PRIMARY place the user becomes authenticated",
|
|
698
|
+
" (after a successful login) and on session restore at app startup; call",
|
|
699
|
+
" reset() on logout. Do NOT enumerate every auth method \u2014 one primary",
|
|
700
|
+
" login path plus session restore is enough. Keep channels minimal: pass",
|
|
701
|
+
" email if it's readily at hand; phone/push are optional, skip them if not",
|
|
702
|
+
" obvious. Do not go hunting through every auth file.",
|
|
703
|
+
"",
|
|
704
|
+
"This is a real app \u2014 be quick and decisive, aim to finish in ~15 steps.",
|
|
705
|
+
"Do NOT instrument product events yet \u2014 that is a separate step. Do not run",
|
|
706
|
+
"the analyzer or build; the human will review the diff.",
|
|
697
707
|
"",
|
|
698
|
-
"-----
|
|
699
|
-
|
|
700
|
-
"----- END
|
|
701
|
-
verifyHint
|
|
708
|
+
"----- IDENTIFY -----",
|
|
709
|
+
renderIdentifyBrief(manifest),
|
|
710
|
+
"----- END -----"
|
|
702
711
|
].join("\n");
|
|
703
|
-
const
|
|
712
|
+
const core = await runPass({
|
|
713
|
+
prompt: corePrompt,
|
|
714
|
+
systemPrompt: systemPrompt6,
|
|
715
|
+
repoPath,
|
|
716
|
+
model: config.model,
|
|
717
|
+
maxTurns: 35,
|
|
718
|
+
progress
|
|
719
|
+
});
|
|
720
|
+
costUsd += core.costUsd;
|
|
721
|
+
if (core.summary) summaries.push(`Core setup:
|
|
722
|
+
${core.summary}`);
|
|
723
|
+
let eventsComplete = true;
|
|
724
|
+
if (manifest.events.length > 0) {
|
|
725
|
+
progress?.onPhase?.("Instrumenting your events");
|
|
726
|
+
const eventsPrompt = [
|
|
727
|
+
`The Whisperr ${playbook.target.displayName} SDK is already installed,`,
|
|
728
|
+
"initialized, and identify() is wired. Now add track() calls for product",
|
|
729
|
+
"events.",
|
|
730
|
+
"",
|
|
731
|
+
"How to work:",
|
|
732
|
+
"- Go in the order listed (highest-impact first).",
|
|
733
|
+
"- For each event, Grep for where that user action happens. If there is a",
|
|
734
|
+
" clear client-side call site, add the track() call with the exact name.",
|
|
735
|
+
" If not, SKIP it (most server-side events won't exist here) \u2014 do not hunt.",
|
|
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.",
|
|
738
|
+
"",
|
|
739
|
+
"----- EVENTS -----",
|
|
740
|
+
renderEventsBrief(manifest),
|
|
741
|
+
"----- END -----"
|
|
742
|
+
].join("\n");
|
|
743
|
+
const events = await runPass({
|
|
744
|
+
prompt: eventsPrompt,
|
|
745
|
+
systemPrompt: systemPrompt6,
|
|
746
|
+
repoPath,
|
|
747
|
+
model: config.model,
|
|
748
|
+
maxTurns: config.maxTurns,
|
|
749
|
+
progress
|
|
750
|
+
});
|
|
751
|
+
costUsd += events.costUsd;
|
|
752
|
+
eventsComplete = !events.maxedOut;
|
|
753
|
+
if (events.summary) summaries.push(`Events:
|
|
754
|
+
${events.summary}`);
|
|
755
|
+
}
|
|
756
|
+
return {
|
|
757
|
+
summary: summaries.join("\n\n"),
|
|
758
|
+
costUsd,
|
|
759
|
+
durationMs: Date.now() - started,
|
|
760
|
+
coreOk: core.ok,
|
|
761
|
+
eventsComplete
|
|
762
|
+
};
|
|
763
|
+
}
|
|
764
|
+
async function runPass(opts) {
|
|
765
|
+
const { prompt, systemPrompt: systemPrompt6, repoPath, model, maxTurns, progress } = opts;
|
|
704
766
|
let summary = "";
|
|
705
|
-
let costUsd;
|
|
767
|
+
let costUsd = 0;
|
|
706
768
|
let ok = false;
|
|
769
|
+
let maxedOut = false;
|
|
707
770
|
const response = query({
|
|
708
771
|
prompt,
|
|
709
772
|
options: {
|
|
710
|
-
model
|
|
773
|
+
model,
|
|
711
774
|
cwd: repoPath,
|
|
712
775
|
systemPrompt: systemPrompt6,
|
|
713
|
-
// Full file-system agent toolset.
|
|
714
776
|
allowedTools: ["Read", "Edit", "Write", "Bash", "Glob", "Grep"],
|
|
715
|
-
// Fully autonomous edits (the user opted into auto-apply; the wizard takes
|
|
716
|
-
// a git checkpoint before this runs so revert is one command).
|
|
717
777
|
permissionMode: "bypassPermissions",
|
|
718
|
-
maxTurns
|
|
719
|
-
// Don't pick up the user's own CLAUDE.md / settings — the wizard's prompt
|
|
720
|
-
// is the single source of truth.
|
|
778
|
+
maxTurns,
|
|
721
779
|
settingSources: []
|
|
722
780
|
}
|
|
723
781
|
});
|
|
@@ -732,12 +790,12 @@ When finished, run \`${playbook.verifyCommand}\` and fix anything it flags.` : "
|
|
|
732
790
|
}
|
|
733
791
|
} else if (message.type === "result") {
|
|
734
792
|
ok = message.subtype === "success";
|
|
735
|
-
|
|
793
|
+
maxedOut = (message.subtype ?? "").includes("max_turns");
|
|
794
|
+
costUsd = message.total_cost_usd ?? 0;
|
|
736
795
|
summary = message.result ?? extractLastText(message) ?? summary;
|
|
737
796
|
}
|
|
738
797
|
}
|
|
739
|
-
|
|
740
|
-
return { summary, costUsd, ok, durationMs: Date.now() - started };
|
|
798
|
+
return { summary, costUsd, ok, maxedOut };
|
|
741
799
|
}
|
|
742
800
|
function applyModelAuthEnv(config, session) {
|
|
743
801
|
if (config.directAnthropicKey) {
|
|
@@ -937,8 +995,16 @@ Flutter is live today; ${theme.bright(
|
|
|
937
995
|
);
|
|
938
996
|
}
|
|
939
997
|
const spin = p.spinner();
|
|
940
|
-
|
|
998
|
+
let phaseLabel = "Integrating";
|
|
941
999
|
let lastLine = "";
|
|
1000
|
+
const startedAt = Date.now();
|
|
1001
|
+
spin.start(theme.bright(phaseLabel));
|
|
1002
|
+
const tick = setInterval(() => {
|
|
1003
|
+
const secs = Math.round((Date.now() - startedAt) / 1e3);
|
|
1004
|
+
spin.message(
|
|
1005
|
+
theme.bright(phaseLabel) + theme.muted(` \xB7 ${secs}s`) + (lastLine ? theme.muted(` \u2014 ${lastLine}`) : "")
|
|
1006
|
+
);
|
|
1007
|
+
}, 1e3);
|
|
942
1008
|
const outcome = await runIntegrationAgent({
|
|
943
1009
|
repoPath,
|
|
944
1010
|
config,
|
|
@@ -946,20 +1012,25 @@ Flutter is live today; ${theme.bright(
|
|
|
946
1012
|
playbook: chosen.playbook,
|
|
947
1013
|
manifest,
|
|
948
1014
|
progress: {
|
|
1015
|
+
onPhase(label) {
|
|
1016
|
+
phaseLabel = label;
|
|
1017
|
+
lastLine = "";
|
|
1018
|
+
},
|
|
949
1019
|
onActivity(line) {
|
|
950
1020
|
lastLine = line;
|
|
951
|
-
spin.message(theme.bright("Integrating") + theme.muted(` \u2014 ${line}`));
|
|
952
1021
|
}
|
|
953
1022
|
}
|
|
954
1023
|
}).catch((err) => {
|
|
955
|
-
spin.stop(theme.alert("Integration failed"));
|
|
956
1024
|
p.log.error(err.message);
|
|
957
1025
|
return null;
|
|
958
1026
|
});
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
1027
|
+
clearInterval(tick);
|
|
1028
|
+
if (!outcome) {
|
|
1029
|
+
spin.stop(theme.alert("Integration stopped"));
|
|
1030
|
+
return 1;
|
|
1031
|
+
}
|
|
1032
|
+
const stopLabel = !outcome.coreOk ? theme.warn("Stopped early \u2014 partial setup is in your working tree") : outcome.eventsComplete ? theme.success("Integration complete") : theme.success("Core integration done") + theme.muted(" \u2014 some events left as follow-ups");
|
|
1033
|
+
spin.stop(stopLabel);
|
|
963
1034
|
const files = await changedFiles(repoPath, checkpoint);
|
|
964
1035
|
if (files.length) {
|
|
965
1036
|
p.note(files.map((f) => theme.muted("\u2022 ") + f).join("\n"), "Files changed");
|
|
@@ -967,6 +1038,11 @@ Flutter is live today; ${theme.bright(
|
|
|
967
1038
|
if (outcome.summary.trim()) {
|
|
968
1039
|
p.log.message(outcome.summary.trim());
|
|
969
1040
|
}
|
|
1041
|
+
p.log.info(
|
|
1042
|
+
theme.muted(
|
|
1043
|
+
`${files.length} file${files.length === 1 ? "" : "s"} changed \xB7 ~$${outcome.costUsd.toFixed(2)} \xB7 ${Math.round(outcome.durationMs / 1e3)}s`
|
|
1044
|
+
)
|
|
1045
|
+
);
|
|
970
1046
|
if (!config.offline) {
|
|
971
1047
|
p.log.step(
|
|
972
1048
|
theme.bright("Run your app once") + theme.muted(" and trigger any tracked action \u2014 I'll watch for the first event.")
|
|
@@ -997,7 +1073,6 @@ Flutter is live today; ${theme.bright(
|
|
|
997
1073
|
].filter(Boolean).join("\n");
|
|
998
1074
|
p.note(nextSteps, theme.bright("Next"));
|
|
999
1075
|
p.outro(theme.signal("\u2301 ") + theme.bright("Whisperr is wired in."));
|
|
1000
|
-
void lastLine;
|
|
1001
1076
|
return 0;
|
|
1002
1077
|
}
|
|
1003
1078
|
async function chooseTarget(detections) {
|
|
@@ -1134,7 +1209,7 @@ async function main() {
|
|
|
1134
1209
|
return;
|
|
1135
1210
|
}
|
|
1136
1211
|
if ("version" in parsed) {
|
|
1137
|
-
console.log("0.1.
|
|
1212
|
+
console.log("0.1.3");
|
|
1138
1213
|
return;
|
|
1139
1214
|
}
|
|
1140
1215
|
try {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@whisperr/wizard",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.3",
|
|
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,
|