@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.
Files changed (2) hide show
  1. package/dist/index.js +104 -46
  2. 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-sonnet-5";
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 the 1-3 files where committed user actions happen.",
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. Committed-action call sites worth instrumenting: file, function/symbol, and what the user just did.",
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
- "- Importance is anchored in the business context: the activation moment, churn-risk and healthy",
2094
- " signals, and how the product charges. A handful of load-bearing events beats wide coverage.",
2095
- "- An empty or small set is a valid outcome. There is no quota. Never invent events to look thorough.",
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.outcome !== "completed" && selection.proposed.length === 0) {
2600
- await failRun(`selection ${selection.outcome}`);
2601
- return { kind: "aborted", reason: `selection ${selection.outcome}` };
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 patchPhase("reporting", "no events registered for this project");
2631
- try {
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 - reference the variable name in .env.example instead of reading the real file";
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
- var ENV_EXAMPLES = /* @__PURE__ */ new Set([".env.example", ".env.sample", ".env.template"]);
3197
- var SECRET_DIRECTORIES = /* @__PURE__ */ new Set([".aws", ".ssh", ".gnupg"]);
3198
- var SECRET_EXACT_FILES = /* @__PURE__ */ new Set([".netrc", ".npmrc", ".pypirc"]);
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
- ...invocation.tools.map((engineTool) => `mcp__engine__${engineTool.name}`)
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 content = readFileSync3(jailedPath(cwd, String(input.path ?? "")), "utf8");
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 = jailedPath(cwd, String(input.path ?? "."));
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
- writeFileSync2(jailedPath(cwd, String(input.path ?? "")), String(input.content ?? ""));
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 = jailedPath(cwd, String(input.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", "claude-sonnet-5"), effort: "medium", maxTurns: 40, maxMs: 12 * MINUTE },
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.1",
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",