@whisperr/wizard 0.3.0 → 0.4.0
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 +449 -46
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -8,6 +8,7 @@ import { fileURLToPath } from "url";
|
|
|
8
8
|
// src/cli.ts
|
|
9
9
|
import { resolve as resolve2 } from "path";
|
|
10
10
|
import * as p from "@clack/prompts";
|
|
11
|
+
import open2 from "open";
|
|
11
12
|
|
|
12
13
|
// src/core/config.ts
|
|
13
14
|
var DEFAULT_API_BASE = "https://api.whisperr.net";
|
|
@@ -1016,42 +1017,67 @@ var divider = theme.dim("\u2500".repeat(48));
|
|
|
1016
1017
|
// src/core/auth.ts
|
|
1017
1018
|
async function authenticate(config) {
|
|
1018
1019
|
if (config.offline) return offlineSession();
|
|
1020
|
+
const auth = await startDeviceAuth(config);
|
|
1021
|
+
const url = auth.verificationUrlComplete ?? auth.verificationUrl;
|
|
1022
|
+
try {
|
|
1023
|
+
await open(url);
|
|
1024
|
+
} catch {
|
|
1025
|
+
}
|
|
1026
|
+
return auth.poll();
|
|
1027
|
+
}
|
|
1028
|
+
async function startDeviceAuth(config) {
|
|
1019
1029
|
const authorize = await fetchJson(
|
|
1020
1030
|
`${config.apiBaseUrl}/wizard/device/authorize`,
|
|
1021
1031
|
{ method: "POST", body: JSON.stringify({ client: "whisperr-wizard" }) }
|
|
1022
1032
|
);
|
|
1023
|
-
const url = authorize.verification_uri_complete ?? authorize.verification_uri;
|
|
1024
1033
|
const intervalMs = (authorize.interval ?? 5) * 1e3;
|
|
1025
1034
|
const deadline = Date.now() + (authorize.expires_in ?? 600) * 1e3;
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1035
|
+
return {
|
|
1036
|
+
verificationUrl: authorize.verification_uri,
|
|
1037
|
+
verificationUrlComplete: authorize.verification_uri_complete,
|
|
1038
|
+
userCode: authorize.user_code,
|
|
1039
|
+
async poll() {
|
|
1040
|
+
let wait = intervalMs;
|
|
1041
|
+
while (Date.now() < deadline) {
|
|
1042
|
+
await sleep(wait);
|
|
1043
|
+
const res = await fetch(
|
|
1044
|
+
`${config.apiBaseUrl}/wizard/device/token`,
|
|
1045
|
+
{
|
|
1046
|
+
method: "POST",
|
|
1047
|
+
headers: { "content-type": "application/json" },
|
|
1048
|
+
body: JSON.stringify({ device_code: authorize.device_code })
|
|
1049
|
+
}
|
|
1050
|
+
);
|
|
1051
|
+
if (res.ok) {
|
|
1052
|
+
const tok = await res.json();
|
|
1053
|
+
return { token: tok.token, appId: tok.app_id, expiresAt: tok.expires_at };
|
|
1054
|
+
}
|
|
1055
|
+
if (res.status === 429) {
|
|
1056
|
+
wait += 2e3;
|
|
1057
|
+
continue;
|
|
1058
|
+
}
|
|
1059
|
+
if (res.status === 428) continue;
|
|
1060
|
+
throw new Error(
|
|
1061
|
+
`Authorization failed (${res.status}). Re-run the wizard and use the printed link and code to try again.`
|
|
1062
|
+
);
|
|
1039
1063
|
}
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
return { token: tok.token, appId: tok.app_id, expiresAt: tok.expires_at };
|
|
1044
|
-
}
|
|
1045
|
-
if (res.status === 429) {
|
|
1046
|
-
wait += 2e3;
|
|
1047
|
-
continue;
|
|
1064
|
+
throw new Error(
|
|
1065
|
+
"Authorization timed out. Re-run the wizard and use the printed link and code to try again."
|
|
1066
|
+
);
|
|
1048
1067
|
}
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
}
|
|
1054
|
-
|
|
1068
|
+
};
|
|
1069
|
+
}
|
|
1070
|
+
function startSessionKeepalive(config, session, intervalMs = 5 * 6e4) {
|
|
1071
|
+
if (config.offline) return () => {
|
|
1072
|
+
};
|
|
1073
|
+
const timer = setInterval(() => {
|
|
1074
|
+
fetch(`${config.apiBaseUrl}/wizard/first-event`, {
|
|
1075
|
+
headers: { authorization: `Bearer ${session.token}` }
|
|
1076
|
+
}).catch(() => {
|
|
1077
|
+
});
|
|
1078
|
+
}, intervalMs);
|
|
1079
|
+
timer.unref?.();
|
|
1080
|
+
return () => clearInterval(timer);
|
|
1055
1081
|
}
|
|
1056
1082
|
function offlineSession() {
|
|
1057
1083
|
return {
|
|
@@ -1148,7 +1174,9 @@ function mockManifest(appId, config) {
|
|
|
1148
1174
|
}
|
|
1149
1175
|
|
|
1150
1176
|
// src/core/agent.ts
|
|
1151
|
-
import {
|
|
1177
|
+
import {
|
|
1178
|
+
query
|
|
1179
|
+
} from "@anthropic-ai/claude-agent-sdk";
|
|
1152
1180
|
|
|
1153
1181
|
// src/core/git.ts
|
|
1154
1182
|
import { spawn } from "child_process";
|
|
@@ -1403,6 +1431,71 @@ async function submitAdditions(config, session, target9, repoFingerprint2, oppor
|
|
|
1403
1431
|
}
|
|
1404
1432
|
return await res.json();
|
|
1405
1433
|
}
|
|
1434
|
+
async function submitSuggestions(config, session, target9, repoFingerprint2, opportunities) {
|
|
1435
|
+
const res = await fetch(`${config.apiBaseUrl}/wizard/universe/suggestions`, {
|
|
1436
|
+
method: "POST",
|
|
1437
|
+
signal: AbortSignal.timeout(SUBMIT_TIMEOUT_MS),
|
|
1438
|
+
headers: {
|
|
1439
|
+
"Content-Type": "application/json",
|
|
1440
|
+
Authorization: `Bearer ${session.token}`
|
|
1441
|
+
},
|
|
1442
|
+
body: JSON.stringify({
|
|
1443
|
+
target: target9,
|
|
1444
|
+
repoFingerprint: repoFingerprint2,
|
|
1445
|
+
events: opportunities.events,
|
|
1446
|
+
interventions: opportunities.interventions
|
|
1447
|
+
})
|
|
1448
|
+
});
|
|
1449
|
+
if (res.status === 404) return null;
|
|
1450
|
+
if (!res.ok) {
|
|
1451
|
+
const body = await res.text().catch(() => "");
|
|
1452
|
+
throw new Error(
|
|
1453
|
+
`Submitting universe suggestions failed (${res.status})${body ? `: ${body.slice(0, 300)}` : ""}`
|
|
1454
|
+
);
|
|
1455
|
+
}
|
|
1456
|
+
return await res.json();
|
|
1457
|
+
}
|
|
1458
|
+
async function fetchSuggestions(config, session, statuses) {
|
|
1459
|
+
if (config.offline) return [];
|
|
1460
|
+
try {
|
|
1461
|
+
const statusQuery = statuses.map(encodeURIComponent).join(",");
|
|
1462
|
+
const url = `${config.apiBaseUrl}/wizard/universe/suggestions${statusQuery ? `?status=${statusQuery}` : ""}`;
|
|
1463
|
+
const res = await fetch(url, {
|
|
1464
|
+
signal: AbortSignal.timeout(SUBMIT_TIMEOUT_MS),
|
|
1465
|
+
headers: { Authorization: `Bearer ${session.token}` }
|
|
1466
|
+
});
|
|
1467
|
+
if (!res.ok) return [];
|
|
1468
|
+
const body = await res.json();
|
|
1469
|
+
return Array.isArray(body) && body.every(isSuggestionRecord) ? body : [];
|
|
1470
|
+
} catch {
|
|
1471
|
+
return [];
|
|
1472
|
+
}
|
|
1473
|
+
}
|
|
1474
|
+
function isSuggestionRecord(value) {
|
|
1475
|
+
if (typeof value !== "object" || value === null) return false;
|
|
1476
|
+
const record = value;
|
|
1477
|
+
return typeof record.id === "string" && (record.kind === "event" || record.kind === "intervention") && typeof record.code === "string" && (record.status === "proposed" || record.status === "approved" || record.status === "rejected" || record.status === "integrated") && typeof record.payload === "object" && record.payload !== null;
|
|
1478
|
+
}
|
|
1479
|
+
async function markSuggestionIntegrated(config, session, id, target9, repoFingerprint2) {
|
|
1480
|
+
if (config.offline) return false;
|
|
1481
|
+
try {
|
|
1482
|
+
const res = await fetch(
|
|
1483
|
+
`${config.apiBaseUrl}/wizard/universe/suggestions/${encodeURIComponent(id)}/integrated`,
|
|
1484
|
+
{
|
|
1485
|
+
method: "POST",
|
|
1486
|
+
signal: AbortSignal.timeout(SUBMIT_TIMEOUT_MS),
|
|
1487
|
+
headers: {
|
|
1488
|
+
"Content-Type": "application/json",
|
|
1489
|
+
Authorization: `Bearer ${session.token}`
|
|
1490
|
+
},
|
|
1491
|
+
body: JSON.stringify({ target: target9, repoFingerprint: repoFingerprint2 })
|
|
1492
|
+
}
|
|
1493
|
+
);
|
|
1494
|
+
return res.status === 200;
|
|
1495
|
+
} catch {
|
|
1496
|
+
return false;
|
|
1497
|
+
}
|
|
1498
|
+
}
|
|
1406
1499
|
function asArray(value) {
|
|
1407
1500
|
return Array.isArray(value) ? value : [];
|
|
1408
1501
|
}
|
|
@@ -1458,6 +1551,7 @@ function coerceEvent(entry) {
|
|
|
1458
1551
|
description: asString(e.description),
|
|
1459
1552
|
...side ? { side } : {},
|
|
1460
1553
|
rationale: asString(e.rationale),
|
|
1554
|
+
expectedEffect: asString(e.expectedEffect),
|
|
1461
1555
|
confidence: asConfidence(e.confidence),
|
|
1462
1556
|
...properties.length ? { properties } : {},
|
|
1463
1557
|
...links.length ? { links } : {}
|
|
@@ -1482,6 +1576,7 @@ function coerceIntervention(entry) {
|
|
|
1482
1576
|
label: asString(i.label),
|
|
1483
1577
|
description: asString(i.description),
|
|
1484
1578
|
rationale: asString(i.rationale),
|
|
1579
|
+
expectedEffect: asString(i.expectedEffect),
|
|
1485
1580
|
confidence: asConfidence(i.confidence),
|
|
1486
1581
|
...links.length ? { links } : {}
|
|
1487
1582
|
};
|
|
@@ -1682,16 +1777,20 @@ function renderOpportunitiesBrief(m, opportunitiesFile) {
|
|
|
1682
1777
|
];
|
|
1683
1778
|
return [
|
|
1684
1779
|
"UNIVERSE OPPORTUNITIES (do this LAST, after your corrections):",
|
|
1685
|
-
"While auditing you read this app's real lifecycle.
|
|
1686
|
-
"
|
|
1687
|
-
"
|
|
1688
|
-
"
|
|
1780
|
+
"While auditing you read this app's real lifecycle. Now sweep it",
|
|
1781
|
+
"deliberately: walk each surface the plan should care about \u2014 signup and",
|
|
1782
|
+
"auth, billing/payments (renewals, failures, upgrades, downgrades,",
|
|
1783
|
+
"cancellation), the core engagement loop, support/feedback/refund flows,",
|
|
1784
|
+
"sharing/invites, and any expiry or dormancy mechanics you saw \u2014 and for",
|
|
1785
|
+
"each one ask: does a churn-relevant moment happen here that the plan does",
|
|
1786
|
+
"NOT cover? Record those as PROPOSALS. Do NOT add track() calls for them.",
|
|
1689
1787
|
`Write a single JSON file at the repo root named ${opportunitiesFile}:`,
|
|
1690
1788
|
"",
|
|
1691
1789
|
"{",
|
|
1692
1790
|
' "events": [{',
|
|
1693
1791
|
' "code": "snake_case_event", "label": "...", "description": "...",',
|
|
1694
1792
|
' "side": "frontend|backend|either", "rationale": "what in the code shows this",',
|
|
1793
|
+
' "expectedEffect": "one sentence: what improves if this is adopted",',
|
|
1695
1794
|
' "confidence": 0.0-1.0,',
|
|
1696
1795
|
' "properties": [{"name": "...", "description": "...", "required": false}],',
|
|
1697
1796
|
' "links": [{"interventionCode": "existing_or_proposed", "weight": 0.0-1.0}]',
|
|
@@ -1699,6 +1798,7 @@ function renderOpportunitiesBrief(m, opportunitiesFile) {
|
|
|
1699
1798
|
' "interventions": [{',
|
|
1700
1799
|
' "code": "snake_case_strategy", "label": "...", "description": "...",',
|
|
1701
1800
|
' "rationale": "...", "confidence": 0.0-1.0,',
|
|
1801
|
+
' "expectedEffect": "one sentence: what improves if this is adopted",',
|
|
1702
1802
|
' "links": [{"eventCode": "existing_or_proposed", "weight": 0.0-1.0}]',
|
|
1703
1803
|
" }]",
|
|
1704
1804
|
"}",
|
|
@@ -1708,8 +1808,14 @@ function renderOpportunitiesBrief(m, opportunitiesFile) {
|
|
|
1708
1808
|
` And these interventions: ${interventionCodes.join(", ") || "(none)"}.`,
|
|
1709
1809
|
" Propose ONLY what is genuinely missing \u2014 never re-propose or rename these.",
|
|
1710
1810
|
"- Every proposal needs concrete code evidence in its rationale (file/flow),",
|
|
1711
|
-
" not speculation.
|
|
1712
|
-
"
|
|
1811
|
+
" not speculation. VERIFY before you write: re-open the file you are citing",
|
|
1812
|
+
" and confirm the flow exists as described \u2014 a false suggestion costs more",
|
|
1813
|
+
" trust than a missed one, so drop anything you cannot point at real code.",
|
|
1814
|
+
"- Be thorough, not shy: a feature-rich product typically yields 3-6 solid",
|
|
1815
|
+
" event proposals and 1-3 interventions. An empty file is only the right",
|
|
1816
|
+
" answer when the plan genuinely already covers the product's lifecycle \u2014",
|
|
1817
|
+
" never because you stopped looking early.",
|
|
1818
|
+
"- Every proposal should include expectedEffect: one sentence explaining what adoption should improve.",
|
|
1713
1819
|
"- Link each proposed event to the intervention(s) it should feed (existing",
|
|
1714
1820
|
" codes or ones you propose in the same file).",
|
|
1715
1821
|
"- This file is metadata for the wizard, not app code \u2014 write it and move on."
|
|
@@ -1730,11 +1836,20 @@ function coverageNote(coverage) {
|
|
|
1730
1836
|
import { isAbsolute, relative, resolve } from "path";
|
|
1731
1837
|
var SECRET_MATERIAL_DENIAL = "blocked: secret material - reference the variable name in .env.example instead of reading the real file";
|
|
1732
1838
|
var WRITE_OUTSIDE_REPO_DENIAL = "blocked: writes must stay inside the target repository";
|
|
1839
|
+
var SENSITIVE_WRITE_DENIAL = "blocked: refusing to write CI/git configuration";
|
|
1733
1840
|
var BASH_ALLOWLIST_DENIAL = "blocked: command is outside the Bash allowlist - use package install/add commands, mkdir, or git status/diff/log only";
|
|
1734
1841
|
var CHAINED_COMMAND_DENIAL = "blocked: chained or substituted shell command - run one command at a time";
|
|
1735
1842
|
var ENV_EXAMPLES = /* @__PURE__ */ new Set([".env.example", ".env.sample", ".env.template"]);
|
|
1736
1843
|
var SECRET_DIRECTORIES = /* @__PURE__ */ new Set([".aws", ".ssh", ".gnupg"]);
|
|
1737
1844
|
var SECRET_EXACT_FILES = /* @__PURE__ */ new Set([".netrc", ".npmrc", ".pypirc"]);
|
|
1845
|
+
var SENSITIVE_WRITE_DIRECTORIES = /* @__PURE__ */ new Set([".git", ".circleci", ".buildkite"]);
|
|
1846
|
+
var SENSITIVE_WRITE_EXACT_FILES = /* @__PURE__ */ new Set([
|
|
1847
|
+
".gitlab-ci.yml",
|
|
1848
|
+
"azure-pipelines.yml",
|
|
1849
|
+
"bitbucket-pipelines.yml",
|
|
1850
|
+
".drone.yml",
|
|
1851
|
+
"jenkinsfile"
|
|
1852
|
+
]);
|
|
1738
1853
|
var SECRET_SUFFIXES = [
|
|
1739
1854
|
".pem",
|
|
1740
1855
|
".key",
|
|
@@ -1818,6 +1933,9 @@ function evaluateWrite(input, context) {
|
|
|
1818
1933
|
if (!isPathInsideRepo(pathValue, context.repoPath)) {
|
|
1819
1934
|
return { behavior: "deny", message: WRITE_OUTSIDE_REPO_DENIAL };
|
|
1820
1935
|
}
|
|
1936
|
+
if (isSensitiveWritePath(pathValue, context.repoPath)) {
|
|
1937
|
+
return { behavior: "deny", message: SENSITIVE_WRITE_DENIAL };
|
|
1938
|
+
}
|
|
1821
1939
|
return { behavior: "allow" };
|
|
1822
1940
|
}
|
|
1823
1941
|
function evaluateBash(input) {
|
|
@@ -1907,6 +2025,19 @@ function firstString(input, keys) {
|
|
|
1907
2025
|
function normalizePathLike(value) {
|
|
1908
2026
|
return stripOuterQuotes(value.trim()).replace(/\\/g, "/");
|
|
1909
2027
|
}
|
|
2028
|
+
function repoRelativePath(pathValue, repoPath) {
|
|
2029
|
+
const repoRoot = resolve(repoPath);
|
|
2030
|
+
const candidate = isAbsolute(pathValue) ? resolve(pathValue) : resolve(repoRoot, pathValue);
|
|
2031
|
+
return normalizePathLike(relative(repoRoot, candidate));
|
|
2032
|
+
}
|
|
2033
|
+
function isSensitiveWritePath(pathValue, repoPath) {
|
|
2034
|
+
const parts = repoRelativePath(pathValue, repoPath).split("/").map((part) => stripOuterQuotes(part.trim().toLowerCase())).filter(Boolean);
|
|
2035
|
+
const [first, second] = parts;
|
|
2036
|
+
if (!first) return false;
|
|
2037
|
+
if (SENSITIVE_WRITE_DIRECTORIES.has(first)) return true;
|
|
2038
|
+
if (first === ".github" && second === "workflows") return true;
|
|
2039
|
+
return parts.length === 1 && SENSITIVE_WRITE_EXACT_FILES.has(first);
|
|
2040
|
+
}
|
|
1910
2041
|
function stripOuterQuotes(value) {
|
|
1911
2042
|
if (value.startsWith('"') && value.endsWith('"') || value.startsWith("'") && value.endsWith("'")) {
|
|
1912
2043
|
return value.slice(1, -1);
|
|
@@ -2672,6 +2803,7 @@ async function runPass(opts) {
|
|
|
2672
2803
|
thinking: { type: "adaptive" },
|
|
2673
2804
|
effort,
|
|
2674
2805
|
tools: [...allowedTools],
|
|
2806
|
+
hooks: buildToolPolicyHooks(repoPath),
|
|
2675
2807
|
canUseTool: createToolPermissionCallback(repoPath),
|
|
2676
2808
|
maxTurns,
|
|
2677
2809
|
// Native SDK hard cost cap. Stops this pass with an `error_max_budget_usd`
|
|
@@ -2708,6 +2840,32 @@ async function runPass(opts) {
|
|
|
2708
2840
|
}
|
|
2709
2841
|
return { summary, costUsd, ok, maxedOut };
|
|
2710
2842
|
}
|
|
2843
|
+
function buildToolPolicyHooks(repoPath) {
|
|
2844
|
+
return {
|
|
2845
|
+
PreToolUse: [
|
|
2846
|
+
{
|
|
2847
|
+
hooks: [
|
|
2848
|
+
async (input) => {
|
|
2849
|
+
if (input.hook_event_name !== "PreToolUse") return { continue: true };
|
|
2850
|
+
const decision = evaluateToolUse(input.tool_name, toolInputRecord(input.tool_input), {
|
|
2851
|
+
repoPath
|
|
2852
|
+
});
|
|
2853
|
+
if (decision.behavior === "allow") {
|
|
2854
|
+
return { continue: true };
|
|
2855
|
+
}
|
|
2856
|
+
return {
|
|
2857
|
+
hookSpecificOutput: {
|
|
2858
|
+
hookEventName: "PreToolUse",
|
|
2859
|
+
permissionDecision: "deny",
|
|
2860
|
+
permissionDecisionReason: decision.message
|
|
2861
|
+
}
|
|
2862
|
+
};
|
|
2863
|
+
}
|
|
2864
|
+
]
|
|
2865
|
+
}
|
|
2866
|
+
]
|
|
2867
|
+
};
|
|
2868
|
+
}
|
|
2711
2869
|
function createToolPermissionCallback(repoPath) {
|
|
2712
2870
|
return async (toolName, input, options) => {
|
|
2713
2871
|
const decision = evaluateToolUse(toolName, input, { repoPath });
|
|
@@ -2718,6 +2876,12 @@ function createToolPermissionCallback(repoPath) {
|
|
|
2718
2876
|
};
|
|
2719
2877
|
};
|
|
2720
2878
|
}
|
|
2879
|
+
function toolInputRecord(input) {
|
|
2880
|
+
if (input && typeof input === "object" && !Array.isArray(input)) {
|
|
2881
|
+
return input;
|
|
2882
|
+
}
|
|
2883
|
+
return {};
|
|
2884
|
+
}
|
|
2721
2885
|
function applyModelAuthEnv(config, session) {
|
|
2722
2886
|
if (config.directAnthropicKey) {
|
|
2723
2887
|
process.env.ANTHROPIC_API_KEY = config.directAnthropicKey;
|
|
@@ -2844,6 +3008,12 @@ function tail2(s) {
|
|
|
2844
3008
|
}
|
|
2845
3009
|
|
|
2846
3010
|
// src/core/report.ts
|
|
3011
|
+
function scrubSummary(text) {
|
|
3012
|
+
return scrubTerminalControls(text).replace(/sk-ant-[A-Za-z0-9_-]+/g, "[redacted]").replace(/sk-[A-Za-z0-9]{16,}/g, "[redacted]").replace(/AKIA[0-9A-Z]{16}/g, "[redacted]").replace(/Bearer\s+[A-Za-z0-9._-]+/g, "[redacted]").replace(
|
|
3013
|
+
/\b(ANTHROPIC_(?:AUTH_TOKEN|API_KEY))\s*=\s*["']?[^"'\s]+["']?/gi,
|
|
3014
|
+
"$1=[redacted]"
|
|
3015
|
+
);
|
|
3016
|
+
}
|
|
2847
3017
|
async function postRunReport(config, session, report) {
|
|
2848
3018
|
if (config.offline) return { ok: true };
|
|
2849
3019
|
try {
|
|
@@ -2879,6 +3049,121 @@ function detailSlice(detail) {
|
|
|
2879
3049
|
const sliced = detail.slice(0, 200).trim();
|
|
2880
3050
|
return sliced || void 0;
|
|
2881
3051
|
}
|
|
3052
|
+
function scrubTerminalControls(value) {
|
|
3053
|
+
return value.replace(/\x1b\[[0-9;?]*[ -/]*[@-~]/g, "").replace(/\x1b\][^\x07\x1b]*(?:\x07|\x1b\\)?/g, "").replace(/[\x00-\x1f\x7f]/g, " ").replace(/ {2,}/g, " ").trim();
|
|
3054
|
+
}
|
|
3055
|
+
|
|
3056
|
+
// src/core/gapreport.ts
|
|
3057
|
+
function buildGapReport(input) {
|
|
3058
|
+
const outcomes = new Map(
|
|
3059
|
+
input.events.map((event) => [
|
|
3060
|
+
event.eventType,
|
|
3061
|
+
{
|
|
3062
|
+
status: event.status,
|
|
3063
|
+
reason: trimmedReason(event.reason)
|
|
3064
|
+
}
|
|
3065
|
+
])
|
|
3066
|
+
);
|
|
3067
|
+
const interventions = /* @__PURE__ */ new Map();
|
|
3068
|
+
for (const event of input.manifest.events) {
|
|
3069
|
+
for (const intervention of event.interventions ?? []) {
|
|
3070
|
+
let collected = interventions.get(intervention.code);
|
|
3071
|
+
if (!collected) {
|
|
3072
|
+
collected = {
|
|
3073
|
+
code: intervention.code,
|
|
3074
|
+
label: intervention.label,
|
|
3075
|
+
drivingEvents: [],
|
|
3076
|
+
drivingEventSet: /* @__PURE__ */ new Set()
|
|
3077
|
+
};
|
|
3078
|
+
interventions.set(intervention.code, collected);
|
|
3079
|
+
} else if (!collected.label && intervention.label) {
|
|
3080
|
+
collected.label = intervention.label;
|
|
3081
|
+
}
|
|
3082
|
+
if (!collected.drivingEventSet.has(event.eventType)) {
|
|
3083
|
+
collected.drivingEventSet.add(event.eventType);
|
|
3084
|
+
collected.drivingEvents.push(event.eventType);
|
|
3085
|
+
}
|
|
3086
|
+
}
|
|
3087
|
+
}
|
|
3088
|
+
const interventionGaps = [];
|
|
3089
|
+
for (const intervention of interventions.values()) {
|
|
3090
|
+
const plannedEvents = intervention.drivingEvents.filter((code) => outcomes.has(code));
|
|
3091
|
+
const missingEvents = plannedEvents.flatMap((code) => {
|
|
3092
|
+
const outcome = outcomes.get(code);
|
|
3093
|
+
if (outcome?.status !== "skipped") return [];
|
|
3094
|
+
return [{ code, ...outcome.reason ? { reason: outcome.reason } : {} }];
|
|
3095
|
+
});
|
|
3096
|
+
if (missingEvents.length === 0) continue;
|
|
3097
|
+
interventionGaps.push({
|
|
3098
|
+
code: intervention.code,
|
|
3099
|
+
...intervention.label ? { label: intervention.label } : {},
|
|
3100
|
+
blocked: missingEvents.length === plannedEvents.length ? "fully" : "partially",
|
|
3101
|
+
missingEvents
|
|
3102
|
+
});
|
|
3103
|
+
}
|
|
3104
|
+
interventionGaps.sort((a, b) => {
|
|
3105
|
+
if (a.blocked !== b.blocked) return a.blocked === "fully" ? -1 : 1;
|
|
3106
|
+
return b.missingEvents.length - a.missingEvents.length;
|
|
3107
|
+
});
|
|
3108
|
+
const otherSurface = input.manifest.universeSummary?.otherSurface ?? 0;
|
|
3109
|
+
const derived = input.manifest.universeSummary?.derived ?? 0;
|
|
3110
|
+
return {
|
|
3111
|
+
interventionGaps,
|
|
3112
|
+
otherSurface,
|
|
3113
|
+
derived,
|
|
3114
|
+
hasContent: interventionGaps.length > 0 || otherSurface > 0
|
|
3115
|
+
};
|
|
3116
|
+
}
|
|
3117
|
+
function renderGapReport(report, theme2) {
|
|
3118
|
+
const lines = [];
|
|
3119
|
+
for (const gap of report.interventionGaps) {
|
|
3120
|
+
const status = gap.blocked === "fully" ? "on hold" : "running below potential";
|
|
3121
|
+
lines.push(`\u25A0 ${theme2.bright(gap.label ?? gap.code)}${theme2.muted(` \u2014 ${status}`)}`);
|
|
3122
|
+
for (const event of gap.missingEvents) {
|
|
3123
|
+
const reason = event.reason ? ` \u2014 ${lowercaseFirst(event.reason)}` : "";
|
|
3124
|
+
lines.push(
|
|
3125
|
+
theme2.muted(" \xB7 ") + theme2.bright(event.code) + theme2.muted(` isn't flowing yet${reason}`)
|
|
3126
|
+
);
|
|
3127
|
+
}
|
|
3128
|
+
lines.push(
|
|
3129
|
+
theme2.muted(
|
|
3130
|
+
gap.blocked === "fully" ? " Ship the missing flow and this play switches on by itself \u2014 the strategy is already built for it." : " It works today, but with only part of its signal \u2014 every event above sharpens it."
|
|
3131
|
+
)
|
|
3132
|
+
);
|
|
3133
|
+
}
|
|
3134
|
+
if (report.otherSurface > 0) {
|
|
3135
|
+
const suffix = report.otherSurface === 1 ? "" : "s";
|
|
3136
|
+
lines.push(
|
|
3137
|
+
`\u25A0 ${report.otherSurface} more event${suffix} live on another surface (your backend or another app)`
|
|
3138
|
+
);
|
|
3139
|
+
lines.push(
|
|
3140
|
+
theme2.muted(
|
|
3141
|
+
" Run the wizard there and they join the same strategy \u2014 nothing to reconfigure."
|
|
3142
|
+
)
|
|
3143
|
+
);
|
|
3144
|
+
}
|
|
3145
|
+
if (report.derived > 0) {
|
|
3146
|
+
const suffix = report.derived === 1 ? "" : "s";
|
|
3147
|
+
lines.push(
|
|
3148
|
+
theme2.muted(
|
|
3149
|
+
`${report.derived} derived signal${suffix} (dormancy, anniversaries, expiries) are computed by Whisperr automatically once your raw events flow \u2014 nothing to wire.`
|
|
3150
|
+
)
|
|
3151
|
+
);
|
|
3152
|
+
}
|
|
3153
|
+
lines.push(
|
|
3154
|
+
theme2.muted(
|
|
3155
|
+
"All of this is capacity your strategy already includes. Each item you unlock compounds the rest."
|
|
3156
|
+
)
|
|
3157
|
+
);
|
|
3158
|
+
return lines.join("\n");
|
|
3159
|
+
}
|
|
3160
|
+
function trimmedReason(reason) {
|
|
3161
|
+
const trimmed = reason?.trim();
|
|
3162
|
+
return trimmed || void 0;
|
|
3163
|
+
}
|
|
3164
|
+
function lowercaseFirst(value) {
|
|
3165
|
+
return value.slice(0, 1).toLowerCase() + value.slice(1);
|
|
3166
|
+
}
|
|
2882
3167
|
|
|
2883
3168
|
// src/ui/banner.ts
|
|
2884
3169
|
function banner() {
|
|
@@ -2943,11 +3228,13 @@ Flutter is live today; ${theme.bright(
|
|
|
2943
3228
|
p.cancel(theme.alert(err.message));
|
|
2944
3229
|
return 1;
|
|
2945
3230
|
}
|
|
3231
|
+
startSessionKeepalive(config, session);
|
|
2946
3232
|
const fingerprint = await repoFingerprint(repoPath);
|
|
2947
3233
|
const manifest = await withSpinner(
|
|
2948
3234
|
"Loading your onboarding context",
|
|
2949
3235
|
() => fetchManifest(config, session, chosen.playbook.target.id, fingerprint)
|
|
2950
3236
|
);
|
|
3237
|
+
const approvedSuggestionsPromise = config.offline ? null : fetchSuggestions(config, session, ["approved"]);
|
|
2951
3238
|
p.note(
|
|
2952
3239
|
summarizeManifest(manifest),
|
|
2953
3240
|
theme.signal(`Plan for ${manifest.appName ?? manifest.appId}`)
|
|
@@ -3071,9 +3358,7 @@ Flutter is live today; ${theme.bright(
|
|
|
3071
3358
|
if (files.length) {
|
|
3072
3359
|
p.note(files.map((f) => theme.muted("\u2022 ") + f).join("\n"), "Files changed");
|
|
3073
3360
|
}
|
|
3074
|
-
|
|
3075
|
-
p.log.message(outcome.summary.trim());
|
|
3076
|
-
}
|
|
3361
|
+
logAgentSummary(outcome.summary);
|
|
3077
3362
|
if (!outcome.coreOk) {
|
|
3078
3363
|
const reverted = await maybeRevert(
|
|
3079
3364
|
repoPath,
|
|
@@ -3124,7 +3409,7 @@ Flutter is live today; ${theme.bright(
|
|
|
3124
3409
|
if (repair.summary.trim()) {
|
|
3125
3410
|
outcome.summary = [outcome.summary, `Repair:
|
|
3126
3411
|
${repair.summary}`].filter(Boolean).join("\n\n");
|
|
3127
|
-
|
|
3412
|
+
logAgentSummary(repair.summary);
|
|
3128
3413
|
}
|
|
3129
3414
|
repairSpin.stop(theme.success("Repair pass finished"));
|
|
3130
3415
|
} catch (err) {
|
|
@@ -3243,7 +3528,7 @@ ${repair.summary}`].filter(Boolean).join("\n\n");
|
|
|
3243
3528
|
verified,
|
|
3244
3529
|
cost_usd: outcome.costUsd,
|
|
3245
3530
|
duration_ms: outcome.durationMs,
|
|
3246
|
-
summary: outcome.summary.slice(0, 4e3),
|
|
3531
|
+
summary: scrubSummary(outcome.summary).slice(0, 4e3),
|
|
3247
3532
|
events: reportEvents
|
|
3248
3533
|
});
|
|
3249
3534
|
if (!reportResult.ok) {
|
|
@@ -3253,10 +3538,62 @@ ${repair.summary}`].filter(Boolean).join("\n\n");
|
|
|
3253
3538
|
)
|
|
3254
3539
|
);
|
|
3255
3540
|
}
|
|
3541
|
+
try {
|
|
3542
|
+
const approvedSuggestions = await approvedSuggestionsPromise;
|
|
3543
|
+
if (approvedSuggestions?.length) {
|
|
3544
|
+
const integratedEventCodes = new Set(
|
|
3545
|
+
reportEvents.filter((event) => event.status === "wired").map((event) => normalizeCode(event.event_type))
|
|
3546
|
+
);
|
|
3547
|
+
for (const event of manifest.events) {
|
|
3548
|
+
if (event.coverage?.some((entry) => entry.sameSurface && entry.status === "wired")) {
|
|
3549
|
+
integratedEventCodes.add(normalizeCode(event.eventType));
|
|
3550
|
+
}
|
|
3551
|
+
}
|
|
3552
|
+
const toMark = approvedSuggestions.filter(
|
|
3553
|
+
(suggestion) => suggestion.status === "approved" && (suggestion.kind === "intervention" || integratedEventCodes.has(normalizeCode(suggestion.code)))
|
|
3554
|
+
);
|
|
3555
|
+
const marked = (await Promise.all(
|
|
3556
|
+
toMark.map(
|
|
3557
|
+
(suggestion) => markSuggestionIntegrated(
|
|
3558
|
+
config,
|
|
3559
|
+
session,
|
|
3560
|
+
suggestion.id,
|
|
3561
|
+
chosen.playbook.target.id,
|
|
3562
|
+
fingerprint
|
|
3563
|
+
)
|
|
3564
|
+
)
|
|
3565
|
+
)).filter(Boolean).length;
|
|
3566
|
+
if (marked > 0) {
|
|
3567
|
+
p.log.info(
|
|
3568
|
+
theme.muted(
|
|
3569
|
+
`${marked} approved suggestion${marked === 1 ? "" : "s"} marked integrated.`
|
|
3570
|
+
)
|
|
3571
|
+
);
|
|
3572
|
+
}
|
|
3573
|
+
}
|
|
3574
|
+
} catch {
|
|
3575
|
+
}
|
|
3256
3576
|
const eventLines = renderEventOutcomeLines(outcome.eventOutcomes, wiredMap);
|
|
3257
3577
|
if (eventLines) {
|
|
3258
3578
|
p.note(eventLines, "Events");
|
|
3259
3579
|
}
|
|
3580
|
+
const eventOutcomeByType = new Map(
|
|
3581
|
+
outcome.eventOutcomes.map((event) => [event.event, event])
|
|
3582
|
+
);
|
|
3583
|
+
const gapReport = buildGapReport({
|
|
3584
|
+
manifest,
|
|
3585
|
+
events: reportEvents.map((event) => ({
|
|
3586
|
+
eventType: event.event_type,
|
|
3587
|
+
status: event.status,
|
|
3588
|
+
reason: eventOutcomeByType.get(event.event_type)?.reason
|
|
3589
|
+
}))
|
|
3590
|
+
});
|
|
3591
|
+
if (gapReport.hasContent) {
|
|
3592
|
+
p.note(
|
|
3593
|
+
renderGapReport(gapReport, theme),
|
|
3594
|
+
theme.signal("What you could be doing")
|
|
3595
|
+
);
|
|
3596
|
+
}
|
|
3260
3597
|
const eventDenominator = manifest.universeSummary?.wireableHere ?? eventTypesToScan.length;
|
|
3261
3598
|
const eventStatsLabel = manifest.universeSummary ? "events wired here" : "events wired";
|
|
3262
3599
|
const timingDetails = outcome.phaseTimings.map(({ phase, ms }) => `${shortPhaseLabel(phase)} ${formatDuration(ms)}`).join(" \xB7 ");
|
|
@@ -3299,7 +3636,21 @@ ${repair.summary}`].filter(Boolean).join("\n\n");
|
|
|
3299
3636
|
}
|
|
3300
3637
|
var NO_ADDITIONS = { acceptedEvents: [], instrumentationSnapshot: null };
|
|
3301
3638
|
async function offerOpportunities(opts) {
|
|
3302
|
-
const {
|
|
3639
|
+
const { manifest, config, session } = opts;
|
|
3640
|
+
const openSuggestions = await fetchSuggestions(config, session, ["proposed", "approved"]);
|
|
3641
|
+
const openSuggestionKeys = new Set(
|
|
3642
|
+
openSuggestions.map(
|
|
3643
|
+
(suggestion) => `${suggestion.kind}:${normalizeCode(suggestion.code)}`
|
|
3644
|
+
)
|
|
3645
|
+
);
|
|
3646
|
+
const opportunities = {
|
|
3647
|
+
events: opts.opportunities.events.filter(
|
|
3648
|
+
(event) => !openSuggestionKeys.has(`event:${normalizeCode(event.code)}`)
|
|
3649
|
+
),
|
|
3650
|
+
interventions: opts.opportunities.interventions.filter(
|
|
3651
|
+
(intervention) => !openSuggestionKeys.has(`intervention:${normalizeCode(intervention.code)}`)
|
|
3652
|
+
)
|
|
3653
|
+
};
|
|
3303
3654
|
const total = opportunities.events.length + opportunities.interventions.length;
|
|
3304
3655
|
if (total === 0) return NO_ADDITIONS;
|
|
3305
3656
|
const describe = (kind, code, why) => `${theme.muted(`${kind}: `)}${theme.bright(code)}${why ? theme.muted(` \u2014 ${why}`) : ""}`;
|
|
@@ -3323,7 +3674,7 @@ async function offerOpportunities(opts) {
|
|
|
3323
3674
|
return NO_ADDITIONS;
|
|
3324
3675
|
}
|
|
3325
3676
|
const selection = await p.multiselect({
|
|
3326
|
-
message: "
|
|
3677
|
+
message: "Send these to your dashboard for approval?",
|
|
3327
3678
|
options: [
|
|
3328
3679
|
...opportunities.events.map((e) => ({
|
|
3329
3680
|
value: `e:${e.code}`,
|
|
@@ -3336,8 +3687,8 @@ async function offerOpportunities(opts) {
|
|
|
3336
3687
|
hint: i.description ?? i.rationale
|
|
3337
3688
|
}))
|
|
3338
3689
|
],
|
|
3339
|
-
// Opt-in per item: nothing is pre-selected, so
|
|
3340
|
-
// explicit choice rather than an opt-out the user has to notice and undo.
|
|
3690
|
+
// Opt-in per item: nothing is pre-selected, so submitting a suggestion is
|
|
3691
|
+
// an explicit choice rather than an opt-out the user has to notice and undo.
|
|
3341
3692
|
initialValues: [],
|
|
3342
3693
|
required: false
|
|
3343
3694
|
});
|
|
@@ -3370,6 +3721,39 @@ async function offerOpportunities(opts) {
|
|
|
3370
3721
|
)
|
|
3371
3722
|
}))
|
|
3372
3723
|
};
|
|
3724
|
+
let stageResult;
|
|
3725
|
+
try {
|
|
3726
|
+
stageResult = await withSpinner(
|
|
3727
|
+
"Sending suggestions for approval",
|
|
3728
|
+
() => submitSuggestions(config, session, opts.playbook.target.id, opts.fingerprint, submitted)
|
|
3729
|
+
);
|
|
3730
|
+
} catch (err) {
|
|
3731
|
+
p.log.warn(
|
|
3732
|
+
theme.warn("Couldn't send the proposals") + theme.muted(` \u2014 ${err.message}. Nothing was submitted.`)
|
|
3733
|
+
);
|
|
3734
|
+
return NO_ADDITIONS;
|
|
3735
|
+
}
|
|
3736
|
+
if (stageResult) {
|
|
3737
|
+
const lines2 = stageResult.outcomes.map((outcome) => {
|
|
3738
|
+
const mark = outcome.status === "staged" ? theme.success("\u2713") : outcome.status === "duplicate" ? theme.muted("=") : theme.warn("\u2717");
|
|
3739
|
+
const reason = outcome.reason ?? (outcome.duplicateOf && outcome.duplicateOf !== outcome.code ? `already queued as ${outcome.duplicateOf}` : void 0);
|
|
3740
|
+
const detail = reason ? theme.muted(` (${reason})`) : "";
|
|
3741
|
+
return `${mark} ${outcome.kind} ${theme.bright(outcome.code)}${detail}`;
|
|
3742
|
+
});
|
|
3743
|
+
lines2.push(
|
|
3744
|
+
theme.muted(
|
|
3745
|
+
`${stageResult.staged} staged \xB7 ${stageResult.duplicates} already queued \xB7 ${stageResult.invalid} rejected`
|
|
3746
|
+
)
|
|
3747
|
+
);
|
|
3748
|
+
p.note(lines2.join("\n"), theme.signal("Suggestions submitted"));
|
|
3749
|
+
const where = stageResult.approvalsUrl ? `approve at ${theme.bright(stageResult.approvalsUrl)}` : "approve them in your Whisperr dashboard";
|
|
3750
|
+
p.log.info(
|
|
3751
|
+
theme.muted(
|
|
3752
|
+
`${stageResult.staged} proposal${stageResult.staged === 1 ? "" : "s"} sent \u2014 ${where}. The next wizard run wires whatever you approve.`
|
|
3753
|
+
)
|
|
3754
|
+
);
|
|
3755
|
+
return NO_ADDITIONS;
|
|
3756
|
+
}
|
|
3373
3757
|
let result;
|
|
3374
3758
|
try {
|
|
3375
3759
|
result = await withSpinner(
|
|
@@ -3435,7 +3819,7 @@ async function offerOpportunities(opts) {
|
|
|
3435
3819
|
});
|
|
3436
3820
|
if (pass.ran) {
|
|
3437
3821
|
spin.stop(theme.success("New events instrumented"));
|
|
3438
|
-
|
|
3822
|
+
logAgentSummary(pass.summary);
|
|
3439
3823
|
} else {
|
|
3440
3824
|
spin.stop(
|
|
3441
3825
|
theme.warn("Skipped instrumenting the new events") + theme.muted(
|
|
@@ -3515,10 +3899,25 @@ async function withBrowserAuth(config) {
|
|
|
3515
3899
|
p.log.step(
|
|
3516
3900
|
theme.bright("Authenticate") + theme.muted(" \u2014 opening your browser to approve this device\u2026")
|
|
3517
3901
|
);
|
|
3902
|
+
const auth = await startDeviceAuth(config);
|
|
3903
|
+
p.note(
|
|
3904
|
+
[
|
|
3905
|
+
theme.bright(`Your code: ${auth.userCode}`),
|
|
3906
|
+
`${theme.muted("Approval URL:")} ${auth.verificationUrl}`,
|
|
3907
|
+
theme.muted(
|
|
3908
|
+
"We tried to open your browser. If it didn't open, visit that link on any device and enter the code."
|
|
3909
|
+
)
|
|
3910
|
+
].join("\n"),
|
|
3911
|
+
theme.signal("Approve this device")
|
|
3912
|
+
);
|
|
3913
|
+
try {
|
|
3914
|
+
await open2(auth.verificationUrlComplete ?? auth.verificationUrl);
|
|
3915
|
+
} catch {
|
|
3916
|
+
}
|
|
3518
3917
|
const spin = p.spinner();
|
|
3519
3918
|
spin.start("Waiting for you to approve in the browser");
|
|
3520
3919
|
try {
|
|
3521
|
-
const session = await
|
|
3920
|
+
const session = await auth.poll();
|
|
3522
3921
|
spin.stop(theme.success("Authenticated \u2713"));
|
|
3523
3922
|
return session;
|
|
3524
3923
|
} catch (err) {
|
|
@@ -3588,6 +3987,10 @@ function renderEventOutcomeLines(outcomes, wiredMap) {
|
|
|
3588
3987
|
if (remainder > 0) lines.push(theme.muted(`\u2026 ${remainder} more events`));
|
|
3589
3988
|
return lines.join("\n");
|
|
3590
3989
|
}
|
|
3990
|
+
function logAgentSummary(summary) {
|
|
3991
|
+
const cleaned = scrubSummary(summary).trim();
|
|
3992
|
+
if (cleaned) p.log.message(cleaned);
|
|
3993
|
+
}
|
|
3591
3994
|
function shortPhaseLabel(phase) {
|
|
3592
3995
|
switch (phase) {
|
|
3593
3996
|
case "Mapping your codebase":
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@whisperr/wizard",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
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",
|