@whisperr/wizard 0.7.0 → 0.7.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.
Files changed (2) hide show
  1. package/dist/index.js +157 -77
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -1969,10 +1969,10 @@ var RunsClient = class {
1969
1969
  return payload;
1970
1970
  }
1971
1971
  createOrResumeRun(input) {
1972
- return this.request("POST", "/wizard/runs", { ...input });
1972
+ return this.request("POST", "/wizard/runs", { ...input }).then(normalizeRunBootstrap);
1973
1973
  }
1974
1974
  getRun(runId) {
1975
- return this.request("GET", `/wizard/runs/${runId}`);
1975
+ return this.request("GET", `/wizard/runs/${runId}`).then(normalizeRunBootstrap);
1976
1976
  }
1977
1977
  patchRun(runId, patch) {
1978
1978
  return this.request("PATCH", `/wizard/runs/${runId}`, { ...patch });
@@ -1983,7 +1983,7 @@ var RunsClient = class {
1983
1983
  idempotencyKey: itemIdempotencyKey(kind, code, idempotencyExtra),
1984
1984
  kind,
1985
1985
  payload
1986
- });
1986
+ }).then(normalizeItemWriteResult);
1987
1987
  }
1988
1988
  completeRun(runId, completion) {
1989
1989
  return this.request(
@@ -1993,6 +1993,64 @@ var RunsClient = class {
1993
1993
  );
1994
1994
  }
1995
1995
  };
1996
+ function normalizeItemWriteResult(payload) {
1997
+ const value = record(payload);
1998
+ const item = record(value?.item);
1999
+ const id = stringValue(value?.id) || stringValue(item?.id);
2000
+ if (!value || value.kind !== "event" || typeof value.created !== "boolean" || !id) {
2001
+ throw new RunsApiError("runs API returned an invalid item write response");
2002
+ }
2003
+ return {
2004
+ kind: "event",
2005
+ created: value.created,
2006
+ id,
2007
+ ...typeof value.code === "string" ? { code: value.code } : {},
2008
+ ...item && typeof item.id === "string" ? { item: { id: item.id, ...typeof item.code === "string" ? { code: item.code } : {} } } : {}
2009
+ };
2010
+ }
2011
+ function normalizeRunBootstrap(payload) {
2012
+ const value = record(payload);
2013
+ if (!value) {
2014
+ throw new RunsApiError("runs API returned an invalid run bootstrap response");
2015
+ }
2016
+ const snapshotSource = record(value.snapshot) ?? value;
2017
+ const events = Array.isArray(snapshotSource.events) ? snapshotSource.events : void 0;
2018
+ const run3 = record(value.run);
2019
+ const project = record(value.project);
2020
+ const ingestion = record(value.ingestion);
2021
+ if (!run3 || typeof run3.id !== "string" || !project || typeof project.id !== "string" || !events || events.some((event) => !isSnapshotEvent(event)) || !ingestion || typeof ingestion.apiKey !== "string" || typeof ingestion.baseUrl !== "string") {
2022
+ throw new RunsApiError("runs API returned an invalid run bootstrap response");
2023
+ }
2024
+ return {
2025
+ project: {
2026
+ id: project.id,
2027
+ repoFingerprint: stringValue(project.repoFingerprint),
2028
+ displayName: stringValue(project.displayName),
2029
+ target: stringValue(project.target),
2030
+ kind: stringValue(project.kind)
2031
+ },
2032
+ run: { id: run3.id, status: stringValue(run3.status), phase: stringValue(run3.phase) },
2033
+ resumed: value.resumed === true,
2034
+ universeMode: stringValue(value.universeMode),
2035
+ ...record(value.onboardingContext) ? { onboardingContext: record(value.onboardingContext) } : {},
2036
+ app: record(value.app) ?? {},
2037
+ snapshot: {
2038
+ setupStatus: stringValue(snapshotSource.setupStatus),
2039
+ events
2040
+ },
2041
+ ingestion: { apiKey: ingestion.apiKey, baseUrl: ingestion.baseUrl }
2042
+ };
2043
+ }
2044
+ function record(value) {
2045
+ return typeof value === "object" && value !== null ? value : void 0;
2046
+ }
2047
+ function stringValue(value) {
2048
+ return typeof value === "string" ? value : "";
2049
+ }
2050
+ function isSnapshotEvent(value) {
2051
+ const event = record(value);
2052
+ return !!event && typeof event.id === "string" && typeof event.code === "string" && typeof event.name === "string" && typeof event.reasoning === "string" && typeof event.projectId === "string" && !!record(event.payloadSchema);
2053
+ }
1996
2054
 
