@whisperr/wizard 0.7.1 → 0.7.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +104 -46
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -11,7 +11,7 @@ import open2 from "open";
|
|
|
11
11
|
|
|
12
12
|
// src/core/config.ts
|
|
13
13
|
var DEFAULT_API_BASE = "https://api.whisperr.net";
|
|
14
|
-
var DEFAULT_MODEL = "claude-
|
|
14
|
+
var DEFAULT_MODEL = "claude-opus-5";
|
|
15
15
|
var DEFAULT_PLANNER_MODEL = "claude-opus-5";
|
|
16
16
|
var DEFAULT_EFFORT = "high";
|
|
17
17
|
var EFFORT_LEVELS = ["low", "medium", "high", "xhigh", "max"];
|
|
@@ -2063,11 +2063,14 @@ var SURVEY_SYSTEM_PROMPT = [
|
|
|
2063
2063
|
"",
|
|
2064
2064
|
"Report, in plain markdown:",
|
|
2065
2065
|
"1. Stack: frameworks, package manager, app entry point file.",
|
|
2066
|
-
"2. Feature areas: name, root directory, and
|
|
2066
|
+
"2. Feature areas: name, root directory, and every relevant file containing",
|
|
2067
|
+
" committed end-user actions or meaningful lifecycle transitions.",
|
|
2067
2068
|
"3. The end-user identify() anchor: the login/signup success and session-restore call sites for the",
|
|
2068
2069
|
" PAYING end user \u2014 never admin, staff, operator, or seller paths.",
|
|
2069
2070
|
"4. Existing analytics calls, if any.",
|
|
2070
|
-
"5.
|
|
2071
|
+
"5. Exhaustive inventory of integratable call sites: file, function/symbol,",
|
|
2072
|
+
" confirmed outcome or state transition, available non-PII payload fields,",
|
|
2073
|
+
" and whether similar paths exist elsewhere.",
|
|
2071
2074
|
"Cite concrete file paths for every claim. Do not propose event names yet."
|
|
2072
2075
|
].join("\n");
|
|
2073
2076
|
function surveyUserPrompt(repoPath, onboardingContext) {
|
|
@@ -2090,9 +2093,20 @@ var SELECTION_SYSTEM_PROMPT = [
|
|
|
2090
2093
|
" streak_broken) \u2014 the server derives those later.",
|
|
2091
2094
|
"- Emit after confirmed outcomes, not intent: a purchase event fires on confirmed success, not on",
|
|
2092
2095
|
" button tap; denied permissions are not grants.",
|
|
2093
|
-
"-
|
|
2094
|
-
"
|
|
2095
|
-
"
|
|
2096
|
+
"- Aim for comprehensive coverage of the product\u2019s observable end-user lifecycle.",
|
|
2097
|
+
" Inspect every meaningful feature area and propose every distinct, concretely",
|
|
2098
|
+
" integratable event that could contribute to activation, engagement, retention,",
|
|
2099
|
+
" monetization, or churn analysis.",
|
|
2100
|
+
"- Prefer complete coverage over a short curated list. Include distinct successful",
|
|
2101
|
+
" outcomes, failures, cancellations, retries, state transitions, and meaningful",
|
|
2102
|
+
" feature usage when each has its own concrete call site and analytical value.",
|
|
2103
|
+
"- Let the product\u2019s actual complexity determine the count. A feature-rich product",
|
|
2104
|
+
" may legitimately produce roughly 100\u2013200 events, while a smaller product may",
|
|
2105
|
+
" produce fewer. Do not stop after finding only the most obvious events, and do",
|
|
2106
|
+
" not invent events merely to reach a number.",
|
|
2107
|
+
"- Before finishing, systematically revisit every surveyed feature area and confirm",
|
|
2108
|
+
" that all meaningful committed actions and lifecycle transitions have either",
|
|
2109
|
+
" been proposed or deliberately excluded with a reason.",
|
|
2096
2110
|
"- Codes are lowercase snake_case, named for what happened (checkout_completed, not do_checkout).",
|
|
2097
2111
|
"- payloadSchema lists the fields worth capturing with a short description of each \u2014 the important",
|
|
2098
2112
|
" info only. NEVER include personal identifiers: no email, phone, names, addresses, birth dates,",
|
|
@@ -2230,6 +2244,7 @@ async function runSelection(provider, models, repoPath, bootstrap, surveyReport,
|
|
|
2230
2244
|
const seen = /* @__PURE__ */ new Set();
|
|
2231
2245
|
const rejected = new Set(rejectedCodes);
|
|
2232
2246
|
let summary = "";
|
|
2247
|
+
let finished = false;
|
|
2233
2248
|
const proposeTool = {
|
|
2234
2249
|
name: "propose_event",
|
|
2235
2250
|
description: "Propose one important, concretely-anchored product event. Returns ok:true when accepted.",
|
|
@@ -2291,6 +2306,7 @@ async function runSelection(provider, models, repoPath, bootstrap, surveyReport,
|
|
|
2291
2306
|
},
|
|
2292
2307
|
handler: async (input) => {
|
|
2293
2308
|
summary = String(input.summary ?? "");
|
|
2309
|
+
finished = true;
|
|
2294
2310
|
return { ok: true };
|
|
2295
2311
|
}
|
|
2296
2312
|
};
|
|
@@ -2314,6 +2330,7 @@ async function runSelection(provider, models, repoPath, bootstrap, surveyReport,
|
|
|
2314
2330
|
);
|
|
2315
2331
|
return {
|
|
2316
2332
|
outcome: outcome.kind,
|
|
2333
|
+
finished,
|
|
2317
2334
|
proposed,
|
|
2318
2335
|
summary,
|
|
2319
2336
|
surveyReport,
|
|
@@ -2596,15 +2613,24 @@ async function runEngine(input) {
|
|
|
2596
2613
|
state.tombstonedCodes,
|
|
2597
2614
|
input.sink
|
|
2598
2615
|
);
|
|
2599
|
-
if (selection.
|
|
2600
|
-
|
|
2601
|
-
|
|
2616
|
+
if (!selection.finished) {
|
|
2617
|
+
const reason = selection.outcome === "completed" ? "selection did not call finish_selection" : `selection ${selection.outcome}`;
|
|
2618
|
+
await failRun(reason);
|
|
2619
|
+
return { kind: "aborted", reason };
|
|
2620
|
+
}
|
|
2621
|
+
if (selection.proposed.length === 0) {
|
|
2622
|
+
await failRun("selection finished with no accepted events");
|
|
2623
|
+
return { kind: "aborted", reason: "selection finished with no accepted events" };
|
|
2602
2624
|
}
|
|
2603
2625
|
const approved = await input.callbacks.reviewEvents(selection.proposed);
|
|
2604
2626
|
if (approved === null) {
|
|
2605
2627
|
await failRun("selection rejected by user");
|
|
2606
2628
|
return { kind: "aborted", reason: "selection rejected by user" };
|
|
2607
2629
|
}
|
|
2630
|
+
if (approved.length === 0) {
|
|
2631
|
+
await failRun("no events accepted for instrumentation");
|
|
2632
|
+
return { kind: "aborted", reason: "no events accepted for instrumentation" };
|
|
2633
|
+
}
|
|
2608
2634
|
const approvedCodes = new Set(approved.map((event) => event.code));
|
|
2609
2635
|
for (const event of selection.proposed) {
|
|
2610
2636
|
if (!approvedCodes.has(event.code) && !state.tombstonedCodes.includes(event.code)) {
|
|
@@ -2627,25 +2653,8 @@ async function runEngine(input) {
|
|
|
2627
2653
|
input.sink.emit({ phase: "designing", action: `resumed with ${registered.length} registered events` });
|
|
2628
2654
|
}
|
|
2629
2655
|
if (registered.length === 0) {
|
|
2630
|
-
await
|
|
2631
|
-
|
|
2632
|
-
await completeRun(runs, runId, [], /* @__PURE__ */ new Map(), [], true, "unavailable", void 0);
|
|
2633
|
-
} catch (error) {
|
|
2634
|
-
const reason = error instanceof Error ? error.message : String(error);
|
|
2635
|
-
await failRun(`completion failed: ${reason}`);
|
|
2636
|
-
return { kind: "aborted", reason: `completion failed: ${reason}` };
|
|
2637
|
-
}
|
|
2638
|
-
return {
|
|
2639
|
-
kind: "completed",
|
|
2640
|
-
runId,
|
|
2641
|
-
registered,
|
|
2642
|
-
eventStatuses: /* @__PURE__ */ new Map(),
|
|
2643
|
-
reportEvents: [],
|
|
2644
|
-
changedFiles: [],
|
|
2645
|
-
identifyWired: false,
|
|
2646
|
-
applied: false,
|
|
2647
|
-
summary: "No important events found for this project; nothing was instrumented."
|
|
2648
|
-
};
|
|
2656
|
+
await failRun("no events registered for this project");
|
|
2657
|
+
return { kind: "aborted", reason: "no events registered for this project" };
|
|
2649
2658
|
}
|
|
2650
2659
|
await patchPhase("binding");
|
|
2651
2660
|
let worktree;
|
|
@@ -3188,14 +3197,35 @@ function coverageNote(coverage) {
|
|
|
3188
3197
|
|
|
3189
3198
|
// src/core/toolPolicy.ts
|
|
3190
3199
|
import { isAbsolute, relative, resolve } from "path";
|
|
3191
|
-
var SECRET_MATERIAL_DENIAL = "blocked: secret material
|
|
3200
|
+
var SECRET_MATERIAL_DENIAL = "blocked: secret material must not be accessed";
|
|
3192
3201
|
var WRITE_OUTSIDE_REPO_DENIAL = "blocked: writes must stay inside the target repository";
|
|
3193
3202
|
var SENSITIVE_WRITE_DENIAL = "blocked: refusing to write CI/git configuration";
|
|
3194
3203
|
var BASH_ALLOWLIST_DENIAL = "blocked: command is outside the Bash allowlist - use package install/add commands, mkdir, or git status/diff/log only";
|
|
3195
3204
|
var CHAINED_COMMAND_DENIAL = "blocked: chained or substituted shell command - run one command at a time";
|
|
3196
|
-
|
|
3197
|
-
|
|
3198
|
-
|
|
3205
|
+
function engineMcpToolName(toolName) {
|
|
3206
|
+
return `mcp__engine__${toolName}`;
|
|
3207
|
+
}
|
|
3208
|
+
function trustedEngineMcpTools(tools) {
|
|
3209
|
+
return new Set(tools.map((tool2) => engineMcpToolName(tool2.name)));
|
|
3210
|
+
}
|
|
3211
|
+
var SECRET_DIRECTORIES = /* @__PURE__ */ new Set([
|
|
3212
|
+
".aws",
|
|
3213
|
+
".ssh",
|
|
3214
|
+
".gnupg",
|
|
3215
|
+
".azure",
|
|
3216
|
+
".docker",
|
|
3217
|
+
".kube",
|
|
3218
|
+
".secrets",
|
|
3219
|
+
"secrets"
|
|
3220
|
+
]);
|
|
3221
|
+
var SECRET_EXACT_FILES = /* @__PURE__ */ new Set([
|
|
3222
|
+
".netrc",
|
|
3223
|
+
".npmrc",
|
|
3224
|
+
".pypirc",
|
|
3225
|
+
"auth.json",
|
|
3226
|
+
"google-services.json",
|
|
3227
|
+
"googleservice-info.plist"
|
|
3228
|
+
]);
|
|
3199
3229
|
var SENSITIVE_WRITE_DIRECTORIES = /* @__PURE__ */ new Set([".git", ".circleci", ".buildkite"]);
|
|
3200
3230
|
var SENSITIVE_WRITE_EXACT_FILES = /* @__PURE__ */ new Set([
|
|
3201
3231
|
".gitlab-ci.yml",
|
|
@@ -3211,12 +3241,16 @@ var SECRET_SUFFIXES = [
|
|
|
3211
3241
|
".pfx",
|
|
3212
3242
|
".jks",
|
|
3213
3243
|
".keystore",
|
|
3244
|
+
".kdbx",
|
|
3214
3245
|
".tfvars"
|
|
3215
3246
|
];
|
|
3216
3247
|
var JS_PACKAGE_MANAGERS = /* @__PURE__ */ new Set(["npm", "pnpm", "yarn", "bun"]);
|
|
3217
3248
|
var PYTHON_PACKAGE_MANAGERS = /* @__PURE__ */ new Set(["pip", "pip3", "poetry", "uv"]);
|
|
3218
3249
|
var RUBY_PACKAGE_MANAGERS = /* @__PURE__ */ new Set(["gem", "bundle"]);
|
|
3219
3250
|
function evaluateToolUse(toolName, input, context) {
|
|
3251
|
+
if (context.trustedTools?.has(toolName)) {
|
|
3252
|
+
return { behavior: "allow" };
|
|
3253
|
+
}
|
|
3220
3254
|
switch (toolName) {
|
|
3221
3255
|
case "Read":
|
|
3222
3256
|
return evaluateReadLike(readPathInputs(input), readPathInputs(input), context);
|
|
@@ -3241,7 +3275,6 @@ function isSecretPathLike(value) {
|
|
|
3241
3275
|
for (const rawPart of parts) {
|
|
3242
3276
|
const part = stripOuterQuotes(rawPart.trim().toLowerCase());
|
|
3243
3277
|
if (!part || part === "." || part === "**") continue;
|
|
3244
|
-
if (ENV_EXAMPLES.has(part)) continue;
|
|
3245
3278
|
if (SECRET_DIRECTORIES.has(part)) return true;
|
|
3246
3279
|
if (SECRET_EXACT_FILES.has(part)) return true;
|
|
3247
3280
|
if (part === ".env" || part === ".envrc" || part.startsWith(".env.") || part.startsWith(".env*")) {
|
|
@@ -3252,6 +3285,7 @@ function isSecretPathLike(value) {
|
|
|
3252
3285
|
}
|
|
3253
3286
|
if (part.includes("credentials")) return true;
|
|
3254
3287
|
if (part.startsWith("secrets.")) return true;
|
|
3288
|
+
if (part.startsWith("service-account") && part.endsWith(".json")) return true;
|
|
3255
3289
|
if (SECRET_SUFFIXES.some((suffix) => part.endsWith(suffix))) return true;
|
|
3256
3290
|
}
|
|
3257
3291
|
return false;
|
|
@@ -3287,6 +3321,9 @@ function evaluateWrite(input, context) {
|
|
|
3287
3321
|
if (!isPathInsideRepo(pathValue, context.repoPath)) {
|
|
3288
3322
|
return { behavior: "deny", message: WRITE_OUTSIDE_REPO_DENIAL };
|
|
3289
3323
|
}
|
|
3324
|
+
if (isSecretPathLike(pathValue)) {
|
|
3325
|
+
return { behavior: "deny", message: SECRET_MATERIAL_DENIAL };
|
|
3326
|
+
}
|
|
3290
3327
|
if (isSensitiveWritePath(pathValue, context.repoPath)) {
|
|
3291
3328
|
return { behavior: "deny", message: SENSITIVE_WRITE_DENIAL };
|
|
3292
3329
|
}
|
|
@@ -4248,7 +4285,7 @@ async function runPass(opts) {
|
|
|
4248
4285
|
}
|
|
4249
4286
|
return { summary, costUsd, ok, maxedOut };
|
|
4250
4287
|
}
|
|
4251
|
-
function buildToolPolicyHooks(repoPath) {
|
|
4288
|
+
function buildToolPolicyHooks(repoPath, trustedTools) {
|
|
4252
4289
|
return {
|
|
4253
4290
|
PreToolUse: [
|
|
4254
4291
|
{
|
|
@@ -4256,7 +4293,8 @@ function buildToolPolicyHooks(repoPath) {
|
|
|
4256
4293
|
async (input) => {
|
|
4257
4294
|
if (input.hook_event_name !== "PreToolUse") return { continue: true };
|
|
4258
4295
|
const decision = evaluateToolUse(input.tool_name, toolInputRecord(input.tool_input), {
|
|
4259
|
-
repoPath
|
|
4296
|
+
repoPath,
|
|
4297
|
+
trustedTools
|
|
4260
4298
|
});
|
|
4261
4299
|
if (decision.behavior === "allow") {
|
|
4262
4300
|
return { continue: true };
|
|
@@ -4274,9 +4312,9 @@ function buildToolPolicyHooks(repoPath) {
|
|
|
4274
4312
|
]
|
|
4275
4313
|
};
|
|
4276
4314
|
}
|
|
4277
|
-
function createToolPermissionCallback(repoPath) {
|
|
4315
|
+
function createToolPermissionCallback(repoPath, trustedTools) {
|
|
4278
4316
|
return async (toolName, input, options) => {
|
|
4279
|
-
const decision = evaluateToolUse(toolName, input, { repoPath });
|
|
4317
|
+
const decision = evaluateToolUse(toolName, input, { repoPath, trustedTools });
|
|
4280
4318
|
return decision.behavior === "allow" ? { behavior: "allow", toolUseID: options.toolUseID } : {
|
|
4281
4319
|
behavior: "deny",
|
|
4282
4320
|
message: decision.message,
|
|
@@ -4344,6 +4382,10 @@ function short(s, max = 60) {
|
|
|
4344
4382
|
// src/engine/providers/claude.ts
|
|
4345
4383
|
var READ_ONLY_TOOLS2 = ["Read", "Glob", "Grep"];
|
|
4346
4384
|
var EDIT_TOOLS = ["Read", "Edit", "Write", "Bash", "Glob", "Grep"];
|
|
4385
|
+
var FAST_MODE_MODELS = /* @__PURE__ */ new Set(["claude-opus-5", "claude-opus-4-8"]);
|
|
4386
|
+
function supportsClaudeFastMode(model) {
|
|
4387
|
+
return FAST_MODE_MODELS.has(model);
|
|
4388
|
+
}
|
|
4347
4389
|
function createClaudeProvider(config, session) {
|
|
4348
4390
|
return {
|
|
4349
4391
|
name: "claude",
|
|
@@ -4355,9 +4397,10 @@ function createClaudeProvider(config, session) {
|
|
|
4355
4397
|
return { content: [{ type: "text", text: JSON.stringify(result) }] };
|
|
4356
4398
|
})
|
|
4357
4399
|
);
|
|
4400
|
+
const trustedTools = trustedEngineMcpTools(invocation.tools);
|
|
4358
4401
|
const allowedTools = [
|
|
4359
4402
|
...invocation.allowRepoWrite ? EDIT_TOOLS : READ_ONLY_TOOLS2,
|
|
4360
|
-
...
|
|
4403
|
+
...trustedTools
|
|
4361
4404
|
];
|
|
4362
4405
|
let sessionId = invocation.resumeSessionId;
|
|
4363
4406
|
let costUsd = 0;
|
|
@@ -4375,9 +4418,10 @@ function createClaudeProvider(config, session) {
|
|
|
4375
4418
|
...roleConfig.effort ? { effort: roleConfig.effort } : {},
|
|
4376
4419
|
tools: [...allowedTools],
|
|
4377
4420
|
...hostTools.length > 0 ? { mcpServers: { engine: createSdkMcpServer({ name: "engine", tools: hostTools }) } } : {},
|
|
4378
|
-
hooks: buildToolPolicyHooks(invocation.cwd),
|
|
4379
|
-
canUseTool: createToolPermissionCallback(invocation.cwd),
|
|
4421
|
+
hooks: buildToolPolicyHooks(invocation.cwd, trustedTools),
|
|
4422
|
+
canUseTool: createToolPermissionCallback(invocation.cwd, trustedTools),
|
|
4380
4423
|
maxTurns: roleConfig.maxTurns,
|
|
4424
|
+
...supportsClaudeFastMode(roleConfig.model) ? { settings: { fastMode: true } } : {},
|
|
4381
4425
|
...invocation.resumeSessionId ? { resume: invocation.resumeSessionId } : {},
|
|
4382
4426
|
settingSources: []
|
|
4383
4427
|
}
|
|
@@ -4438,6 +4482,18 @@ function jailedPath(cwd, candidate) {
|
|
|
4438
4482
|
}
|
|
4439
4483
|
return resolved;
|
|
4440
4484
|
}
|
|
4485
|
+
function guardedPath(cwd, toolName, candidate) {
|
|
4486
|
+
const path = jailedPath(cwd, candidate);
|
|
4487
|
+
const decision = evaluateToolUse(
|
|
4488
|
+
toolName,
|
|
4489
|
+
toolName === "Glob" ? { path: candidate, pattern: "*" } : { file_path: candidate },
|
|
4490
|
+
{ repoPath: cwd }
|
|
4491
|
+
);
|
|
4492
|
+
if (decision.behavior === "deny") {
|
|
4493
|
+
throw new Error(decision.message);
|
|
4494
|
+
}
|
|
4495
|
+
return path;
|
|
4496
|
+
}
|
|
4441
4497
|
function fileTools(cwd, allowWrite) {
|
|
4442
4498
|
const tools = [
|
|
4443
4499
|
{
|
|
@@ -4450,7 +4506,8 @@ function fileTools(cwd, allowWrite) {
|
|
|
4450
4506
|
required: ["path"]
|
|
4451
4507
|
},
|
|
4452
4508
|
handler: async (input) => {
|
|
4453
|
-
const
|
|
4509
|
+
const path = guardedPath(cwd, "Read", String(input.path ?? ""));
|
|
4510
|
+
const content = readFileSync3(path, "utf8");
|
|
4454
4511
|
return { content: content.slice(0, 4e4) };
|
|
4455
4512
|
}
|
|
4456
4513
|
},
|
|
@@ -4464,8 +4521,8 @@ function fileTools(cwd, allowWrite) {
|
|
|
4464
4521
|
required: ["path"]
|
|
4465
4522
|
},
|
|
4466
4523
|
handler: async (input) => {
|
|
4467
|
-
const target9 =
|
|
4468
|
-
const entries = readdirSync(target9).map((entry) => {
|
|
4524
|
+
const target9 = guardedPath(cwd, "Glob", String(input.path ?? "."));
|
|
4525
|
+
const entries = readdirSync(target9).filter((entry) => !isSecretPathLike(entry)).map((entry) => {
|
|
4469
4526
|
const kind = statSync(join8(target9, entry)).isDirectory() ? "dir" : "file";
|
|
4470
4527
|
return `${kind}:${entry}`;
|
|
4471
4528
|
});
|
|
@@ -4485,7 +4542,8 @@ function fileTools(cwd, allowWrite) {
|
|
|
4485
4542
|
required: ["path", "content"]
|
|
4486
4543
|
},
|
|
4487
4544
|
handler: async (input) => {
|
|
4488
|
-
|
|
4545
|
+
const path = guardedPath(cwd, "Write", String(input.path ?? ""));
|
|
4546
|
+
writeFileSync2(path, String(input.content ?? ""));
|
|
4489
4547
|
return { ok: true };
|
|
4490
4548
|
}
|
|
4491
4549
|
},
|
|
@@ -4503,7 +4561,7 @@ function fileTools(cwd, allowWrite) {
|
|
|
4503
4561
|
required: ["path", "oldText", "newText"]
|
|
4504
4562
|
},
|
|
4505
4563
|
handler: async (input) => {
|
|
4506
|
-
const path =
|
|
4564
|
+
const path = guardedPath(cwd, "Edit", String(input.path ?? ""));
|
|
4507
4565
|
const content = readFileSync3(path, "utf8");
|
|
4508
4566
|
const oldText = String(input.oldText ?? "");
|
|
4509
4567
|
if (!content.includes(oldText)) {
|
|
@@ -4673,7 +4731,7 @@ var MINUTE = 6e4;
|
|
|
4673
4731
|
function engineModelConfig(config) {
|
|
4674
4732
|
const model = (role, fallback) => process.env[`WHISPERR_WIZARD_ENGINE_${role.toUpperCase()}_MODEL`]?.trim() || fallback;
|
|
4675
4733
|
return {
|
|
4676
|
-
survey: { model: model("survey",
|
|
4734
|
+
survey: { model: model("survey", config.model), effort: "medium", maxTurns: 40, maxMs: 12 * MINUTE },
|
|
4677
4735
|
design: { model: model("design", config.plannerModel), effort: "high", maxTurns: 40, maxMs: 15 * MINUTE },
|
|
4678
4736
|
edit: { model: model("edit", config.model), effort: config.effort, maxTurns: 50, maxMs: 15 * MINUTE },
|
|
4679
4737
|
review: { model: model("review", config.model), effort: "medium", maxTurns: 15, maxMs: 8 * MINUTE }
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@whisperr/wizard",
|
|
3
|
-
"version": "0.7.
|
|
3
|
+
"version": "0.7.3",
|
|
4
4
|
"description": "Whisperr Wizard \u2014 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",
|