@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.
Files changed (2) hide show
  1. package/dist/index.js +151 -92
  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,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 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 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
- "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.",
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
- "----- MANIFEST -----",
699
- renderManifestBrief(manifest),
700
- "----- END MANIFEST -----",
701
- verifyHint
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 started = Date.now();
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: config.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: 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.
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
- costUsd = message.total_cost_usd;
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
- progress?.onResult?.(summary, costUsd);
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
- spin.start(theme.bright("Integrating") + theme.muted(" \u2014 the agent is working in your code"));
941
- let lastLine = "";
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
- lastLine = line;
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 failed"));
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
- spin.stop(
961
- outcome.ok ? theme.success("Integration complete") : theme.warn("Integration finished with notes")
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.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.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,