1997
2055
  // src/engine/selection.ts
1998
2056
  import { z } from "zod";
@@ -2271,8 +2329,7 @@ async function registerApprovedEvents(runs, runId, approved, sink) {
2271
2329
  reasoning: event.reasoning,
2272
2330
  ...Object.keys(event.payloadSchema).length > 0 ? { payloadSchema: event.payloadSchema } : {}
2273
2331
  });
2274
- const item = result.item;
2275
- registered.push({ ...event, eventId: item?.id ?? result.id ?? "" });
2332
+ registered.push({ ...event, eventId: result.id || result.item?.id || "" });
2276
2333
  sink.emit({ phase: "persisting", file: event.anchorFile, action: `registered ${event.code}` });
2277
2334
  }
2278
2335
  return registered;
@@ -2290,7 +2347,7 @@ function placementsFor(registered) {
2290
2347
  // src/engine/state.ts
2291
2348
  import { createHash as createHash3 } from "crypto";
2292
2349
  import { mkdirSync, readFileSync as readFileSync2, renameSync, writeFileSync } from "fs";
2293
- import { homedir } from "os";
2350
+ import { homedir, tmpdir } from "os";
2294
2351
  import { join as join4 } from "path";
2295
2352
 
2296
2353
  // src/engine/types.ts
@@ -2310,10 +2367,16 @@ var ENGINE_PHASES = [
2310
2367
  // src/engine/state.ts
2311
2368
  function stateDirectory() {
2312
2369
  const override = process.env.WHISPERR_WIZARD_STATE_DIR?.trim();
2313
- if (override) {
2314
- return override;
2370
+ const preferred = override || join4(homedir(), ".whisperr", "wizard-runs");
2371
+ if (canCreateStateDirectory(preferred)) {
2372
+ return preferred;
2315
2373
  }
2316
- return join4(homedir(), ".whisperr", "wizard-runs");
2374
+ const user = typeof process.getuid === "function" ? process.getuid() : "user";
2375
+ const fallback = join4(tmpdir(), `whisperr-${user}`, "wizard-runs");
2376
+ if (canCreateStateDirectory(fallback)) {
2377
+ return fallback;
2378
+ }
2379
+ throw new Error("Whisperr Wizard could not create a directory for run state");
2317
2380
  }
2318
2381
  function stateKey(apiBaseUrl, appId, repoFingerprint2) {
2319
2382
  return createHash3("sha256").update(`${apiBaseUrl}
@@ -2324,13 +2387,21 @@ function statePath(apiBaseUrl, appId, repoFingerprint2) {
2324
2387
  return join4(stateDirectory(), `${stateKey(apiBaseUrl, appId, repoFingerprint2)}.json`);
2325
2388
  }
2326
2389
  function saveRunState(state) {
2327
- const path = statePath(state.apiBaseUrl, state.appId, state.repoFingerprint);
2328
- mkdirSync(stateDirectory(), { recursive: true, mode: 448 });
2390
+ const directory = stateDirectory();
2391
+ const path = join4(directory, `${stateKey(state.apiBaseUrl, state.appId, state.repoFingerprint)}.json`);
2329
2392
  const payload = JSON.stringify({ ...state, updatedAt: (/* @__PURE__ */ new Date()).toISOString() }, null, 2);
2330
2393
  const temporary = `${path}.tmp`;
2331
2394
  writeFileSync(temporary, payload, { mode: 384 });
2332
2395
  renameSync(temporary, path);
2333
2396
  }
2397
+ function canCreateStateDirectory(directory) {
2398
+ try {
2399
+ mkdirSync(directory, { recursive: true, mode: 448 });
2400
+ return true;
2401
+ } catch {
2402
+ return false;
2403
+ }
2404
+ }
2334
2405
  function loadRunState(apiBaseUrl, appId, repoFingerprint2) {
2335
2406
  const path = statePath(apiBaseUrl, appId, repoFingerprint2);
2336
2407
  let raw;
@@ -2364,7 +2435,7 @@ function isEngineRunState(value) {
2364
2435
  // src/engine/worktree.ts
2365
2436
  import { spawn as spawn2 } from "child_process";
2366
2437
  import { mkdtemp, writeFile as writeFile2 } from "fs/promises";
2367
- import { tmpdir } from "os";
2438
+ import { tmpdir as tmpdir2 } from "os";
2368
2439
  import { join as join5 } from "path";
2369
2440
  var WorktreeError = class extends Error {
2370
2441
  constructor(code, message) {
@@ -2409,7 +2480,7 @@ async function createWorktree(repoPath, force = false) {
2409
2480
  );
2410
2481
  }
2411
2482
  const baseRef = (await must(repoPath, ["rev-parse", "HEAD"])).trim();
2412
- const path = await mkdtemp(join5(tmpdir(), "whisperr-worktree-"));
2483
+ const path = await mkdtemp(join5(tmpdir2(), "whisperr-worktree-"));
2413
2484
  await must(repoPath, ["worktree", "add", "--detach", "--force", path, "HEAD"]);
2414
2485
  return { path, baseRef, repoPath };
2415
2486
  }
@@ -2485,73 +2556,85 @@ async function runEngine(input) {
2485
2556
  } catch {
2486
2557
  }
2487
2558
  };
2488
- let state = loadRunState(input.config.apiBaseUrl, input.session.appId, input.repoFingerprint) ?? freshState(input, runId);
2489
- if (state.runId !== runId) {
2490
- state = freshState(input, runId);
2559
+ let state;
2560
+ try {
2561
+ state = loadRunState(input.config.apiBaseUrl, input.session.appId, input.repoFingerprint) ?? freshState(input, runId);
2562
+ if (state.runId !== runId) {
2563
+ state = freshState(input, runId);
2564
+ }
2565
+ } catch (error) {
2566
+ const reason = error instanceof Error ? error.message : String(error);
2567
+ await failRun(`state initialization failed: ${reason}`);
2568
+ return { kind: "aborted", reason: `state initialization failed: ${reason}` };
2491
2569
  }
2492
2570
  const persist = () => saveRunState(state);
2493
2571
  let registered;
2494
2572
  if (!state.designComplete) {
2495
- await patchPhase("surveying");
2496
- const survey = await runSurvey(
2497
- input.provider,
2498
- input.models,
2499
- input.repoPath,
2500
- bootstrap,
2501
- input.sink,
2502
- state.providerSessions.survey
2503
- );
2504
- if (survey.outcome.kind !== "completed") {
2505
- await failRun(`survey ${survey.outcome.kind}`);
2506
- return { kind: "aborted", reason: `survey ${survey.outcome.kind}` };
2507
- }
2508
- state.providerSessions.survey = survey.outcome.sessionId;
2509
- persist();
2510
- await patchPhase("designing");
2511
- const selection = await runSelection(
2512
- input.provider,
2513
- input.models,
2514
- input.repoPath,
2515
- bootstrap,
2516
- survey.report,
2517
- state.tombstonedCodes,
2518
- input.sink
2519
- );
2520
- if (selection.outcome !== "completed" && selection.proposed.length === 0) {
2521
- await failRun(`selection ${selection.outcome}`);
2522
- return { kind: "aborted", reason: `selection ${selection.outcome}` };
2523
- }
2524
- const approved = await input.callbacks.reviewEvents(selection.proposed);
2525
- if (approved === null) {
2526
- await failRun("selection rejected by user");
2527
- return { kind: "aborted", reason: "selection rejected by user" };
2528
- }
2529
- const approvedCodes = new Set(approved.map((event) => event.code));
2530
- for (const event of selection.proposed) {
2531
- if (!approvedCodes.has(event.code) && !state.tombstonedCodes.includes(event.code)) {
2532
- state.tombstonedCodes.push(event.code);
2533
- }
2534
- }
2535
- persist();
2536
- await patchPhase("persisting", `registering ${approved.length} events`);
2537
2573
  try {
2574
+ await patchPhase("surveying");
2575
+ const survey = await runSurvey(
2576
+ input.provider,
2577
+ input.models,
2578
+ input.repoPath,
2579
+ bootstrap,
2580
+ input.sink,
2581
+ state.providerSessions.survey
2582
+ );
2583
+ if (survey.outcome.kind !== "completed") {
2584
+ await failRun(`survey ${survey.outcome.kind}`);
2585
+ return { kind: "aborted", reason: `survey ${survey.outcome.kind}` };
2586
+ }
2587
+ state.providerSessions.survey = survey.outcome.sessionId;
2588
+ persist();
2589
+ await patchPhase("designing");
2590
+ const selection = await runSelection(
2591
+ input.provider,
2592
+ input.models,
2593
+ input.repoPath,
2594
+ bootstrap,
2595
+ survey.report,
2596
+ state.tombstonedCodes,
2597
+ input.sink
2598
+ );
2599
+ if (selection.outcome !== "completed" && selection.proposed.length === 0) {
2600
+ await failRun(`selection ${selection.outcome}`);
2601
+ return { kind: "aborted", reason: `selection ${selection.outcome}` };
2602
+ }
2603
+ const approved = await input.callbacks.reviewEvents(selection.proposed);
2604
+ if (approved === null) {
2605
+ await failRun("selection rejected by user");
2606
+ return { kind: "aborted", reason: "selection rejected by user" };
2607
+ }
2608
+ const approvedCodes = new Set(approved.map((event) => event.code));
2609
+ for (const event of selection.proposed) {
2610
+ if (!approvedCodes.has(event.code) && !state.tombstonedCodes.includes(event.code)) {
2611
+ state.tombstonedCodes.push(event.code);
2612
+ }
2613
+ }
2614
+ persist();
2615
+ await patchPhase("persisting", `registering ${approved.length} events`);
2538
2616
  registered = await registerApprovedEvents(runs, runId, approved, input.sink);
2617
+ state.designComplete = true;
2618
+ state.clusters = deriveClusters(placementsFor(registered));
2619
+ persist();
2539
2620
  } catch (error) {
2540
2621
  const reason = error instanceof Error ? error.message : String(error);
2541
- await failRun(`event registration failed: ${reason}`);
2542
- persist();
2543
- return { kind: "aborted", reason: `event registration failed: ${reason}` };
2622
+ await failRun(`design failed: ${reason}`);
2623
+ return { kind: "aborted", reason: `design failed: ${reason}` };
2544
2624
  }
2545
- state.designComplete = true;
2546
- state.clusters = deriveClusters(placementsFor(registered));
2547
- persist();
2548
2625
  } else {
2549
2626
  registered = rebuildRegistered(state, bootstrap);
2550
2627
  input.sink.emit({ phase: "designing", action: `resumed with ${registered.length} registered events` });
2551
2628
  }
2552
2629
  if (registered.length === 0) {
2553
2630
  await patchPhase("reporting", "no events registered for this project");
2554
- await completeRun(runs, runId, [], /* @__PURE__ */ new Map(), [], true, "unavailable", void 0);
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
+ }
2555
2638
  return {
2556
2639
  kind: "completed",
2557
2640
  runId,
@@ -2713,16 +2796,13 @@ function buildEvidence(registered, statuses, changedFiles2) {
2713
2796
  });
2714
2797
  }
2715
2798
  async function completeRun(runs, runId, evidence, _statuses, changedFiles2, identifyWired, verificationStatus, verificationCommand2) {
2716
- try {
2717
- await runs.completeRun(runId, {
2718
- changedFiles: changedFiles2,
2719
- identifyWired,
2720
- verificationStatus,
2721
- ...verificationCommand2 ? { verificationCommand: verificationCommand2 } : {},
2722
- events: evidence
2723
- });
2724
- } catch {
2725
- }
2799
+ await runs.completeRun(runId, {
2800
+ changedFiles: changedFiles2,
2801
+ identifyWired,
2802
+ verificationStatus,
2803
+ ...verificationCommand2 ? { verificationCommand: verificationCommand2 } : {},
2804
+ events: evidence
2805
+ });
2726
2806
  }
2727
2807
  function buildReportEvents(registered, statuses) {
2728
2808
  return registered.map((event) => {
@@ -4798,8 +4878,8 @@ async function fetchSuggestions(config, session, statuses) {
4798
4878
  }
4799
4879
  function isSuggestionRecord(value) {
4800
4880
  if (typeof value !== "object" || value === null) return false;
4801
- const record = value;
4802
- 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;
4881
+ const record2 = value;
4882
+ return typeof record2.id === "string" && (record2.kind === "event" || record2.kind === "intervention") && typeof record2.code === "string" && (record2.status === "proposed" || record2.status === "approved" || record2.status === "rejected" || record2.status === "integrated") && typeof record2.payload === "object" && record2.payload !== null;
4803
4883
  }
4804
4884
  async function markSuggestionIntegrated(config, session, id, target9, repoFingerprint2) {
4805
4885
  if (config.offline) return false;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@whisperr/wizard",
3
- "version": "0.7.0",
3
+ "version": "0.7.1",
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",