@whisperr/wizard 0.1.1 → 0.1.2
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 +151 -92
- 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,94 @@ 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 verifyHint = playbook.verifyCommand ? ` Finish with a single \`${playbook.verifyCommand}\` and fix only what it flags.` : "";
|
|
691
|
+
const corePrompt = [
|
|
692
|
+
`Integrate the Whisperr ${playbook.target.displayName} SDK \u2014 CORE SETUP ONLY.`,
|
|
692
693
|
`Project root: ${repoPath}`,
|
|
693
694
|
"",
|
|
694
|
-
"
|
|
695
|
-
|
|
696
|
-
"
|
|
695
|
+
"Do exactly these, then stop:",
|
|
696
|
+
`1. Add the Whisperr SDK dependency and install it (${playbook.packageRef}).`,
|
|
697
|
+
"2. Initialize it once at app startup with the key + base URL below.",
|
|
698
|
+
"3. Wire identify() where the end-user becomes known (login / signup /",
|
|
699
|
+
" session restore), and reset() on logout.",
|
|
697
700
|
"",
|
|
698
|
-
"
|
|
699
|
-
|
|
700
|
-
"-----
|
|
701
|
-
|
|
701
|
+
"Do NOT instrument product events yet \u2014 that is a separate step." + verifyHint,
|
|
702
|
+
"",
|
|
703
|
+
"----- IDENTIFY -----",
|
|
704
|
+
renderIdentifyBrief(manifest),
|
|
705
|
+
"----- END -----"
|
|
702
706
|
].join("\n");
|
|
703
|
-
const
|
|
707
|
+
const core = await runPass({
|
|
708
|
+
prompt: corePrompt,
|
|
709
|
+
systemPrompt: systemPrompt6,
|
|
710
|
+
repoPath,
|
|
711
|
+
model: config.model,
|
|
712
|
+
maxTurns: 20,
|
|
713
|
+
progress
|
|
714
|
+
});
|
|
715
|
+
costUsd += core.costUsd;
|
|
716
|
+
if (core.summary) summaries.push(`Core setup:
|
|
717
|
+
${core.summary}`);
|
|
718
|
+
let eventsComplete = true;
|
|
719
|
+
if (manifest.events.length > 0) {
|
|
720
|
+
progress?.onPhase?.("Instrumenting your events");
|
|
721
|
+
const eventsPrompt = [
|
|
722
|
+
`The Whisperr ${playbook.target.displayName} SDK is already installed,`,
|
|
723
|
+
"initialized, and identify() is wired. Now add track() calls for product",
|
|
724
|
+
"events.",
|
|
725
|
+
"",
|
|
726
|
+
"How to work:",
|
|
727
|
+
"- Go in the order listed (highest-impact first).",
|
|
728
|
+
"- For each event, Grep for where that user action happens. If there is a",
|
|
729
|
+
" clear client-side call site, add the track() call with the exact name.",
|
|
730
|
+
" If not, SKIP it (most server-side events won't exist here) \u2014 do not hunt.",
|
|
731
|
+
"- You have a limited number of steps; spend them placing events, not",
|
|
732
|
+
" exploring. Stop once the events that clearly exist in this app are wired.",
|
|
733
|
+
"",
|
|
734
|
+
"----- EVENTS -----",
|
|
735
|
+
renderEventsBrief(manifest),
|
|
736
|
+
"----- END -----"
|
|
737
|
+
].join("\n");
|
|
738
|
+
const events = await runPass({
|
|
739
|
+
prompt: eventsPrompt,
|
|
740
|
+
systemPrompt: systemPrompt6,
|
|
741
|
+
repoPath,
|
|
742
|
+
model: config.model,
|
|
743
|
+
maxTurns: config.maxTurns,
|
|
744
|
+
progress
|
|
745
|
+
});
|
|
746
|
+
costUsd += events.costUsd;
|
|
747
|
+
eventsComplete = !events.maxedOut;
|
|
748
|
+
if (events.summary) summaries.push(`Events:
|
|
749
|
+
${events.summary}`);
|
|
750
|
+
}
|
|
751
|
+
return {
|
|
752
|
+
summary: summaries.join("\n\n"),
|
|
753
|
+
costUsd,
|
|
754
|
+
durationMs: Date.now() - started,
|
|
755
|
+
coreOk: core.ok,
|
|
756
|
+
eventsComplete
|
|
757
|
+
};
|
|
758
|
+
}
|
|
759
|
+
async function runPass(opts) {
|
|
760
|
+
const { prompt, systemPrompt: systemPrompt6, repoPath, model, maxTurns, progress } = opts;
|
|
704
761
|
let summary = "";
|
|
705
|
-
let costUsd;
|
|
762
|
+
let costUsd = 0;
|
|
706
763
|
let ok = false;
|
|
764
|
+
let maxedOut = false;
|
|
707
765
|
const response = query({
|
|
708
766
|
prompt,
|
|
709
767
|
options: {
|
|
710
|
-
model
|
|
768
|
+
model,
|
|
711
769
|
cwd: repoPath,
|
|
712
770
|
systemPrompt: systemPrompt6,
|
|
713
|
-
// Full file-system agent toolset.
|
|
714
771
|
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
772
|
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.
|
|
773
|
+
maxTurns,
|
|
721
774
|
settingSources: []
|
|
722
775
|
}
|
|
723
776
|
});
|
|
@@ -732,12 +785,12 @@ When finished, run \`${playbook.verifyCommand}\` and fix anything it flags.` : "
|
|
|
732
785
|
}
|
|
733
786
|
} else if (message.type === "result") {
|
|
734
787
|
ok = message.subtype === "success";
|
|
735
|
-
|
|
788
|
+
maxedOut = (message.subtype ?? "").includes("max_turns");
|
|
789
|
+
costUsd = message.total_cost_usd ?? 0;
|
|
736
790
|
summary = message.result ?? extractLastText(message) ?? summary;
|
|
737
791
|
}
|
|
738
792
|
}
|
|
739
|
-
|
|
740
|
-
return { summary, costUsd, ok, durationMs: Date.now() - started };
|
|
793
|
+
return { summary, costUsd, ok, maxedOut };
|
|
741
794
|
}
|
|
742
795
|
function applyModelAuthEnv(config, session) {
|
|
743
796
|
if (config.directAnthropicKey) {
|
|
@@ -937,8 +990,8 @@ Flutter is live today; ${theme.bright(
|
|
|
937
990
|
);
|
|
938
991
|
}
|
|
939
992
|
const spin = p.spinner();
|
|
940
|
-
|
|
941
|
-
|
|
993
|
+
let phaseLabel = "Integrating";
|
|
994
|
+
spin.start(theme.bright(phaseLabel) + theme.muted(" \u2014 the agent is working in your code"));
|
|
942
995
|
const outcome = await runIntegrationAgent({
|
|
943
996
|
repoPath,
|
|
944
997
|
config,
|
|
@@ -946,20 +999,22 @@ Flutter is live today; ${theme.bright(
|
|
|
946
999
|
playbook: chosen.playbook,
|
|
947
1000
|
manifest,
|
|
948
1001
|
progress: {
|
|
1002
|
+
onPhase(label) {
|
|
1003
|
+
phaseLabel = label;
|
|
1004
|
+
spin.message(theme.bright(label));
|
|
1005
|
+
},
|
|
949
1006
|
onActivity(line) {
|
|
950
|
-
|
|
951
|
-
spin.message(theme.bright("Integrating") + theme.muted(` \u2014 ${line}`));
|
|
1007
|
+
spin.message(theme.bright(phaseLabel) + theme.muted(` \u2014 ${line}`));
|
|
952
1008
|
}
|
|
953
1009
|
}
|
|
954
1010
|
}).catch((err) => {
|
|
955
|
-
spin.stop(theme.alert("Integration
|
|
1011
|
+
spin.stop(theme.alert("Integration stopped"));
|
|
956
1012
|
p.log.error(err.message);
|
|
957
1013
|
return null;
|
|
958
1014
|
});
|
|
959
1015
|
if (!outcome) return 1;
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
);
|
|
1016
|
+
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");
|
|
1017
|
+
spin.stop(stopLabel);
|
|
963
1018
|
const files = await changedFiles(repoPath, checkpoint);
|
|
964
1019
|
if (files.length) {
|
|
965
1020
|
p.note(files.map((f) => theme.muted("\u2022 ") + f).join("\n"), "Files changed");
|
|
@@ -967,6 +1022,11 @@ Flutter is live today; ${theme.bright(
|
|
|
967
1022
|
if (outcome.summary.trim()) {
|
|
968
1023
|
p.log.message(outcome.summary.trim());
|
|
969
1024
|
}
|
|
1025
|
+
p.log.info(
|
|
1026
|
+
theme.muted(
|
|
1027
|
+
`${files.length} file${files.length === 1 ? "" : "s"} changed \xB7 ~$${outcome.costUsd.toFixed(2)} \xB7 ${Math.round(outcome.durationMs / 1e3)}s`
|
|
1028
|
+
)
|
|
1029
|
+
);
|
|
970
1030
|
if (!config.offline) {
|
|
971
1031
|
p.log.step(
|
|
972
1032
|
theme.bright("Run your app once") + theme.muted(" and trigger any tracked action \u2014 I'll watch for the first event.")
|
|
@@ -997,7 +1057,6 @@ Flutter is live today; ${theme.bright(
|
|
|
997
1057
|
].filter(Boolean).join("\n");
|
|
998
1058
|
p.note(nextSteps, theme.bright("Next"));
|
|
999
1059
|
p.outro(theme.signal("\u2301 ") + theme.bright("Whisperr is wired in."));
|
|
1000
|
-
void lastLine;
|
|
1001
1060
|
return 0;
|
|
1002
1061
|
}
|
|
1003
1062
|
async function chooseTarget(detections) {
|
|
@@ -1134,7 +1193,7 @@ async function main() {
|
|
|
1134
1193
|
return;
|
|
1135
1194
|
}
|
|
1136
1195
|
if ("version" in parsed) {
|
|
1137
|
-
console.log("0.1.
|
|
1196
|
+
console.log("0.1.2");
|
|
1138
1197
|
return;
|
|
1139
1198
|
}
|
|
1140
1199
|
try {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@whisperr/wizard",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.2",
|
|
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,
|