@whisperr/wizard 0.1.4 → 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.
Files changed (2) hide show
  1. package/dist/index.js +94 -5
  2. 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
  });
@@ -680,10 +681,17 @@ function renderEventsBrief(m) {
680
681
  lines.push(
681
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.`
682
683
  );
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
+ }
683
690
  for (const e of events) {
684
691
  lines.push("");
685
692
  const why = e.interventions?.length ? ` \xB7 drives: ${e.interventions.map((i) => i.label ?? i.code).join(", ")}` : "";
686
- lines.push(`\u25A0 ${e.eventType}${e.label ? ` (${e.label})` : ""}${why}`);
693
+ const cov = coverageNote(e.coverage);
694
+ lines.push(`\u25A0 ${e.eventType}${e.label ? ` (${e.label})` : ""}${why}${cov}`);
687
695
  if (e.description) lines.push(` ${e.description}`);
688
696
  if (e.properties?.length) {
689
697
  lines.push(" properties to capture if available:");
@@ -696,6 +704,16 @@ function renderEventsBrief(m) {
696
704
  }
697
705
  return lines.join("\n");
698
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
+ }
699
717
 
700
718
  // src/core/agent.ts
701
719
  async function runIntegrationAgent(opts) {
@@ -867,6 +885,9 @@ function short(s, max = 60) {
867
885
 
868
886
  // src/core/git.ts
869
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";
870
891
  async function takeCheckpoint(repoPath) {
871
892
  const isRepo = (await run(repoPath, ["rev-parse", "--is-inside-work-tree"])).ok;
872
893
  if (!isRepo) return { isRepo: false };
@@ -897,6 +918,38 @@ function revertHint(checkpoint) {
897
918
  if (!checkpoint.isRepo || !checkpoint.baseRef) return void 0;
898
919
  return `git restore . && git clean -fd (back to ${checkpoint.baseRef.slice(0, 7)})`;
899
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
+ }
900
953
  function run(cwd, args) {
901
954
  return new Promise((resolve2) => {
902
955
  const child = spawn("git", args, { cwd });
@@ -934,6 +987,22 @@ async function pollFirstEvent(config, session, opts = {}) {
934
987
  return { received: false };
935
988
  }
936
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
+
937
1006
  // src/ui/banner.ts
938
1007
  function banner() {
939
1008
  const wave = theme.signal("\u223F\u223F\u223F");
@@ -997,9 +1066,10 @@ Flutter is live today; ${theme.bright(
997
1066
  p.cancel(theme.alert(err.message));
998
1067
  return 1;
999
1068
  }
1069
+ const fingerprint = await repoFingerprint(repoPath);
1000
1070
  const manifest = await withSpinner(
1001
1071
  "Loading your onboarding context",
1002
- () => fetchManifest(config, session, chosen.playbook.target.id)
1072
+ () => fetchManifest(config, session, chosen.playbook.target.id, fingerprint)
1003
1073
  );
1004
1074
  p.note(
1005
1075
  summarizeManifest(manifest),
@@ -1055,9 +1125,28 @@ Flutter is live today; ${theme.bright(
1055
1125
  if (outcome.summary.trim()) {
1056
1126
  p.log.message(outcome.summary.trim());
1057
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
+ });
1058
1147
  p.log.info(
1059
1148
  theme.muted(
1060
- `${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`
1061
1150
  )
1062
1151
  );
1063
1152
  if (!config.offline) {
@@ -1226,7 +1315,7 @@ async function main() {
1226
1315
  return;
1227
1316
  }
1228
1317
  if ("version" in parsed) {
1229
- console.log("0.1.4");
1318
+ console.log("0.1.5");
1230
1319
  return;
1231
1320
  }
1232
1321
  try {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@whisperr/wizard",
3
- "version": "0.1.4",
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,