@whisperr/wizard 0.1.15 → 0.2.1
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 +855 -161
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
// src/index.ts
|
|
4
4
|
import { readFileSync, realpathSync } from "fs";
|
|
5
|
-
import { dirname, join as
|
|
5
|
+
import { dirname as dirname2, join as join4 } from "path";
|
|
6
6
|
import { fileURLToPath } from "url";
|
|
7
7
|
|
|
8
8
|
// src/cli.ts
|
|
@@ -647,51 +647,135 @@ var phpPlaybook = {
|
|
|
647
647
|
// src/core/playbooks/react-native.ts
|
|
648
648
|
var target7 = {
|
|
649
649
|
id: "react-native",
|
|
650
|
-
displayName: "React Native",
|
|
650
|
+
displayName: "React Native / Expo",
|
|
651
651
|
language: "typescript",
|
|
652
|
-
availability: "
|
|
652
|
+
availability: "available"
|
|
653
653
|
};
|
|
654
654
|
async function detect7(ctx) {
|
|
655
655
|
if (!await ctx.exists("package.json")) return null;
|
|
656
656
|
const pkgJson = await ctx.read("package.json");
|
|
657
|
-
let
|
|
657
|
+
let hasRN = false;
|
|
658
|
+
let hasExpo = false;
|
|
658
659
|
try {
|
|
659
660
|
const pkg = JSON.parse(pkgJson);
|
|
660
661
|
const all = { ...pkg.dependencies ?? {}, ...pkg.devDependencies ?? {} };
|
|
661
|
-
|
|
662
|
+
hasRN = "react-native" in all;
|
|
663
|
+
hasExpo = "expo" in all;
|
|
662
664
|
} catch {
|
|
663
|
-
match = false;
|
|
664
665
|
}
|
|
665
|
-
if (!
|
|
666
|
-
const evidence = [
|
|
666
|
+
if (!hasRN && !hasExpo) return null;
|
|
667
|
+
const evidence = [];
|
|
668
|
+
if (hasExpo) evidence.push("expo dependency");
|
|
669
|
+
if (hasRN) evidence.push("react-native dependency");
|
|
667
670
|
let confidence = 0.85;
|
|
668
671
|
if (await ctx.exists("app.json")) {
|
|
669
672
|
evidence.push("app.json");
|
|
670
673
|
confidence += 0.1;
|
|
674
|
+
} else if (await ctx.exists("app.config.js") || await ctx.exists("app.config.ts")) {
|
|
675
|
+
evidence.push("app.config");
|
|
676
|
+
confidence += 0.1;
|
|
671
677
|
}
|
|
672
678
|
return { target: target7, confidence: Math.min(confidence, 1), evidence };
|
|
673
679
|
}
|
|
674
680
|
var systemPrompt7 = `
|
|
675
681
|
## SDK: Whisperr for React Native \u2014 package \`@whisperr/react-native\`
|
|
676
682
|
|
|
677
|
-
|
|
678
|
-
|
|
683
|
+
Pure TypeScript, ZERO native code and zero dependencies of its own: it works in
|
|
684
|
+
Expo Go, bare React Native, and dev clients with no pod install, no config
|
|
685
|
+
plugin, no prebuild. Do not add or modify anything under ios/ or android/ for
|
|
686
|
+
the SDK itself.
|
|
687
|
+
|
|
688
|
+
0) Expo or bare? Decide first \u2014 it changes storage install + key handling.
|
|
689
|
+
Expo = \`expo\` in package.json dependencies (usually with app.json /
|
|
690
|
+
app.config.* containing an "expo" key).
|
|
691
|
+
|
|
692
|
+
1) Install with the repo's package manager (pick by lockfile: pnpm-lock.yaml \u2192
|
|
693
|
+
pnpm, yarn.lock \u2192 yarn, bun.lockb \u2192 bun, else npm):
|
|
694
|
+
<pm> add @whisperr/react-native
|
|
695
|
+
|
|
696
|
+
2) Durable storage \u2014 IMPORTANT. The SDK's offline queue is MEMORY-ONLY unless
|
|
697
|
+
you inject an AsyncStorage-compatible adapter (\`getItem\`/\`setItem\`/
|
|
698
|
+
\`removeItem\`); the SDK never imports native modules itself. In priority
|
|
699
|
+
order:
|
|
700
|
+
- \`@react-native-async-storage/async-storage\` already a dependency \u2192 import
|
|
701
|
+
it and pass as \`storage\`.
|
|
702
|
+
- The app already uses \`react-native-mmkv\` \u2192 reuse its existing instance
|
|
703
|
+
with a 3-line adapter (do NOT add a new storage dep):
|
|
704
|
+
const storage = {
|
|
705
|
+
getItem: (k: string) => mmkv.getString(k) ?? null,
|
|
706
|
+
setItem: (k: string, v: string) => { mmkv.set(k, v); },
|
|
707
|
+
removeItem: (k: string) => { mmkv.delete(k); },
|
|
708
|
+
};
|
|
709
|
+
- Neither \u2192 add async-storage. Expo: run \`npx expo install
|
|
710
|
+
@react-native-async-storage/async-storage\` via bash (installs the
|
|
711
|
+
SDK-matched version; works in Expo Go \u2014 it ships in the Go client). Bare
|
|
712
|
+
RN: \`<pm> add @react-native-async-storage/async-storage\` and note
|
|
713
|
+
"run npx pod-install before the next iOS build" as a follow-up (it IS a
|
|
714
|
+
native module; do not run pod install yourself).
|
|
715
|
+
Never silently ship without storage \u2014 if you genuinely can't add it, say so
|
|
716
|
+
in the summary.
|
|
717
|
+
|
|
718
|
+
3) Initialize ONCE via a module singleton, e.g. src/whisperr.ts (match the
|
|
719
|
+
repo's source layout \u2014 src/, lib/, app/lib/\u2026):
|
|
720
|
+
import AsyncStorage from '@react-native-async-storage/async-storage';
|
|
721
|
+
import { Whisperr } from '@whisperr/react-native';
|
|
679
722
|
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
723
|
+
export const whisperr = Whisperr.init({
|
|
724
|
+
apiKey: process.env.EXPO_PUBLIC_WHISPERR_KEY!,
|
|
725
|
+
storage: AsyncStorage,
|
|
726
|
+
baseUrl: '<INGESTION_BASE_URL from manifest>', // omit if it's the SDK default
|
|
727
|
+
});
|
|
728
|
+
Import the module for its side effect from the app entry: Expo Router \u2192
|
|
729
|
+
app/_layout.tsx (root layout); bare / React Navigation \u2192 App.tsx (or
|
|
730
|
+
index.js). Prefer the singleton over <WhisperrProvider> \u2014 auth listeners,
|
|
731
|
+
API clients, and non-component code can import it directly. If the codebase
|
|
732
|
+
is strongly hook-oriented you may ADDITIONALLY wrap the root with
|
|
733
|
+
<WhisperrProvider client={whisperr}> and use useWhisperr() in components \u2014
|
|
734
|
+
but never create a second client.
|
|
735
|
+
Whisperr.init() is an idempotent singleton \u2014 safe under Fast Refresh and
|
|
736
|
+
React StrictMode.
|
|
737
|
+
|
|
738
|
+
Key handling (the key is a publishable ingestion key \u2014 shipping it in the
|
|
739
|
+
bundle is expected):
|
|
740
|
+
- Expo SDK 49+: put EXPO_PUBLIC_WHISPERR_KEY=<key> in .env (create or
|
|
741
|
+
append) and mirror it in .env.example; reference via
|
|
742
|
+
process.env.EXPO_PUBLIC_WHISPERR_KEY.
|
|
743
|
+
- Bare RN with react-native-config: add WHISPERR_KEY to .env and read
|
|
744
|
+
Config.WHISPERR_KEY.
|
|
745
|
+
- Otherwise: a small constants file with the literal key is acceptable.
|
|
746
|
+
|
|
747
|
+
4) identify() right after the end-user is known \u2014 on login/signup success AND
|
|
748
|
+
on session restore at app startup:
|
|
749
|
+
whisperr.identify(user.id, {
|
|
750
|
+
traits: { /* manifest traits sourced from the user object */ },
|
|
751
|
+
email: user.email, // shortcut \u2192 opted-in email channel
|
|
752
|
+
phone: user.phone, // shortcut \u2192 sms channel
|
|
753
|
+
pushToken: expoPushToken, // shortcut \u2192 push channel, if the app
|
|
754
|
+
// registers for push (expo-notifications
|
|
755
|
+
// or @react-native-firebase/messaging)
|
|
756
|
+
});
|
|
757
|
+
Call whisperr.reset() on logout (already-queued events keep their user).
|
|
758
|
+
ANONYMOUS EVENTS ARE FINE: track() before identify() buffers on-device and
|
|
759
|
+
attributes to the user retroactively on identify() \u2014 do NOT gate track()
|
|
760
|
+
calls behind auth state, and do NOT skip pre-login manifest events.
|
|
761
|
+
|
|
762
|
+
5) track() in event handlers / business logic (onPress, mutation onSuccess,
|
|
763
|
+
store/saga/thunk actions) \u2014 never in render:
|
|
764
|
+
whisperr.track('event_type_from_manifest', { /* properties in scope */ });
|
|
765
|
+
Synchronous fire-and-forget: it returns void \u2014 do NOT await it. Batching,
|
|
766
|
+
retries, background flush are all internal. event_type verbatim snake_case
|
|
767
|
+
from the manifest \u2014 the SDK client-side drops invalid names.
|
|
690
768
|
|
|
691
|
-
Notes:
|
|
692
|
-
-
|
|
693
|
-
|
|
694
|
-
|
|
769
|
+
Notes / gotchas:
|
|
770
|
+
- Every call is sync void except flush(); the only sensible manual flush is
|
|
771
|
+
\`await whisperr.flush()\` right before something destroys the process.
|
|
772
|
+
- The SDK auto-flushes on an interval, at 20 queued events, and when the app
|
|
773
|
+
backgrounds. Don't sprinkle flush() calls.
|
|
774
|
+
- whisperr.screen(name) records a screen_viewed event \u2014 wire it to the
|
|
775
|
+
navigation container's state-change callback ONLY if the manifest includes a
|
|
776
|
+
screen/page-view style event; otherwise leave navigation alone.
|
|
777
|
+
- Works on the New Architecture and in Expo Go by construction (no native
|
|
778
|
+
code). Never suggest expo prebuild / dev-client for this SDK.
|
|
695
779
|
`.trim();
|
|
696
780
|
var reactNativePlaybook = {
|
|
697
781
|
target: target7,
|
|
@@ -703,9 +787,9 @@ var reactNativePlaybook = {
|
|
|
703
787
|
// src/core/playbooks/swift.ts
|
|
704
788
|
var target8 = {
|
|
705
789
|
id: "swift",
|
|
706
|
-
displayName: "Swift (iOS)",
|
|
790
|
+
displayName: "Swift (iOS / Apple)",
|
|
707
791
|
language: "swift",
|
|
708
|
-
availability: "
|
|
792
|
+
availability: "available"
|
|
709
793
|
};
|
|
710
794
|
async function detect8(ctx) {
|
|
711
795
|
const evidence = [];
|
|
@@ -727,40 +811,115 @@ async function detect8(ctx) {
|
|
|
727
811
|
evidence.push("Podfile");
|
|
728
812
|
confidence += 0.2;
|
|
729
813
|
}
|
|
814
|
+
if (await ctx.exists("Project.swift")) {
|
|
815
|
+
evidence.push("Project.swift (Tuist)");
|
|
816
|
+
confidence += 0.4;
|
|
817
|
+
}
|
|
818
|
+
if (await ctx.exists("project.yml")) {
|
|
819
|
+
evidence.push("project.yml (XcodeGen)");
|
|
820
|
+
confidence += 0.3;
|
|
821
|
+
}
|
|
730
822
|
if (confidence === 0) return null;
|
|
731
823
|
return { target: target8, confidence: Math.min(confidence, 1), evidence };
|
|
732
824
|
}
|
|
733
825
|
var systemPrompt8 = `
|
|
734
|
-
## SDK: Whisperr for Swift
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
826
|
+
## SDK: Whisperr for Swift \u2014 Swift Package \`Whisperr\` (github.com/WhisperrAI/whisperr-swift)
|
|
827
|
+
|
|
828
|
+
Platforms: iOS 13+, macOS 12+, tvOS 13+, watchOS 6+. The client is a Swift
|
|
829
|
+
ACTOR: every call is \`await\`ed, and identify()/track() are throwing. Getting
|
|
830
|
+
the concurrency and JSONValue details below right is what makes the diff
|
|
831
|
+
compile first try.
|
|
832
|
+
|
|
833
|
+
1) Dependency \u2014 pick by how the project is defined:
|
|
834
|
+
- Package.swift: add to the package's dependencies
|
|
835
|
+
.package(url: "https://github.com/WhisperrAI/whisperr-swift.git", from: "0.1.0")
|
|
836
|
+
and \`"Whisperr"\` to the app target's dependencies array.
|
|
837
|
+
- Tuist (Project.swift) or XcodeGen (project.yml): these manifests are plain
|
|
838
|
+
text \u2014 add the SPM package + product there following the file's existing
|
|
839
|
+
style.
|
|
840
|
+
- Raw .xcodeproj / .xcworkspace only: do NOT hand-edit project.pbxproj to
|
|
841
|
+
add a package reference (fragile, breaks the project on a bad GUID).
|
|
842
|
+
Write ALL the integration code anyway (imports, init, identify, track),
|
|
843
|
+
add a short WHISPERR_SETUP.md with the one manual step \u2014 Xcode \u2192 File \u2192
|
|
844
|
+
Add Package Dependencies\u2026 \u2192 \`https://github.com/WhisperrAI/whisperr-swift\`
|
|
845
|
+
\u2192 add "Whisperr" to the app target \u2014 and report that as the single manual
|
|
846
|
+
follow-up in your summary.
|
|
847
|
+
|
|
848
|
+
2) Initialize once at launch.
|
|
849
|
+
SwiftUI:
|
|
850
|
+
import Whisperr
|
|
851
|
+
|
|
852
|
+
@main
|
|
853
|
+
struct MyApp: App {
|
|
854
|
+
init() {
|
|
855
|
+
Task { await Whisperr.initialize(apiKey: "<INGESTION_API_KEY>") }
|
|
856
|
+
}
|
|
857
|
+
// ... existing body
|
|
858
|
+
}
|
|
859
|
+
UIKit: same Task { } inside application(_:didFinishLaunchingOptions:).
|
|
860
|
+
Only pass a base URL if the manifest's differs from the SDK default:
|
|
861
|
+
await Whisperr.initialize(apiKey: "...", baseURL: URL(string: "<INGESTION_BASE_URL>")!)
|
|
862
|
+
Note baseURL takes a URL, not a String. Prefer reading the key from
|
|
863
|
+
Info.plist / an .xcconfig if the project already uses that pattern;
|
|
864
|
+
otherwise a literal is acceptable (publishable ingestion key).
|
|
865
|
+
|
|
866
|
+
3) identify() right after the end-user is known \u2014 login/signup success AND
|
|
867
|
+
session restore. All calls go through the async optional singleton
|
|
868
|
+
\`await Whisperr.shared\`; in synchronous contexts wrap in Task { }:
|
|
869
|
+
Task {
|
|
870
|
+
try? await Whisperr.shared?.identify(
|
|
871
|
+
user.id, // stable external id \u2014 never a device id
|
|
872
|
+
traits: ["plan": .string(user.plan)],
|
|
873
|
+
email: user.email, // shortcut \u2192 opted-in email channel
|
|
874
|
+
phone: user.phone, // shortcut \u2192 sms channel
|
|
875
|
+
pushToken: apnsTokenHex // if the app registers for remote notifications
|
|
876
|
+
)
|
|
877
|
+
}
|
|
878
|
+
Logout:
|
|
879
|
+
Task { await Whisperr.shared?.reset() } // flushes, then clears the user
|
|
880
|
+
For explicit channel consent/verification use WhisperrChannel:
|
|
881
|
+
channels: [.email("a@b.com", verified: true), .sms("+1555\u2026", optedIn: false)]
|
|
882
|
+
|
|
883
|
+
4) track() in action handlers / view-model methods \u2014 never in a View body:
|
|
884
|
+
Task {
|
|
885
|
+
try? await Whisperr.shared?.track(
|
|
886
|
+
"event_type_from_manifest",
|
|
887
|
+
properties: ["amount_cents": .number(Double(amountCents)),
|
|
888
|
+
"plan": .string(plan)]
|
|
889
|
+
)
|
|
890
|
+
}
|
|
891
|
+
Awaiting only ENQUEUES (delivery batches in the background) \u2014 \`try? await\`
|
|
892
|
+
inside Task { } is the correct fire-and-forget in UI code. In an already-
|
|
893
|
+
async throwing context, plain \`try await\` is fine. event_type verbatim
|
|
894
|
+
snake_case from the manifest.
|
|
895
|
+
|
|
896
|
+
CRITICAL \u2014 JSONValue typing for properties/traits:
|
|
897
|
+
Values are \`JSONValue\`, not \`Any\`. LITERALS convert automatically \u2014
|
|
898
|
+
\`["plan": "pro", "amount": 42, "active": true]\` compiles as-is \u2014 but
|
|
899
|
+
VARIABLES must be wrapped: \`.string(user.plan)\`, \`.number(Double(count))\`,
|
|
900
|
+
\`.bool(flag)\`, \`.array(...)\`, \`.object(...)\`. Mixing literals and wrapped
|
|
901
|
+
variables in one dictionary is fine. Forgetting the wrapper is the #1
|
|
902
|
+
compile error \u2014 check every non-literal value you pass.
|
|
903
|
+
|
|
904
|
+
CRITICAL \u2014 no anonymous buffering in this SDK:
|
|
905
|
+
track() THROWS WhisperrClientError.missingUserID until identify() has run in
|
|
906
|
+
this process (or when you pass \`userID:\` explicitly). Session restore must
|
|
907
|
+
therefore call identify() early at startup. Do not place track() on
|
|
908
|
+
pre-auth surfaces (onboarding, login screen) unless you pass
|
|
909
|
+
\`userID: "<id>"\` \u2014 if a manifest event genuinely happens before the user
|
|
910
|
+
exists, skip it and note it in the summary.
|
|
751
911
|
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
-
|
|
758
|
-
- Do not attempt to run xcodebuild; verification is the human's step here.
|
|
912
|
+
Notes / gotchas:
|
|
913
|
+
- \`Whisperr.shared\` is \`async\` and optional \u2014 the call shape is always
|
|
914
|
+
\`await Whisperr.shared?.method(...)\`. Never store it in a non-async global.
|
|
915
|
+
- The queue persists via UserDefaults and survives restarts; retries, backoff,
|
|
916
|
+
and auth-pause semantics are built in. No manual flush() needed.
|
|
917
|
+
- Do not run xcodebuild, swift build, or tests \u2014 the human verifies in Xcode.
|
|
759
918
|
`.trim();
|
|
760
919
|
var swiftPlaybook = {
|
|
761
920
|
target: target8,
|
|
762
921
|
detect: detect8,
|
|
763
|
-
packageRef: "
|
|
922
|
+
packageRef: "whisperr-swift (Swift Package)",
|
|
764
923
|
systemPrompt: systemPrompt8
|
|
765
924
|
};
|
|
766
925
|
|
|
@@ -989,6 +1148,341 @@ function mockManifest(appId, config) {
|
|
|
989
1148
|
// src/core/agent.ts
|
|
990
1149
|
import { query } from "@anthropic-ai/claude-agent-sdk";
|
|
991
1150
|
|
|
1151
|
+
// src/core/opportunities.ts
|
|
1152
|
+
import { readFile as readFile3, rm as rm2 } from "fs/promises";
|
|
1153
|
+
import { join as join3 } from "path";
|
|
1154
|
+
|
|
1155
|
+
// src/core/git.ts
|
|
1156
|
+
import { spawn } from "child_process";
|
|
1157
|
+
import { createHash } from "crypto";
|
|
1158
|
+
import { mkdir, readFile as readFile2, rm, writeFile } from "fs/promises";
|
|
1159
|
+
import { basename, dirname, join as join2 } from "path";
|
|
1160
|
+
async function takeCheckpoint(repoPath) {
|
|
1161
|
+
const isRepo = (await run(repoPath, ["rev-parse", "--is-inside-work-tree"])).ok;
|
|
1162
|
+
if (!isRepo) return { isRepo: false };
|
|
1163
|
+
const branch = (await run(repoPath, ["rev-parse", "--abbrev-ref", "HEAD"])).stdout.trim();
|
|
1164
|
+
const head = await run(repoPath, ["rev-parse", "HEAD"]);
|
|
1165
|
+
return {
|
|
1166
|
+
isRepo: true,
|
|
1167
|
+
baseRef: head.ok ? head.stdout.trim() : void 0,
|
|
1168
|
+
branch: branch || void 0
|
|
1169
|
+
};
|
|
1170
|
+
}
|
|
1171
|
+
async function changedFiles(repoPath, checkpoint) {
|
|
1172
|
+
if (!checkpoint.isRepo) return [];
|
|
1173
|
+
const tracked = await run(repoPath, ["diff", "--name-only"]);
|
|
1174
|
+
const untracked = await run(repoPath, [
|
|
1175
|
+
"ls-files",
|
|
1176
|
+
"--others",
|
|
1177
|
+
"--exclude-standard"
|
|
1178
|
+
]);
|
|
1179
|
+
const set = /* @__PURE__ */ new Set();
|
|
1180
|
+
for (const f of `${tracked.stdout}
|
|
1181
|
+
${untracked.stdout}`.split("\n")) {
|
|
1182
|
+
if (f.trim()) set.add(f.trim());
|
|
1183
|
+
}
|
|
1184
|
+
return [...set];
|
|
1185
|
+
}
|
|
1186
|
+
function revertHint(checkpoint) {
|
|
1187
|
+
if (!checkpoint.isRepo || !checkpoint.baseRef) return void 0;
|
|
1188
|
+
return `git restore . && git clean -fd (back to ${checkpoint.baseRef.slice(0, 7)})`;
|
|
1189
|
+
}
|
|
1190
|
+
async function wasTrackedAtCheckpoint(repoPath, checkpoint, file) {
|
|
1191
|
+
if (!checkpoint.isRepo) return false;
|
|
1192
|
+
if (checkpoint.baseRef) {
|
|
1193
|
+
const res2 = await run(repoPath, [
|
|
1194
|
+
"ls-tree",
|
|
1195
|
+
"--name-only",
|
|
1196
|
+
checkpoint.baseRef,
|
|
1197
|
+
"--",
|
|
1198
|
+
file
|
|
1199
|
+
]);
|
|
1200
|
+
return res2.ok && res2.stdout.trim() !== "";
|
|
1201
|
+
}
|
|
1202
|
+
const res = await run(repoPath, ["ls-files", "--", file]);
|
|
1203
|
+
return res.ok && res.stdout.trim() !== "";
|
|
1204
|
+
}
|
|
1205
|
+
async function isWorkingTreeClean(repoPath) {
|
|
1206
|
+
const status = await run(repoPath, ["status", "--porcelain"]);
|
|
1207
|
+
return status.ok && status.stdout.trim() === "";
|
|
1208
|
+
}
|
|
1209
|
+
async function revertToCheckpoint(repoPath, checkpoint) {
|
|
1210
|
+
if (!checkpoint.isRepo) return false;
|
|
1211
|
+
if (checkpoint.baseRef) {
|
|
1212
|
+
const reset = await run(repoPath, ["reset", "--hard", checkpoint.baseRef]);
|
|
1213
|
+
const clean2 = await run(repoPath, ["clean", "-fd"]);
|
|
1214
|
+
return reset.ok && clean2.ok;
|
|
1215
|
+
}
|
|
1216
|
+
const restore = await run(repoPath, ["restore", "."]);
|
|
1217
|
+
const clean = await run(repoPath, ["clean", "-fd"]);
|
|
1218
|
+
return restore.ok && clean.ok;
|
|
1219
|
+
}
|
|
1220
|
+
async function snapshotChanges(repoPath, checkpoint) {
|
|
1221
|
+
const snapshot = /* @__PURE__ */ new Map();
|
|
1222
|
+
for (const file of await changedFiles(repoPath, checkpoint)) {
|
|
1223
|
+
try {
|
|
1224
|
+
snapshot.set(file, await readFile2(join2(repoPath, file)));
|
|
1225
|
+
} catch {
|
|
1226
|
+
snapshot.set(file, null);
|
|
1227
|
+
}
|
|
1228
|
+
}
|
|
1229
|
+
return snapshot;
|
|
1230
|
+
}
|
|
1231
|
+
function snapshotsEqual(a, b) {
|
|
1232
|
+
if (a.size !== b.size) return false;
|
|
1233
|
+
for (const [file, content] of a) {
|
|
1234
|
+
const other = b.get(file);
|
|
1235
|
+
if (other === void 0) return false;
|
|
1236
|
+
if (content === null || other === null) {
|
|
1237
|
+
if (content !== other) return false;
|
|
1238
|
+
} else if (!content.equals(other)) {
|
|
1239
|
+
return false;
|
|
1240
|
+
}
|
|
1241
|
+
}
|
|
1242
|
+
return true;
|
|
1243
|
+
}
|
|
1244
|
+
async function restoreToSnapshot(repoPath, checkpoint, snapshot) {
|
|
1245
|
+
try {
|
|
1246
|
+
const current = await changedFiles(repoPath, checkpoint);
|
|
1247
|
+
for (const file of /* @__PURE__ */ new Set([...current, ...snapshot.keys()])) {
|
|
1248
|
+
const recorded = snapshot.get(file);
|
|
1249
|
+
const path = join2(repoPath, file);
|
|
1250
|
+
if (recorded === void 0) {
|
|
1251
|
+
if (checkpoint.baseRef) {
|
|
1252
|
+
const co = await run(repoPath, ["checkout", checkpoint.baseRef, "--", file]);
|
|
1253
|
+
if (co.ok) continue;
|
|
1254
|
+
}
|
|
1255
|
+
await rm(path, { force: true });
|
|
1256
|
+
} else if (recorded === null) {
|
|
1257
|
+
await rm(path, { force: true });
|
|
1258
|
+
} else {
|
|
1259
|
+
await mkdir(dirname(path), { recursive: true });
|
|
1260
|
+
await writeFile(path, recorded);
|
|
1261
|
+
}
|
|
1262
|
+
}
|
|
1263
|
+
return true;
|
|
1264
|
+
} catch {
|
|
1265
|
+
return false;
|
|
1266
|
+
}
|
|
1267
|
+
}
|
|
1268
|
+
async function repoFingerprint(repoPath) {
|
|
1269
|
+
const remote = await run(repoPath, ["remote", "get-url", "origin"]);
|
|
1270
|
+
const seed = remote.ok && remote.stdout.trim() ? normalizeRemote(remote.stdout.trim()) : `dir:${basename(repoPath)}`;
|
|
1271
|
+
return createHash("sha256").update(seed).digest("hex").slice(0, 16);
|
|
1272
|
+
}
|
|
1273
|
+
function normalizeRemote(url) {
|
|
1274
|
+
return url.replace(/^git@([^:]+):/, "$1/").replace(/^https?:\/\//, "").replace(/\.git$/, "").replace(/\/+$/, "").toLowerCase();
|
|
1275
|
+
}
|
|
1276
|
+
async function scanWiredEvents(repoPath, files, eventTypes) {
|
|
1277
|
+
const wired = /* @__PURE__ */ new Map();
|
|
1278
|
+
const patterns = eventTypes.map((e) => ({
|
|
1279
|
+
eventType: e,
|
|
1280
|
+
re: new RegExp(
|
|
1281
|
+
`\\btrack\\s*\\(\\s*[\\s\\S]{0,160}?['"\`]${escapeRegExp(e)}['"\`]`
|
|
1282
|
+
)
|
|
1283
|
+
}));
|
|
1284
|
+
for (const file of files) {
|
|
1285
|
+
let content = "";
|
|
1286
|
+
try {
|
|
1287
|
+
content = await readFile2(join2(repoPath, file), "utf8");
|
|
1288
|
+
} catch {
|
|
1289
|
+
continue;
|
|
1290
|
+
}
|
|
1291
|
+
for (const { eventType, re } of patterns) {
|
|
1292
|
+
if (!wired.has(eventType) && re.test(content)) {
|
|
1293
|
+
wired.set(eventType, file);
|
|
1294
|
+
}
|
|
1295
|
+
}
|
|
1296
|
+
}
|
|
1297
|
+
return wired;
|
|
1298
|
+
}
|
|
1299
|
+
function escapeRegExp(s) {
|
|
1300
|
+
return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
1301
|
+
}
|
|
1302
|
+
function run(cwd, args) {
|
|
1303
|
+
return new Promise((resolve2) => {
|
|
1304
|
+
const child = spawn("git", args, { cwd });
|
|
1305
|
+
let stdout = "";
|
|
1306
|
+
let stderr = "";
|
|
1307
|
+
child.stdout.on("data", (d) => stdout += d.toString());
|
|
1308
|
+
child.stderr.on("data", (d) => stderr += d.toString());
|
|
1309
|
+
child.on("close", (code) => resolve2({ ok: code === 0, stdout, stderr }));
|
|
1310
|
+
child.on("error", () => resolve2({ ok: false, stdout, stderr }));
|
|
1311
|
+
});
|
|
1312
|
+
}
|
|
1313
|
+
|
|
1314
|
+
// src/core/opportunities.ts
|
|
1315
|
+
var OPPORTUNITIES_FILE = "whisperr-opportunities.json";
|
|
1316
|
+
function normalizeCode(value) {
|
|
1317
|
+
return value.trim().toLowerCase().replace(/[ \-./]/g, "_").replace(/[^a-z0-9_]/g, "").replace(/_+/g, "_").replace(/^_+|_+$/g, "");
|
|
1318
|
+
}
|
|
1319
|
+
async function collectOpportunities(repoPath, manifest, checkpoint) {
|
|
1320
|
+
const path = join3(repoPath, OPPORTUNITIES_FILE);
|
|
1321
|
+
if (await wasTrackedAtCheckpoint(repoPath, checkpoint, OPPORTUNITIES_FILE)) {
|
|
1322
|
+
return { events: [], interventions: [] };
|
|
1323
|
+
}
|
|
1324
|
+
let rawText;
|
|
1325
|
+
try {
|
|
1326
|
+
rawText = await readFile3(path, "utf8");
|
|
1327
|
+
} catch {
|
|
1328
|
+
return { events: [], interventions: [] };
|
|
1329
|
+
}
|
|
1330
|
+
await rm2(path, { force: true }).catch(() => {
|
|
1331
|
+
});
|
|
1332
|
+
let raw;
|
|
1333
|
+
try {
|
|
1334
|
+
raw = JSON.parse(rawText);
|
|
1335
|
+
} catch {
|
|
1336
|
+
return { events: [], interventions: [] };
|
|
1337
|
+
}
|
|
1338
|
+
return sanitizeOpportunities(raw, manifest);
|
|
1339
|
+
}
|
|
1340
|
+
function sanitizeOpportunities(raw, manifest) {
|
|
1341
|
+
const out = { events: [], interventions: [] };
|
|
1342
|
+
if (typeof raw !== "object" || raw === null) return out;
|
|
1343
|
+
const doc = raw;
|
|
1344
|
+
const knownEvents = new Set(manifest.events.map((e) => normalizeCode(e.eventType)));
|
|
1345
|
+
const knownInterventions = new Set(
|
|
1346
|
+
manifest.events.flatMap((e) => e.interventions ?? []).map((i) => normalizeCode(i.code))
|
|
1347
|
+
);
|
|
1348
|
+
const seenEvents = /* @__PURE__ */ new Set();
|
|
1349
|
+
for (const entry of asArray(doc.events)) {
|
|
1350
|
+
const ev = coerceEvent(entry);
|
|
1351
|
+
if (!ev) continue;
|
|
1352
|
+
if (knownEvents.has(ev.code) || seenEvents.has(ev.code)) continue;
|
|
1353
|
+
seenEvents.add(ev.code);
|
|
1354
|
+
out.events.push(ev);
|
|
1355
|
+
}
|
|
1356
|
+
const seenInterventions = /* @__PURE__ */ new Set();
|
|
1357
|
+
for (const entry of asArray(doc.interventions)) {
|
|
1358
|
+
const iv = coerceIntervention(entry);
|
|
1359
|
+
if (!iv) continue;
|
|
1360
|
+
if (knownInterventions.has(iv.code) || seenInterventions.has(iv.code)) continue;
|
|
1361
|
+
seenInterventions.add(iv.code);
|
|
1362
|
+
out.interventions.push(iv);
|
|
1363
|
+
}
|
|
1364
|
+
return out;
|
|
1365
|
+
}
|
|
1366
|
+
var SUBMIT_TIMEOUT_MS = 3e4;
|
|
1367
|
+
async function submitAdditions(config, session, target9, repoFingerprint2, opportunities) {
|
|
1368
|
+
const res = await fetch(`${config.apiBaseUrl}/wizard/universe/additions`, {
|
|
1369
|
+
method: "POST",
|
|
1370
|
+
signal: AbortSignal.timeout(SUBMIT_TIMEOUT_MS),
|
|
1371
|
+
headers: {
|
|
1372
|
+
"Content-Type": "application/json",
|
|
1373
|
+
Authorization: `Bearer ${session.token}`
|
|
1374
|
+
},
|
|
1375
|
+
body: JSON.stringify({
|
|
1376
|
+
target: target9,
|
|
1377
|
+
repoFingerprint: repoFingerprint2,
|
|
1378
|
+
events: opportunities.events,
|
|
1379
|
+
interventions: opportunities.interventions
|
|
1380
|
+
})
|
|
1381
|
+
});
|
|
1382
|
+
if (!res.ok) {
|
|
1383
|
+
const body = await res.text().catch(() => "");
|
|
1384
|
+
throw new Error(
|
|
1385
|
+
`Submitting universe additions failed (${res.status})${body ? `: ${body.slice(0, 300)}` : ""}`
|
|
1386
|
+
);
|
|
1387
|
+
}
|
|
1388
|
+
return await res.json();
|
|
1389
|
+
}
|
|
1390
|
+
function asArray(value) {
|
|
1391
|
+
return Array.isArray(value) ? value : [];
|
|
1392
|
+
}
|
|
1393
|
+
function asString(value) {
|
|
1394
|
+
if (typeof value !== "string") return void 0;
|
|
1395
|
+
const cleaned = value.replace(/\x1b\[[0-9;?]*[ -/]*[@-~]/g, "").replace(/\x1b\][^\x07\x1b]*(?:\x07|\x1b\\)?/g, "").replace(/[\x00-\x1f\x7f]/g, " ").replace(/ {2,}/g, " ").trim();
|
|
1396
|
+
return cleaned ? cleaned : void 0;
|
|
1397
|
+
}
|
|
1398
|
+
function asConfidence(value) {
|
|
1399
|
+
if (typeof value !== "number" || Number.isNaN(value)) return void 0;
|
|
1400
|
+
return Math.min(1, Math.max(0, value));
|
|
1401
|
+
}
|
|
1402
|
+
function asWeight(value) {
|
|
1403
|
+
if (typeof value !== "number" || Number.isNaN(value)) return void 0;
|
|
1404
|
+
if (value < 0 || value > 1) return void 0;
|
|
1405
|
+
return value;
|
|
1406
|
+
}
|
|
1407
|
+
function coerceEvent(entry) {
|
|
1408
|
+
if (typeof entry !== "object" || entry === null) return null;
|
|
1409
|
+
const e = entry;
|
|
1410
|
+
const code = normalizeCode(asString(e.code) ?? "");
|
|
1411
|
+
if (!code) return null;
|
|
1412
|
+
const properties = [];
|
|
1413
|
+
for (const prop of asArray(e.properties)) {
|
|
1414
|
+
if (typeof prop === "string") {
|
|
1415
|
+
if (prop.trim()) properties.push({ name: prop.trim() });
|
|
1416
|
+
continue;
|
|
1417
|
+
}
|
|
1418
|
+
if (typeof prop !== "object" || prop === null) continue;
|
|
1419
|
+
const pr = prop;
|
|
1420
|
+
const name = asString(pr.name);
|
|
1421
|
+
if (!name) continue;
|
|
1422
|
+
const desc = asString(pr.description);
|
|
1423
|
+
properties.push({
|
|
1424
|
+
name,
|
|
1425
|
+
...desc ? { description: desc } : {},
|
|
1426
|
+
...pr.required === true ? { required: true } : {}
|
|
1427
|
+
});
|
|
1428
|
+
}
|
|
1429
|
+
const links = [];
|
|
1430
|
+
for (const link of asArray(e.links)) {
|
|
1431
|
+
if (typeof link !== "object" || link === null) continue;
|
|
1432
|
+
const l = link;
|
|
1433
|
+
const interventionCode = normalizeCode(asString(l.interventionCode) ?? "");
|
|
1434
|
+
if (!interventionCode) continue;
|
|
1435
|
+
const weight = asWeight(l.weight);
|
|
1436
|
+
links.push({ interventionCode, ...weight !== void 0 ? { weight } : {} });
|
|
1437
|
+
}
|
|
1438
|
+
const side = normalizeSide(asString(e.side));
|
|
1439
|
+
return {
|
|
1440
|
+
code,
|
|
1441
|
+
label: asString(e.label),
|
|
1442
|
+
description: asString(e.description),
|
|
1443
|
+
...side ? { side } : {},
|
|
1444
|
+
rationale: asString(e.rationale),
|
|
1445
|
+
confidence: asConfidence(e.confidence),
|
|
1446
|
+
...properties.length ? { properties } : {},
|
|
1447
|
+
...links.length ? { links } : {}
|
|
1448
|
+
};
|
|
1449
|
+
}
|
|
1450
|
+
function coerceIntervention(entry) {
|
|
1451
|
+
if (typeof entry !== "object" || entry === null) return null;
|
|
1452
|
+
const i = entry;
|
|
1453
|
+
const code = normalizeCode(asString(i.code) ?? "");
|
|
1454
|
+
if (!code) return null;
|
|
1455
|
+
const links = [];
|
|
1456
|
+
for (const link of asArray(i.links)) {
|
|
1457
|
+
if (typeof link !== "object" || link === null) continue;
|
|
1458
|
+
const l = link;
|
|
1459
|
+
const eventCode = normalizeCode(asString(l.eventCode) ?? "");
|
|
1460
|
+
if (!eventCode) continue;
|
|
1461
|
+
const weight = asWeight(l.weight);
|
|
1462
|
+
links.push({ eventCode, ...weight !== void 0 ? { weight } : {} });
|
|
1463
|
+
}
|
|
1464
|
+
return {
|
|
1465
|
+
code,
|
|
1466
|
+
label: asString(i.label),
|
|
1467
|
+
description: asString(i.description),
|
|
1468
|
+
rationale: asString(i.rationale),
|
|
1469
|
+
confidence: asConfidence(i.confidence),
|
|
1470
|
+
...links.length ? { links } : {}
|
|
1471
|
+
};
|
|
1472
|
+
}
|
|
1473
|
+
function normalizeSide(value) {
|
|
1474
|
+
switch (value?.toLowerCase()) {
|
|
1475
|
+
case "frontend":
|
|
1476
|
+
return "frontend";
|
|
1477
|
+
case "backend":
|
|
1478
|
+
return "backend";
|
|
1479
|
+
case "either":
|
|
1480
|
+
return "either";
|
|
1481
|
+
default:
|
|
1482
|
+
return void 0;
|
|
1483
|
+
}
|
|
1484
|
+
}
|
|
1485
|
+
|
|
992
1486
|
// src/core/playbooks/shared-prompt.ts
|
|
993
1487
|
var BASE_WIZARD_PROMPT = `
|
|
994
1488
|
You are the Whisperr Wizard \u2014 an expert integration agent running INSIDE a
|
|
@@ -1165,6 +1659,46 @@ function renderEventsBrief(m) {
|
|
|
1165
1659
|
}
|
|
1166
1660
|
return lines.join("\n");
|
|
1167
1661
|
}
|
|
1662
|
+
function renderOpportunitiesBrief(m, opportunitiesFile) {
|
|
1663
|
+
const eventCodes = m.events.map((e) => e.eventType);
|
|
1664
|
+
const interventionCodes = [
|
|
1665
|
+
...new Set(m.events.flatMap((e) => e.interventions ?? []).map((i) => i.code))
|
|
1666
|
+
];
|
|
1667
|
+
return [
|
|
1668
|
+
"UNIVERSE OPPORTUNITIES (do this LAST, after your corrections):",
|
|
1669
|
+
"While auditing you read this app's real lifecycle. If you found churn-",
|
|
1670
|
+
"relevant moments the plan does NOT cover \u2014 e.g. a recurring-payment or",
|
|
1671
|
+
"cancellation path with no event, a support/refund flow worth a retention",
|
|
1672
|
+
"play \u2014 record them as PROPOSALS. Do NOT add track() calls for them.",
|
|
1673
|
+
`Write a single JSON file at the repo root named ${opportunitiesFile}:`,
|
|
1674
|
+
"",
|
|
1675
|
+
"{",
|
|
1676
|
+
' "events": [{',
|
|
1677
|
+
' "code": "snake_case_event", "label": "...", "description": "...",',
|
|
1678
|
+
' "side": "frontend|backend|either", "rationale": "what in the code shows this",',
|
|
1679
|
+
' "confidence": 0.0-1.0,',
|
|
1680
|
+
' "properties": [{"name": "...", "description": "...", "required": false}],',
|
|
1681
|
+
' "links": [{"interventionCode": "existing_or_proposed", "weight": 0.0-1.0}]',
|
|
1682
|
+
" }],",
|
|
1683
|
+
' "interventions": [{',
|
|
1684
|
+
' "code": "snake_case_strategy", "label": "...", "description": "...",',
|
|
1685
|
+
' "rationale": "...", "confidence": 0.0-1.0,',
|
|
1686
|
+
' "links": [{"eventCode": "existing_or_proposed", "weight": 0.0-1.0}]',
|
|
1687
|
+
" }]",
|
|
1688
|
+
"}",
|
|
1689
|
+
"",
|
|
1690
|
+
"Rules:",
|
|
1691
|
+
`- The plan already covers these events: ${eventCodes.join(", ") || "(none)"}.`,
|
|
1692
|
+
` And these interventions: ${interventionCodes.join(", ") || "(none)"}.`,
|
|
1693
|
+
" Propose ONLY what is genuinely missing \u2014 never re-propose or rename these.",
|
|
1694
|
+
"- Every proposal needs concrete code evidence in its rationale (file/flow),",
|
|
1695
|
+
" not speculation. 0-3 events and 0-2 interventions is the normal range;",
|
|
1696
|
+
" an empty file or no file at all is a perfectly good outcome.",
|
|
1697
|
+
"- Link each proposed event to the intervention(s) it should feed (existing",
|
|
1698
|
+
" codes or ones you propose in the same file).",
|
|
1699
|
+
"- This file is metadata for the wizard, not app code \u2014 write it and move on."
|
|
1700
|
+
].join("\n");
|
|
1701
|
+
}
|
|
1168
1702
|
function coverageNote(coverage) {
|
|
1169
1703
|
if (!coverage?.length) return "";
|
|
1170
1704
|
const here = coverage.find((c) => c.sameSurface && c.status === "wired");
|
|
@@ -1289,8 +1823,12 @@ ${events.summary}`);
|
|
|
1289
1823
|
"4. Coverage \u2014 every gateway/callback path that should emit an event does;",
|
|
1290
1824
|
" no recurring or secondary path left as a dead zone.",
|
|
1291
1825
|
"Change ONLY what is genuinely wrong or incomplete \u2014 do not churn correct",
|
|
1292
|
-
"calls or re-explore the whole repo. Be surgical.
|
|
1293
|
-
"
|
|
1826
|
+
"calls or re-explore the whole repo. Be surgical.",
|
|
1827
|
+
"",
|
|
1828
|
+
renderOpportunitiesBrief(manifest, OPPORTUNITIES_FILE),
|
|
1829
|
+
"",
|
|
1830
|
+
"End with a one-line, plain-text note of what you corrected (or 'No",
|
|
1831
|
+
"corrections needed.'), plus one line on what you proposed, if anything."
|
|
1294
1832
|
].join("\n");
|
|
1295
1833
|
const review = await runPass({
|
|
1296
1834
|
prompt: reviewPrompt,
|
|
@@ -1314,6 +1852,53 @@ ${review.summary}`);
|
|
|
1314
1852
|
eventsComplete
|
|
1315
1853
|
};
|
|
1316
1854
|
}
|
|
1855
|
+
async function runAdditionsInstrumentationPass(opts) {
|
|
1856
|
+
const { repoPath, config, session, playbook, acceptedEvents, budgetUsd, progress } = opts;
|
|
1857
|
+
if (!acceptedEvents.length || budgetUsd <= 0) {
|
|
1858
|
+
return { summary: "", costUsd: 0, ran: false };
|
|
1859
|
+
}
|
|
1860
|
+
applyModelAuthEnv(config, session);
|
|
1861
|
+
const systemPrompt9 = [BASE_WIZARD_PROMPT, playbook.systemPrompt].join("\n\n");
|
|
1862
|
+
const eventLines = [];
|
|
1863
|
+
for (const e of acceptedEvents) {
|
|
1864
|
+
eventLines.push("");
|
|
1865
|
+
eventLines.push(`\u25A0 ${e.code}${e.label ? ` (${e.label})` : ""}`);
|
|
1866
|
+
if (e.description) eventLines.push(` ${e.description}`);
|
|
1867
|
+
if (e.rationale) eventLines.push(` where: ${e.rationale}`);
|
|
1868
|
+
if (e.properties?.length) {
|
|
1869
|
+
eventLines.push(" properties to capture if available:");
|
|
1870
|
+
for (const prop of e.properties) {
|
|
1871
|
+
eventLines.push(` \xB7 ${prop.name}${prop.required ? " (required)" : ""}${prop.description ? ` \u2014 ${prop.description}` : ""}`);
|
|
1872
|
+
}
|
|
1873
|
+
}
|
|
1874
|
+
}
|
|
1875
|
+
progress?.onPhase?.("Instrumenting the newly added events");
|
|
1876
|
+
const prompt = [
|
|
1877
|
+
"You proposed these events as universe opportunities while reviewing this",
|
|
1878
|
+
"repo, and the user just APPROVED adding them to the plan. Instrument them",
|
|
1879
|
+
"now with track(), exactly like the plan's other events: place each at the",
|
|
1880
|
+
"real call site (your own rationale tells you where you saw it), attach the",
|
|
1881
|
+
"properties available in scope, and skip anything that turns out not to have",
|
|
1882
|
+
"a clear trigger after all. The SDK is already installed and initialized.",
|
|
1883
|
+
`Project root: ${repoPath}`,
|
|
1884
|
+
"",
|
|
1885
|
+
"----- APPROVED NEW EVENTS -----",
|
|
1886
|
+
...eventLines,
|
|
1887
|
+
"",
|
|
1888
|
+
"----- END -----"
|
|
1889
|
+
].join("\n");
|
|
1890
|
+
const pass = await runPass({
|
|
1891
|
+
prompt,
|
|
1892
|
+
systemPrompt: systemPrompt9,
|
|
1893
|
+
repoPath,
|
|
1894
|
+
model: config.model,
|
|
1895
|
+
effort: config.effort,
|
|
1896
|
+
maxTurns: 25,
|
|
1897
|
+
budgetUsd,
|
|
1898
|
+
progress
|
|
1899
|
+
});
|
|
1900
|
+
return { summary: pass.summary, costUsd: pass.costUsd, ran: true };
|
|
1901
|
+
}
|
|
1317
1902
|
function isLimitStop(err) {
|
|
1318
1903
|
const msg = err?.message ?? "";
|
|
1319
1904
|
return /max(imum)?[\s_-]*(turn|budget)|budget.*exceeded|number of turns/i.test(msg);
|
|
@@ -1423,102 +2008,6 @@ function short(s, max = 60) {
|
|
|
1423
2008
|
return s.length > max ? `${s.slice(0, max - 1)}\u2026` : s;
|
|
1424
2009
|
}
|
|
1425
2010
|
|
|
1426
|
-
// src/core/git.ts
|
|
1427
|
-
import { spawn } from "child_process";
|
|
1428
|
-
import { createHash } from "crypto";
|
|
1429
|
-
import { readFile as readFile2 } from "fs/promises";
|
|
1430
|
-
import { basename, join as join2 } from "path";
|
|
1431
|
-
async function takeCheckpoint(repoPath) {
|
|
1432
|
-
const isRepo = (await run(repoPath, ["rev-parse", "--is-inside-work-tree"])).ok;
|
|
1433
|
-
if (!isRepo) return { isRepo: false };
|
|
1434
|
-
const branch = (await run(repoPath, ["rev-parse", "--abbrev-ref", "HEAD"])).stdout.trim();
|
|
1435
|
-
const head = await run(repoPath, ["rev-parse", "HEAD"]);
|
|
1436
|
-
return {
|
|
1437
|
-
isRepo: true,
|
|
1438
|
-
baseRef: head.ok ? head.stdout.trim() : void 0,
|
|
1439
|
-
branch: branch || void 0
|
|
1440
|
-
};
|
|
1441
|
-
}
|
|
1442
|
-
async function changedFiles(repoPath, checkpoint) {
|
|
1443
|
-
if (!checkpoint.isRepo) return [];
|
|
1444
|
-
const tracked = await run(repoPath, ["diff", "--name-only"]);
|
|
1445
|
-
const untracked = await run(repoPath, [
|
|
1446
|
-
"ls-files",
|
|
1447
|
-
"--others",
|
|
1448
|
-
"--exclude-standard"
|
|
1449
|
-
]);
|
|
1450
|
-
const set = /* @__PURE__ */ new Set();
|
|
1451
|
-
for (const f of `${tracked.stdout}
|
|
1452
|
-
${untracked.stdout}`.split("\n")) {
|
|
1453
|
-
if (f.trim()) set.add(f.trim());
|
|
1454
|
-
}
|
|
1455
|
-
return [...set];
|
|
1456
|
-
}
|
|
1457
|
-
function revertHint(checkpoint) {
|
|
1458
|
-
if (!checkpoint.isRepo || !checkpoint.baseRef) return void 0;
|
|
1459
|
-
return `git restore . && git clean -fd (back to ${checkpoint.baseRef.slice(0, 7)})`;
|
|
1460
|
-
}
|
|
1461
|
-
async function isWorkingTreeClean(repoPath) {
|
|
1462
|
-
const status = await run(repoPath, ["status", "--porcelain"]);
|
|
1463
|
-
return status.ok && status.stdout.trim() === "";
|
|
1464
|
-
}
|
|
1465
|
-
async function revertToCheckpoint(repoPath, checkpoint) {
|
|
1466
|
-
if (!checkpoint.isRepo) return false;
|
|
1467
|
-
if (checkpoint.baseRef) {
|
|
1468
|
-
const reset = await run(repoPath, ["reset", "--hard", checkpoint.baseRef]);
|
|
1469
|
-
const clean2 = await run(repoPath, ["clean", "-fd"]);
|
|
1470
|
-
return reset.ok && clean2.ok;
|
|
1471
|
-
}
|
|
1472
|
-
const restore = await run(repoPath, ["restore", "."]);
|
|
1473
|
-
const clean = await run(repoPath, ["clean", "-fd"]);
|
|
1474
|
-
return restore.ok && clean.ok;
|
|
1475
|
-
}
|
|
1476
|
-
async function repoFingerprint(repoPath) {
|
|
1477
|
-
const remote = await run(repoPath, ["remote", "get-url", "origin"]);
|
|
1478
|
-
const seed = remote.ok && remote.stdout.trim() ? normalizeRemote(remote.stdout.trim()) : `dir:${basename(repoPath)}`;
|
|
1479
|
-
return createHash("sha256").update(seed).digest("hex").slice(0, 16);
|
|
1480
|
-
}
|
|
1481
|
-
function normalizeRemote(url) {
|
|
1482
|
-
return url.replace(/^git@([^:]+):/, "$1/").replace(/^https?:\/\//, "").replace(/\.git$/, "").replace(/\/+$/, "").toLowerCase();
|
|
1483
|
-
}
|
|
1484
|
-
async function scanWiredEvents(repoPath, files, eventTypes) {
|
|
1485
|
-
const wired = /* @__PURE__ */ new Map();
|
|
1486
|
-
const patterns = eventTypes.map((e) => ({
|
|
1487
|
-
eventType: e,
|
|
1488
|
-
re: new RegExp(
|
|
1489
|
-
`\\btrack\\s*\\(\\s*[\\s\\S]{0,160}?['"\`]${escapeRegExp(e)}['"\`]`
|
|
1490
|
-
)
|
|
1491
|
-
}));
|
|
1492
|
-
for (const file of files) {
|
|
1493
|
-
let content = "";
|
|
1494
|
-
try {
|
|
1495
|
-
content = await readFile2(join2(repoPath, file), "utf8");
|
|
1496
|
-
} catch {
|
|
1497
|
-
continue;
|
|
1498
|
-
}
|
|
1499
|
-
for (const { eventType, re } of patterns) {
|
|
1500
|
-
if (!wired.has(eventType) && re.test(content)) {
|
|
1501
|
-
wired.set(eventType, file);
|
|
1502
|
-
}
|
|
1503
|
-
}
|
|
1504
|
-
}
|
|
1505
|
-
return wired;
|
|
1506
|
-
}
|
|
1507
|
-
function escapeRegExp(s) {
|
|
1508
|
-
return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
1509
|
-
}
|
|
1510
|
-
function run(cwd, args) {
|
|
1511
|
-
return new Promise((resolve2) => {
|
|
1512
|
-
const child = spawn("git", args, { cwd });
|
|
1513
|
-
let stdout = "";
|
|
1514
|
-
let stderr = "";
|
|
1515
|
-
child.stdout.on("data", (d) => stdout += d.toString());
|
|
1516
|
-
child.stderr.on("data", (d) => stderr += d.toString());
|
|
1517
|
-
child.on("close", (code) => resolve2({ ok: code === 0, stdout, stderr }));
|
|
1518
|
-
child.on("error", () => resolve2({ ok: false, stdout, stderr }));
|
|
1519
|
-
});
|
|
1520
|
-
}
|
|
1521
|
-
|
|
1522
2011
|
// src/core/verify.ts
|
|
1523
2012
|
async function pollFirstEvent(config, session, opts = {}) {
|
|
1524
2013
|
if (config.offline) return { received: false };
|
|
@@ -1546,6 +2035,10 @@ async function pollFirstEvent(config, session, opts = {}) {
|
|
|
1546
2035
|
|
|
1547
2036
|
// src/core/postflight.ts
|
|
1548
2037
|
import { spawn as spawn2 } from "child_process";
|
|
2038
|
+
function verdictToVerified(verdict) {
|
|
2039
|
+
if (verdict.toolMissing || verdict.timedOut) return null;
|
|
2040
|
+
return verdict.ok;
|
|
2041
|
+
}
|
|
1549
2042
|
var MAX_OUTPUT = 4e3;
|
|
1550
2043
|
var DEFAULT_TIMEOUT_MS = 18e4;
|
|
1551
2044
|
function runVerifyCommand(repoPath, command, opts = {}) {
|
|
@@ -1737,6 +2230,7 @@ Flutter is live today; ${theme.bright(
|
|
|
1737
2230
|
}
|
|
1738
2231
|
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");
|
|
1739
2232
|
spin.stop(stopLabel);
|
|
2233
|
+
const opportunities = await collectOpportunities(repoPath, manifest, checkpoint);
|
|
1740
2234
|
const files = await changedFiles(repoPath, checkpoint);
|
|
1741
2235
|
if (files.length) {
|
|
1742
2236
|
p.note(files.map((f) => theme.muted("\u2022 ") + f).join("\n"), "Files changed");
|
|
@@ -1761,6 +2255,7 @@ Flutter is live today; ${theme.bright(
|
|
|
1761
2255
|
const vspin = p.spinner();
|
|
1762
2256
|
vspin.start(`Verifying the integration \u2014 ${theme.muted(cmd)}`);
|
|
1763
2257
|
const verdict = await runVerifyCommand(repoPath, cmd);
|
|
2258
|
+
verified = verdictToVerified(verdict);
|
|
1764
2259
|
if (verdict.toolMissing) {
|
|
1765
2260
|
vspin.stop(
|
|
1766
2261
|
theme.warn("Couldn't verify automatically") + theme.muted(` \u2014 \`${cmd}\` isn't available here. Run it yourself before committing.`)
|
|
@@ -1770,10 +2265,8 @@ Flutter is live today; ${theme.bright(
|
|
|
1770
2265
|
theme.warn(`Verification timed out \u2014 \`${cmd}\``) + theme.muted(" \u2014 run it yourself before committing.")
|
|
1771
2266
|
);
|
|
1772
2267
|
} else if (verdict.ok) {
|
|
1773
|
-
verified = true;
|
|
1774
2268
|
vspin.stop(theme.success("Verified \u2713") + theme.muted(` (${cmd})`));
|
|
1775
2269
|
} else {
|
|
1776
|
-
verified = false;
|
|
1777
2270
|
vspin.stop(theme.alert(`Verification failed \u2014 ${cmd}`));
|
|
1778
2271
|
if (verdict.output) {
|
|
1779
2272
|
p.note(verdict.output, theme.warn("Verifier output"));
|
|
@@ -1789,15 +2282,69 @@ Flutter is live today; ${theme.bright(
|
|
|
1789
2282
|
}
|
|
1790
2283
|
}
|
|
1791
2284
|
}
|
|
1792
|
-
const
|
|
2285
|
+
const { acceptedEvents, instrumentationSnapshot } = await offerOpportunities({
|
|
1793
2286
|
repoPath,
|
|
1794
|
-
|
|
1795
|
-
|
|
1796
|
-
|
|
1797
|
-
|
|
1798
|
-
|
|
1799
|
-
|
|
1800
|
-
|
|
2287
|
+
config,
|
|
2288
|
+
session,
|
|
2289
|
+
playbook: chosen.playbook,
|
|
2290
|
+
manifest,
|
|
2291
|
+
opportunities,
|
|
2292
|
+
fingerprint,
|
|
2293
|
+
checkpoint,
|
|
2294
|
+
remainingBudgetUsd: config.budgetUsd - outcome.costUsd
|
|
2295
|
+
});
|
|
2296
|
+
if (instrumentationSnapshot && chosen.playbook.verifyCommand) {
|
|
2297
|
+
const cmd = chosen.playbook.verifyCommand;
|
|
2298
|
+
const preInstrumentationVerified = verified;
|
|
2299
|
+
const vspin = p.spinner();
|
|
2300
|
+
vspin.start(`Re-verifying after instrumenting the new events \u2014 ${theme.muted(cmd)}`);
|
|
2301
|
+
const verdict = await runVerifyCommand(repoPath, cmd);
|
|
2302
|
+
verified = verdictToVerified(verdict);
|
|
2303
|
+
if (verdict.toolMissing) {
|
|
2304
|
+
vspin.stop(
|
|
2305
|
+
theme.warn("Couldn't re-verify automatically") + theme.muted(` \u2014 \`${cmd}\` isn't available here. Run it yourself before committing.`)
|
|
2306
|
+
);
|
|
2307
|
+
} else if (verdict.timedOut) {
|
|
2308
|
+
vspin.stop(
|
|
2309
|
+
theme.warn(`Re-verification timed out \u2014 \`${cmd}\``) + theme.muted(" \u2014 run it yourself before committing.")
|
|
2310
|
+
);
|
|
2311
|
+
} else if (verdict.ok) {
|
|
2312
|
+
vspin.stop(theme.success("Verified \u2713") + theme.muted(` (${cmd})`));
|
|
2313
|
+
} else {
|
|
2314
|
+
vspin.stop(theme.alert(`Verification failed after instrumenting the new events \u2014 ${cmd}`));
|
|
2315
|
+
if (verdict.output) {
|
|
2316
|
+
p.note(verdict.output, theme.warn("Verifier output"));
|
|
2317
|
+
}
|
|
2318
|
+
const doRevert = await p.confirm({
|
|
2319
|
+
message: "Instrumenting the new events didn't pass the build/lint check. Revert just those edits? (The events stay in your universe \u2014 wire them on a future run.)",
|
|
2320
|
+
initialValue: true
|
|
2321
|
+
});
|
|
2322
|
+
if (!p.isCancel(doRevert) && doRevert) {
|
|
2323
|
+
if (await restoreToSnapshot(repoPath, checkpoint, instrumentationSnapshot)) {
|
|
2324
|
+
verified = preInstrumentationVerified;
|
|
2325
|
+
p.log.success(theme.success("Reverted the instrumentation edits."));
|
|
2326
|
+
} else {
|
|
2327
|
+
p.log.warn(
|
|
2328
|
+
theme.warn("Couldn't revert automatically \u2014 undo manually: ") + (revertHint(checkpoint) ?? "git reset --hard")
|
|
2329
|
+
);
|
|
2330
|
+
}
|
|
2331
|
+
} else {
|
|
2332
|
+
p.log.info(
|
|
2333
|
+
theme.muted("Left in your working tree \u2014 fix the verifier errors before committing.")
|
|
2334
|
+
);
|
|
2335
|
+
}
|
|
2336
|
+
}
|
|
2337
|
+
}
|
|
2338
|
+
const filesForScan = acceptedEvents.length ? await changedFiles(repoPath, checkpoint) : files;
|
|
2339
|
+
const eventTypesToScan = [
|
|
2340
|
+
...manifest.events.map((e) => e.eventType),
|
|
2341
|
+
...acceptedEvents.map((e) => e.code)
|
|
2342
|
+
];
|
|
2343
|
+
const wiredMap = await scanWiredEvents(repoPath, filesForScan, eventTypesToScan);
|
|
2344
|
+
const reportEvents = eventTypesToScan.map((eventType) => ({
|
|
2345
|
+
event_type: eventType,
|
|
2346
|
+
status: wiredMap.has(eventType) ? "wired" : "skipped",
|
|
2347
|
+
file: wiredMap.get(eventType)
|
|
1801
2348
|
}));
|
|
1802
2349
|
await postRunReport(config, session, {
|
|
1803
2350
|
target: chosen.playbook.target.id,
|
|
@@ -1811,7 +2358,7 @@ Flutter is live today; ${theme.bright(
|
|
|
1811
2358
|
});
|
|
1812
2359
|
p.log.info(
|
|
1813
2360
|
theme.muted(
|
|
1814
|
-
`${wiredMap.size}/${
|
|
2361
|
+
`${wiredMap.size}/${eventTypesToScan.length} events wired \xB7 ${files.length} file${files.length === 1 ? "" : "s"} changed \xB7 ` + (verified === true ? "verified \xB7 " : verified === false ? "unverified \xB7 " : "") + `${Math.round(outcome.durationMs / 1e3)}s`
|
|
1815
2362
|
)
|
|
1816
2363
|
);
|
|
1817
2364
|
if (!config.offline) {
|
|
@@ -1846,6 +2393,153 @@ Flutter is live today; ${theme.bright(
|
|
|
1846
2393
|
p.outro(theme.signal("\u2301 ") + theme.bright("Whisperr is wired in."));
|
|
1847
2394
|
return 0;
|
|
1848
2395
|
}
|
|
2396
|
+
var NO_ADDITIONS = { acceptedEvents: [], instrumentationSnapshot: null };
|
|
2397
|
+
async function offerOpportunities(opts) {
|
|
2398
|
+
const { opportunities, manifest, config, session } = opts;
|
|
2399
|
+
const total = opportunities.events.length + opportunities.interventions.length;
|
|
2400
|
+
if (total === 0) return NO_ADDITIONS;
|
|
2401
|
+
const describe = (kind, code, why) => `${theme.muted(`${kind}: `)}${theme.bright(code)}${why ? theme.muted(` \u2014 ${why}`) : ""}`;
|
|
2402
|
+
p.note(
|
|
2403
|
+
[
|
|
2404
|
+
...opportunities.events.map((e) => describe("event", e.code, e.rationale ?? e.description)),
|
|
2405
|
+
...opportunities.interventions.map(
|
|
2406
|
+
(i) => describe("intervention", i.code, i.rationale ?? i.description)
|
|
2407
|
+
)
|
|
2408
|
+
].join("\n"),
|
|
2409
|
+
theme.signal("Opportunities found beyond your onboarding plan")
|
|
2410
|
+
);
|
|
2411
|
+
if (config.offline) {
|
|
2412
|
+
p.log.info(theme.muted("Offline mode \u2014 proposals shown only, nothing submitted."));
|
|
2413
|
+
return NO_ADDITIONS;
|
|
2414
|
+
}
|
|
2415
|
+
if (!process.stdout.isTTY || !process.stdin.isTTY) {
|
|
2416
|
+
p.log.info(
|
|
2417
|
+
theme.muted("Non-interactive run \u2014 proposals were not submitted. Re-run interactively to add them.")
|
|
2418
|
+
);
|
|
2419
|
+
return NO_ADDITIONS;
|
|
2420
|
+
}
|
|
2421
|
+
const selection = await p.multiselect({
|
|
2422
|
+
message: "Add these to your Whisperr universe? (new interventions start paused)",
|
|
2423
|
+
options: [
|
|
2424
|
+
...opportunities.events.map((e) => ({
|
|
2425
|
+
value: `e:${e.code}`,
|
|
2426
|
+
label: `event \xB7 ${e.code}`,
|
|
2427
|
+
hint: e.description ?? e.rationale
|
|
2428
|
+
})),
|
|
2429
|
+
...opportunities.interventions.map((i) => ({
|
|
2430
|
+
value: `i:${i.code}`,
|
|
2431
|
+
label: `intervention \xB7 ${i.code}`,
|
|
2432
|
+
hint: i.description ?? i.rationale
|
|
2433
|
+
}))
|
|
2434
|
+
],
|
|
2435
|
+
initialValues: [
|
|
2436
|
+
...opportunities.events.map((e) => `e:${e.code}`),
|
|
2437
|
+
...opportunities.interventions.map((i) => `i:${i.code}`)
|
|
2438
|
+
],
|
|
2439
|
+
required: false
|
|
2440
|
+
});
|
|
2441
|
+
if (p.isCancel(selection) || selection.length === 0) {
|
|
2442
|
+
p.log.info(theme.muted("Skipped \u2014 your universe is unchanged."));
|
|
2443
|
+
return NO_ADDITIONS;
|
|
2444
|
+
}
|
|
2445
|
+
const chosenSet = new Set(selection);
|
|
2446
|
+
const knownInterventionCodes = new Set(
|
|
2447
|
+
manifest.events.flatMap((e) => e.interventions ?? []).map((i) => i.code)
|
|
2448
|
+
);
|
|
2449
|
+
const knownEventCodes = new Set(manifest.events.map((e) => e.eventType));
|
|
2450
|
+
const pickedEvents = opportunities.events.filter((e) => chosenSet.has(`e:${e.code}`));
|
|
2451
|
+
const pickedInterventions = opportunities.interventions.filter(
|
|
2452
|
+
(i) => chosenSet.has(`i:${i.code}`)
|
|
2453
|
+
);
|
|
2454
|
+
const pickedInterventionCodes = new Set(pickedInterventions.map((i) => i.code));
|
|
2455
|
+
const pickedEventCodes = new Set(pickedEvents.map((e) => e.code));
|
|
2456
|
+
const submitted = {
|
|
2457
|
+
events: pickedEvents.map((e) => ({
|
|
2458
|
+
...e,
|
|
2459
|
+
links: e.links?.filter(
|
|
2460
|
+
(l) => knownInterventionCodes.has(l.interventionCode) || pickedInterventionCodes.has(l.interventionCode)
|
|
2461
|
+
)
|
|
2462
|
+
})),
|
|
2463
|
+
interventions: pickedInterventions.map((i) => ({
|
|
2464
|
+
...i,
|
|
2465
|
+
links: i.links?.filter(
|
|
2466
|
+
(l) => knownEventCodes.has(l.eventCode) || pickedEventCodes.has(l.eventCode)
|
|
2467
|
+
)
|
|
2468
|
+
}))
|
|
2469
|
+
};
|
|
2470
|
+
let result;
|
|
2471
|
+
try {
|
|
2472
|
+
result = await withSpinner(
|
|
2473
|
+
"Adding to your universe",
|
|
2474
|
+
() => submitAdditions(config, session, opts.playbook.target.id, opts.fingerprint, submitted)
|
|
2475
|
+
);
|
|
2476
|
+
} catch (err) {
|
|
2477
|
+
p.log.warn(
|
|
2478
|
+
theme.warn("Couldn't add the proposals") + theme.muted(` \u2014 ${err.message}. Your universe is unchanged.`)
|
|
2479
|
+
);
|
|
2480
|
+
return NO_ADDITIONS;
|
|
2481
|
+
}
|
|
2482
|
+
const lines = result.outcomes.map((o) => {
|
|
2483
|
+
const mark = o.status === "applied" ? theme.success("\u2713") : o.status === "duplicate" ? theme.muted("=") : theme.warn("\u2717");
|
|
2484
|
+
const detail = o.status === "duplicate" ? theme.muted(` (already in your universe${o.duplicateOf && o.duplicateOf !== o.code ? ` as ${o.duplicateOf}` : ""})`) : o.status === "invalid" && o.reason ? theme.muted(` (${o.reason})`) : "";
|
|
2485
|
+
return `${mark} ${o.kind} ${theme.bright(o.code)}${detail}`;
|
|
2486
|
+
});
|
|
2487
|
+
lines.push(
|
|
2488
|
+
theme.muted(
|
|
2489
|
+
`${result.applied} added \xB7 ${result.duplicates} already present \xB7 ${result.invalid} rejected`
|
|
2490
|
+
)
|
|
2491
|
+
);
|
|
2492
|
+
if (submitted.interventions.length) {
|
|
2493
|
+
lines.push(theme.muted("New interventions start paused \u2014 activate them in your dashboard."));
|
|
2494
|
+
}
|
|
2495
|
+
if (result.policyRegen?.status === "pending") {
|
|
2496
|
+
lines.push(
|
|
2497
|
+
theme.muted("Runtime policy update queued \u2014 the additions go live once it completes.")
|
|
2498
|
+
);
|
|
2499
|
+
}
|
|
2500
|
+
p.note(lines.join("\n"), theme.signal("Universe updated"));
|
|
2501
|
+
if (result.policyRegen?.status === "failed") {
|
|
2502
|
+
p.log.warn(
|
|
2503
|
+
theme.warn("Live runtime NOT updated") + theme.muted(
|
|
2504
|
+
" \u2014 the additions were recorded, but the runtime policy wasn't regenerated" + (result.policyRegen.reason ? `: ${result.policyRegen.reason}` : "") + ". They won't drive live decisions until the policy regenerates."
|
|
2505
|
+
)
|
|
2506
|
+
);
|
|
2507
|
+
}
|
|
2508
|
+
const appliedEventCodes = new Set(
|
|
2509
|
+
result.outcomes.filter((o) => o.kind === "event" && o.status === "applied").map((o) => o.code)
|
|
2510
|
+
);
|
|
2511
|
+
const acceptedEvents = pickedEvents.filter((e) => appliedEventCodes.has(e.code));
|
|
2512
|
+
if (!acceptedEvents.length) return NO_ADDITIONS;
|
|
2513
|
+
const prePass = await snapshotChanges(opts.repoPath, opts.checkpoint);
|
|
2514
|
+
const spin = p.spinner();
|
|
2515
|
+
spin.start(theme.bright("Instrumenting the newly added events"));
|
|
2516
|
+
try {
|
|
2517
|
+
const pass = await runAdditionsInstrumentationPass({
|
|
2518
|
+
repoPath: opts.repoPath,
|
|
2519
|
+
config,
|
|
2520
|
+
session,
|
|
2521
|
+
playbook: opts.playbook,
|
|
2522
|
+
acceptedEvents,
|
|
2523
|
+
budgetUsd: opts.remainingBudgetUsd
|
|
2524
|
+
});
|
|
2525
|
+
if (pass.ran) {
|
|
2526
|
+
spin.stop(theme.success("New events instrumented"));
|
|
2527
|
+
if (pass.summary.trim()) p.log.message(pass.summary.trim());
|
|
2528
|
+
} else {
|
|
2529
|
+
spin.stop(
|
|
2530
|
+
theme.warn("Skipped instrumenting the new events") + theme.muted(
|
|
2531
|
+
" \u2014 the spend limit was reached. They're in your universe; wire them on a future run."
|
|
2532
|
+
)
|
|
2533
|
+
);
|
|
2534
|
+
}
|
|
2535
|
+
} catch (err) {
|
|
2536
|
+
spin.stop(
|
|
2537
|
+
theme.warn("Couldn't instrument the new events") + theme.muted(` \u2014 ${err.message}. They're in your universe; wire them on a future run.`)
|
|
2538
|
+
);
|
|
2539
|
+
}
|
|
2540
|
+
const edited = !snapshotsEqual(prePass, await snapshotChanges(opts.repoPath, opts.checkpoint));
|
|
2541
|
+
return { acceptedEvents, instrumentationSnapshot: edited ? prePass : null };
|
|
2542
|
+
}
|
|
1849
2543
|
async function maybeRevert(repoPath, checkpoint, reason) {
|
|
1850
2544
|
if (!checkpoint.isRepo) return false;
|
|
1851
2545
|
const files = await changedFiles(repoPath, checkpoint);
|
|
@@ -1946,7 +2640,7 @@ function summarizeManifest(m) {
|
|
|
1946
2640
|
// src/index.ts
|
|
1947
2641
|
var modulePath = fileURLToPath(import.meta.url);
|
|
1948
2642
|
function packageVersion() {
|
|
1949
|
-
const packagePath =
|
|
2643
|
+
const packagePath = join4(dirname2(modulePath), "..", "package.json");
|
|
1950
2644
|
const pkg = JSON.parse(readFileSync(packagePath, "utf8"));
|
|
1951
2645
|
return pkg.version ?? "0.0.0";
|
|
1952
2646
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@whisperr/wizard",
|
|
3
|
-
"version": "0.1
|
|
3
|
+
"version": "0.2.1",
|
|
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
|
"repository": {
|
|
6
6
|
"type": "git",
|