@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.
Files changed (2) hide show
  1. package/dist/index.js +166 -91
  2. 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-opus-4-8";
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 ?? 40),
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. You are running
600
- INSIDE a customer's code repository. Your single job: integrate the Whisperr SDK
601
- so the product starts sending the right events to Whisperr, then stop.
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
- Whisperr is a churn-prevention engine. It needs two things from the app:
604
- 1. identify() \u2014 tell Whisperr who the end-user is (a stable external user id)
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
- Operating rules:
613
- - Work autonomously. Make the edits directly; do not ask for confirmation.
614
- - Be surgical. Add the SDK dependency, initialize it once at app startup, wire
615
- identify() where the user becomes known, and add track() calls for the
616
- manifest events. Do not refactor unrelated code, restyle, or "improve" things.
617
- - Find the RIGHT call site for each named event by reading the code, not by
618
- guessing. If you genuinely cannot find where an event occurs, add a clearly
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
- When done, output a concise summary:
629
- - files changed
630
- - where identify() was wired
631
- - each manifest event -> the call site you instrumented (or TODO + reason)
632
- - any follow-ups the human must do (env var, push token, etc.)
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 renderManifestBrief(m) {
636
+ function renderIdentifyBrief(m) {
635
637
  const lines = [];
636
- lines.push(`# Whisperr integration manifest for app: ${m.appName ?? m.appId}`);
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("## Business context");
643
- lines.push(m.businessContext);
643
+ lines.push(`Context: ${m.businessContext}`);
644
644
  }
645
645
  lines.push("");
646
- lines.push("## identify()");
646
+ lines.push("identify():");
647
647
  if (m.identify.traits?.length) {
648
- lines.push("Traits to send when available:");
648
+ lines.push(" Send these traits when available:");
649
649
  for (const t of m.identify.traits) {
650
- lines.push(` - ${t.name}${t.description ? ` \u2014 ${t.description}` : ""}`);
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.push("");
658
- lines.push("## Events to instrument (use these exact event_type strings)");
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
- lines.push("");
664
- lines.push(`### ${e.eventType}${e.label ? ` (${e.label})` : ""}`);
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("Properties:");
668
- for (const p2 of e.properties) {
669
- lines.push(
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 verifyHint = playbook.verifyCommand ? `
688
-
689
- When finished, run \`${playbook.verifyCommand}\` and fix anything it flags.` : "";
690
- const prompt = [
691
- `Integrate the Whisperr ${playbook.target.displayName} SDK into this repository.`,
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
- "Here is the integration manifest derived from this customer's Whisperr",
695
- "onboarding. Wire identify() and instrument every event below at the correct",
696
- "call site, following the SDK guide in your system prompt.",
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
- "----- MANIFEST -----",
699
- renderManifestBrief(manifest),
700
- "----- END MANIFEST -----",
701
- verifyHint
708
+ "----- IDENTIFY -----",
709
+ renderIdentifyBrief(manifest),
710
+ "----- END -----"
702
711
  ].join("\n");
703
- const started = Date.now();
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: config.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: config.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
- costUsd = message.total_cost_usd;
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
- progress?.onResult?.(summary, costUsd);
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
- spin.start(theme.bright("Integrating") + theme.muted(" \u2014 the agent is working in your code"));
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
- if (!outcome) return 1;
960
- spin.stop(
961
- outcome.ok ? theme.success("Integration complete") : theme.warn("Integration finished with notes")
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.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.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,