@whisperr/wizard 0.5.0 → 0.5.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 (3) hide show
  1. package/README.md +7 -0
  2. package/dist/index.js +645 -119
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -60,6 +60,13 @@ Development overrides:
60
60
  - `WHISPERR_WIZARD_EXPLORER_MODEL`
61
61
  - `WHISPERR_WIZARD_EXPLORER_EFFORT`
62
62
  - `WHISPERR_WIZARD_DIRECT_OPENAI_KEY` or `OPENAI_API_KEY`
63
+ - `WHISPERR_WIZARD_LOG_DIR` to place private diagnostic logs in a specific directory outside the target repository
64
+
65
+ ## Diagnostics
66
+
67
+ Each wizard invocation prints the path to one private JSONL diagnostic file. By default it is stored under the wizard state directory's `logs` folder, outside the target repository. The directory is mode `0700` and each file is mode `0600`. Filenames contain only a timestamp and random invocation ID.
68
+
69
+ Diagnostics contain bounded lifecycle, progress, Git-safety, coverage, first-event, and HTTP timing/status metadata. They do not contain request or response bodies or headers, prompts, repository source, model output or reasoning, subprocess output, environment contents, authorization codes or URLs, or credentials. Known credentials are registered for exact redaction as they become available, and common token/key formats are also scrubbed.
63
70
 
64
71
  ## Safety
65
72
 
package/dist/index.js CHANGED
@@ -1,13 +1,13 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  // src/index.ts
4
- import { readFileSync, realpathSync } from "fs";
5
- import { dirname as dirname4, join as join7 } from "path";
4
+ import { readFileSync, realpathSync as realpathSync2 } from "fs";
5
+ import { dirname as dirname5, join as join8, resolve as resolve5 } from "path";
6
6
  import { fileURLToPath } from "url";
7
7
 
8
8
  // src/cli.ts
9
9
  import { readFile as readFile6 } from "fs/promises";
10
- import { join as join6, resolve as resolve3 } from "path";
10
+ import { join as join7, resolve as resolve4 } from "path";
11
11
  import * as p from "@clack/prompts";
12
12
  import open3 from "open";
13
13
 
@@ -273,14 +273,14 @@ function readStringLiteral(content, start, quote) {
273
273
  return { value, end: content.length };
274
274
  }
275
275
  function run(cwd, args) {
276
- return new Promise((resolve4) => {
276
+ return new Promise((resolve6) => {
277
277
  const child = spawn("git", args, { cwd });
278
278
  let stdout = "";
279
279
  let stderr = "";
280
280
  child.stdout.on("data", (d) => stdout += d.toString());
281
281
  child.stderr.on("data", (d) => stderr += d.toString());
282
- child.on("close", (code2) => resolve4({ ok: code2 === 0, stdout, stderr }));
283
- child.on("error", () => resolve4({ ok: false, stdout, stderr }));
282
+ child.on("close", (code2) => resolve6({ ok: code2 === 0, stdout, stderr }));
283
+ child.on("error", () => resolve6({ ok: false, stdout, stderr }));
284
284
  });
285
285
  }
286
286
 
@@ -288,14 +288,20 @@ function run(cwd, args) {
288
288
  import { spawn as spawn2 } from "child_process";
289
289
 
290
290
  // src/core/scrub.ts
291
+ import { stripVTControlCharacters } from "util";
291
292
  var SECRET_PATTERNS = [
292
293
  /sk-[A-Za-z0-9_-]{12,}/g,
293
294
  /Bearer\s+[A-Za-z0-9._~+\/-]+/gi,
294
- /\b(?:OPENAI_API_KEY|WHISPERR_WIZARD_DIRECT_OPENAI_KEY|WHISPERR_INGESTION_KEY)\s*=\s*["']?[^"'\s]+["']?/gi
295
+ /\beyJ[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\b/g,
296
+ /-----BEGIN(?: [A-Z0-9]+)? PRIVATE KEY-----[\s\S]*?(?:-----END(?: [A-Z0-9]+)? PRIVATE KEY-----|$)/gi,
297
+ /["']?\b(?:[A-Z0-9_]*(?:API[_-]?KEY|TOKEN|SECRET|PASSWORD|PASSWD|ACCESS[_-]?KEY|AUTH)[A-Z0-9_]*)\b["']?\s*(?:=|:)\s*["']?[^"'\s,;}]+["']?/gi
295
298
  ];
296
299
  function scrubError(error, secrets = []) {
297
- let text = error instanceof Error ? error.message : String(error);
298
- text = stripTerminalControls(text);
300
+ const text = error instanceof Error ? error.message : String(error);
301
+ return scrubText(text, secrets, 2e3);
302
+ }
303
+ function scrubText(value, secrets = [], maxLength = 2e3) {
304
+ let text = stripTerminalControls(value);
299
305
  for (const secret of secrets) {
300
306
  if (secret) text = text.replaceAll(secret, "[redacted]");
301
307
  }
@@ -305,7 +311,12 @@ function scrubError(error, secrets = []) {
305
311
  return assignment >= 0 ? `${match.slice(0, assignment + 1)}[redacted]` : "[redacted]";
306
312
  });
307
313
  }
308
- return text.slice(0, 2e3).trim();
314
+ text = text.replace(
315
+ /\b(https?:\/\/)([^\s\/@]+):([^\s@\/]+)@([^\s?#]+)([^\s]*)/gi,
316
+ "$1[redacted]@$4$5"
317
+ );
318
+ text = text.replace(/\b(https?:\/\/[^\s?#]+)\?[^\s#]*/gi, "$1?[redacted]");
319
+ return text.slice(0, maxLength).trim();
309
320
  }
310
321
  function sanitizedChildEnv(source = process.env) {
311
322
  const env = { ...source };
@@ -320,7 +331,7 @@ function sanitizedChildEnv(source = process.env) {
320
331
  return env;
321
332
  }
322
333
  function stripTerminalControls(value) {
323
- return value.replace(/\x1b\[[0-9;?]*[ -/]*[@-~]/g, "").replace(/[\x00-\x08\x0b\x0c\x0e-\x1f\x7f]/g, " ").replace(/ {2,}/g, " ");
334
+ return stripVTControlCharacters(value).replace(/\x1b\](?:[^\x07\x1b]|\x1b(?!\\))*(?:\x07|\x1b\\)/g, "").replace(/\x1b\[[0-9;?]*[ -/]*[@-~]/g, "").replace(/[\x00-\x1f\x7f-\x9f]/g, " ").replace(/[\u2028\u2029]/g, " ").replace(/ {2,}/g, " ");
324
335
  }
325
336
 
326
337
  // src/core/postflight.ts
@@ -335,7 +346,7 @@ function runVerifyCommand(repoPath, command, opts = {}) {
335
346
  return Promise.resolve({ ran: false, ok: true, output: "" });
336
347
  }
337
348
  const timeoutMs = opts.timeoutMs ?? DEFAULT_TIMEOUT_MS;
338
- return new Promise((resolve4) => {
349
+ return new Promise((resolve6) => {
339
350
  const child = spawn2(command, {
340
351
  cwd: repoPath,
341
352
  shell: true,
@@ -351,11 +362,11 @@ function runVerifyCommand(repoPath, command, opts = {}) {
351
362
  child.stderr?.on("data", append);
352
363
  const timer = setTimeout(() => {
353
364
  killProcessTree(child.pid, () => child.kill("SIGKILL"));
354
- resolve4({ ran: true, ok: false, command, output: tail(out), timedOut: true });
365
+ resolve6({ ran: true, ok: false, command, output: tail(out), timedOut: true });
355
366
  }, timeoutMs);
356
367
  child.on("close", (code2) => {
357
368
  clearTimeout(timer);
358
- resolve4({
369
+ resolve6({
359
370
  ran: true,
360
371
  ok: code2 === 0,
361
372
  command,
@@ -367,7 +378,7 @@ function runVerifyCommand(repoPath, command, opts = {}) {
367
378
  });
368
379
  child.on("error", () => {
369
380
  clearTimeout(timer);
370
- resolve4({ ran: true, ok: false, command, output: tail(out), toolMissing: true });
381
+ resolve6({ ran: true, ok: false, command, output: tail(out), toolMissing: true });
371
382
  });
372
383
  });
373
384
  }
@@ -1838,10 +1849,10 @@ var wordmark = (() => {
1838
1849
  var divider = theme.dim("\u2500".repeat(48));
1839
1850
 
1840
1851
  // src/core/auth.ts
1841
- async function startDeviceAuth(config) {
1852
+ async function startDeviceAuth(config, signal) {
1842
1853
  const authorize = await fetchJson(
1843
1854
  `${config.apiBaseUrl}/wizard/device/authorize`,
1844
- { method: "POST", body: JSON.stringify({ client: "whisperr-wizard" }) }
1855
+ { method: "POST", body: JSON.stringify({ client: "whisperr-wizard" }), signal }
1845
1856
  );
1846
1857
  const intervalMs = (authorize.interval ?? 5) * 1e3;
1847
1858
  const deadline = Date.now() + (authorize.expires_in ?? 600) * 1e3;
@@ -1852,13 +1863,14 @@ async function startDeviceAuth(config) {
1852
1863
  async poll() {
1853
1864
  let wait = intervalMs;
1854
1865
  while (Date.now() < deadline) {
1855
- await sleep(wait);
1866
+ await sleep(wait, signal);
1856
1867
  const res = await fetch(
1857
1868
  `${config.apiBaseUrl}/wizard/device/token`,
1858
1869
  {
1859
1870
  method: "POST",
1860
1871
  headers: { "content-type": "application/json" },
1861
- body: JSON.stringify({ device_code: authorize.device_code })
1872
+ body: JSON.stringify({ device_code: authorize.device_code }),
1873
+ signal
1862
1874
  }
1863
1875
  );
1864
1876
  if (res.ok) {
@@ -1902,8 +1914,20 @@ async function fetchJson(url, init) {
1902
1914
  }
1903
1915
  return await res.json();
1904
1916
  }
1905
- function sleep(ms) {
1906
- return new Promise((r) => setTimeout(r, ms));
1917
+ function sleep(ms, signal) {
1918
+ signal?.throwIfAborted();
1919
+ return new Promise((resolve6, reject) => {
1920
+ const timer = setTimeout(done, ms);
1921
+ signal?.addEventListener("abort", aborted, { once: true });
1922
+ function done() {
1923
+ signal?.removeEventListener("abort", aborted);
1924
+ resolve6();
1925
+ }
1926
+ function aborted() {
1927
+ clearTimeout(timer);
1928
+ reject(signal?.reason);
1929
+ }
1930
+ });
1907
1931
  }
1908
1932
 
1909
1933
  // src/core/config.ts
@@ -1917,10 +1941,19 @@ function resolveEffort(value, fallback) {
1917
1941
  const raw = (value ?? "").toLowerCase();
1918
1942
  return EFFORT_LEVELS.includes(raw) ? raw : fallback;
1919
1943
  }
1944
+ function stripTrailingSlashes(value) {
1945
+ let end = value.length;
1946
+ while (end > 0 && value.charCodeAt(end - 1) === 47) end -= 1;
1947
+ return value.slice(0, end);
1948
+ }
1920
1949
  function resolveConfig(flags = {}) {
1921
- const apiBaseUrl = (flags.apiBaseUrl ?? process.env.WHISPERR_WIZARD_API_BASE ?? DEFAULT_API_BASE).replace(/\/+$/, "");
1950
+ const apiBaseUrl = stripTrailingSlashes(
1951
+ flags.apiBaseUrl ?? process.env.WHISPERR_WIZARD_API_BASE ?? DEFAULT_API_BASE
1952
+ );
1922
1953
  const directOpenAIKey = process.env.WHISPERR_WIZARD_DIRECT_OPENAI_KEY ?? process.env.OPENAI_API_KEY;
1923
- const openAIBaseUrl = (process.env.WHISPERR_WIZARD_OPENAI_BASE ?? (directOpenAIKey ? "https://api.openai.com/v1" : `${apiBaseUrl}/wizard/openai`)).replace(/\/+$/, "");
1954
+ const openAIBaseUrl = stripTrailingSlashes(
1955
+ process.env.WHISPERR_WIZARD_OPENAI_BASE ?? (directOpenAIKey ? "https://api.openai.com/v1" : `${apiBaseUrl}/wizard/openai`)
1956
+ );
1924
1957
  return {
1925
1958
  apiBaseUrl,
1926
1959
  openAIBaseUrl,
@@ -2883,6 +2916,247 @@ async function detectStack(repoPath) {
2883
2916
  return results.filter((d) => d !== null).sort((a, b) => b.confidence - a.confidence);
2884
2917
  }
2885
2918
 
2919
+ // src/core/diagnostics.ts
2920
+ import { randomUUID } from "crypto";
2921
+ import {
2922
+ closeSync,
2923
+ constants,
2924
+ fchmodSync,
2925
+ fstatSync,
2926
+ fsyncSync,
2927
+ lstatSync,
2928
+ mkdirSync,
2929
+ openSync,
2930
+ realpathSync,
2931
+ writeSync
2932
+ } from "fs";
2933
+ import { dirname as dirname4, isAbsolute as isAbsolute2, join as join6, relative as relative3, resolve as resolve3 } from "path";
2934
+
2935
+ // src/core/resumeState.ts
2936
+ import { createHash as createHash3 } from "crypto";
2937
+ import { chmod, mkdir as mkdir3, readFile as readFile5, rename, rm as rm2, writeFile as writeFile3 } from "fs/promises";
2938
+ import { homedir } from "os";
2939
+ import { dirname as dirname3, join as join5 } from "path";
2940
+ function wizardStateRoot(env = process.env) {
2941
+ return env.WHISPERR_WIZARD_STATE_DIR ?? join5(env.XDG_STATE_HOME ?? join5(homedir(), ".local", "state"), "whisperr-wizard");
2942
+ }
2943
+ function resumeStatePath(identity, env = process.env) {
2944
+ const root = wizardStateRoot(env);
2945
+ const key = createHash3("sha256").update(
2946
+ [stripTrailingSlashes(identity.apiBaseUrl), identity.appId, identity.repoFingerprint].join(
2947
+ "\n"
2948
+ )
2949
+ ).digest("hex");
2950
+ return join5(root, `${key}.json`);
2951
+ }
2952
+ async function loadResumeState(path) {
2953
+ try {
2954
+ const parsed = JSON.parse(await readFile5(path, "utf8"));
2955
+ if (typeof parsed.runId !== "string" || !parsed.runId.trim()) return null;
2956
+ if (parsed.conversationId !== void 0 && (typeof parsed.conversationId !== "string" || !parsed.conversationId.trim())) {
2957
+ return null;
2958
+ }
2959
+ return {
2960
+ runId: parsed.runId,
2961
+ ...typeof parsed.conversationId === "string" ? { conversationId: parsed.conversationId } : {}
2962
+ };
2963
+ } catch {
2964
+ return null;
2965
+ }
2966
+ }
2967
+ async function saveResumeState(path, state) {
2968
+ await mkdir3(dirname3(path), { recursive: true, mode: 448 });
2969
+ const tempPath = `${path}.${process.pid}.tmp`;
2970
+ const serialized = `${JSON.stringify(state)}
2971
+ `;
2972
+ try {
2973
+ await writeFile3(tempPath, serialized, { encoding: "utf8", mode: 384 });
2974
+ await rename(tempPath, path);
2975
+ await chmod(path, 384);
2976
+ } finally {
2977
+ await rm2(tempPath, { force: true });
2978
+ }
2979
+ }
2980
+ async function clearResumeState(path) {
2981
+ await rm2(path, { force: true });
2982
+ }
2983
+
2984
+ // src/core/diagnostics.ts
2985
+ var SENSITIVE_FIELD = /(?:body|headers?|prompt|source|modelOutput|reasoning|subprocessOutput|env(?:ironment)?|deviceCode|userCode|verificationUrl|apiKey|token|secret|password)/i;
2986
+ var UNSAFE_DIRECTORY_MESSAGE = "Wizard diagnostics directory is not secure.";
2987
+ var WizardDiagnostics = class _WizardDiagnostics {
2988
+ invocationId;
2989
+ path;
2990
+ secrets = /* @__PURE__ */ new Set();
2991
+ fd;
2992
+ constructor(path, invocationId, fd) {
2993
+ this.path = path;
2994
+ this.invocationId = invocationId;
2995
+ this.fd = fd;
2996
+ }
2997
+ static create(repoPath, env = process.env, now = /* @__PURE__ */ new Date()) {
2998
+ const configuredRoot = env.WHISPERR_WIZARD_LOG_DIR;
2999
+ const logRoot = resolve3(configuredRoot ?? join6(wizardStateRoot(env), "logs"));
3000
+ assertOutsideRepository(logRoot, resolve3(repoPath));
3001
+ const directory = preparePrivateDirectory(logRoot);
3002
+ assertOutsideRepository(directory.realPath, realpathOrResolved(repoPath));
3003
+ const invocationId = randomUUID();
3004
+ const timestamp = now.toISOString().replaceAll(":", "-");
3005
+ const path = join6(logRoot, `${timestamp}-${invocationId}.jsonl`);
3006
+ const fd = createPrivateFile(path, logRoot, directory);
3007
+ return new _WizardDiagnostics(path, invocationId, fd);
3008
+ }
3009
+ registerSecrets(...values) {
3010
+ for (const value of values) {
3011
+ if (value) this.secrets.add(value);
3012
+ }
3013
+ }
3014
+ log(event, data = {}) {
3015
+ if (this.fd === void 0) return;
3016
+ const record = {
3017
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
3018
+ invocationId: this.invocationId,
3019
+ event: scrubText(event, [...this.secrets], 80)
3020
+ };
3021
+ for (const [key, value] of Object.entries(data)) {
3022
+ if (value === void 0 || SENSITIVE_FIELD.test(key)) continue;
3023
+ record[key] = typeof value === "string" ? scrubText(value, [...this.secrets], 300) : value;
3024
+ }
3025
+ try {
3026
+ writeSync(this.fd, `${JSON.stringify(record)}
3027
+ `);
3028
+ } catch {
3029
+ this.close();
3030
+ }
3031
+ }
3032
+ close() {
3033
+ if (this.fd === void 0) return;
3034
+ const fd = this.fd;
3035
+ this.fd = void 0;
3036
+ try {
3037
+ fsyncSync(fd);
3038
+ } catch {
3039
+ }
3040
+ try {
3041
+ closeSync(fd);
3042
+ } catch {
3043
+ }
3044
+ }
3045
+ };
3046
+ function preparePrivateDirectory(path) {
3047
+ try {
3048
+ lstatSync(path);
3049
+ } catch (error) {
3050
+ if (!isErrorCode(error, "ENOENT")) throwUnsafeDirectory();
3051
+ try {
3052
+ mkdirSync(path, { recursive: true, mode: 448 });
3053
+ } catch (mkdirError) {
3054
+ if (!isErrorCode(mkdirError, "EEXIST")) throwUnsafeDirectory();
3055
+ }
3056
+ }
3057
+ const stats = privateDirectoryStats(path);
3058
+ let realPath;
3059
+ try {
3060
+ realPath = realpathSync(path);
3061
+ } catch {
3062
+ throwUnsafeDirectory();
3063
+ }
3064
+ const confirmedStats = privateDirectoryStats(path);
3065
+ if (!sameFile(stats, confirmedStats)) throwUnsafeDirectory();
3066
+ return { realPath, stats: confirmedStats };
3067
+ }
3068
+ function createPrivateFile(path, directoryPath, directory) {
3069
+ let fd;
3070
+ try {
3071
+ assertDirectoryUnchanged(directoryPath, directory);
3072
+ fd = openSync(
3073
+ path,
3074
+ constants.O_CREAT | constants.O_EXCL | constants.O_WRONLY | constants.O_NOFOLLOW,
3075
+ 384
3076
+ );
3077
+ fchmodSync(fd, 384);
3078
+ const fileStats = fstatSync(fd);
3079
+ if (!fileStats.isFile() || fileStats.nlink !== 1 || (fileStats.mode & 511) !== 384 || !ownedByCurrentUser(fileStats)) {
3080
+ throwUnsafeDirectory();
3081
+ }
3082
+ assertDirectoryUnchanged(directoryPath, directory);
3083
+ if (dirname4(realpathSync(path)) !== directory.realPath) throwUnsafeDirectory();
3084
+ return fd;
3085
+ } catch {
3086
+ if (fd !== void 0) {
3087
+ try {
3088
+ closeSync(fd);
3089
+ } catch {
3090
+ }
3091
+ }
3092
+ throwUnsafeDirectory();
3093
+ }
3094
+ }
3095
+ function privateDirectoryStats(path) {
3096
+ let stats;
3097
+ try {
3098
+ stats = lstatSync(path);
3099
+ } catch {
3100
+ throwUnsafeDirectory();
3101
+ }
3102
+ if (stats.isSymbolicLink() || !stats.isDirectory() || (stats.mode & 63) !== 0 || !ownedByCurrentUser(stats)) {
3103
+ throwUnsafeDirectory();
3104
+ }
3105
+ return stats;
3106
+ }
3107
+ function assertDirectoryUnchanged(path, expected) {
3108
+ const stats = privateDirectoryStats(path);
3109
+ let realPath;
3110
+ try {
3111
+ realPath = realpathSync(path);
3112
+ } catch {
3113
+ throwUnsafeDirectory();
3114
+ }
3115
+ if (!sameFile(stats, expected.stats) || realPath !== expected.realPath) {
3116
+ throwUnsafeDirectory();
3117
+ }
3118
+ }
3119
+ function ownedByCurrentUser(stats) {
3120
+ return typeof process.getuid !== "function" || stats.uid === process.getuid();
3121
+ }
3122
+ function sameFile(left, right) {
3123
+ return left.dev === right.dev && left.ino === right.ino;
3124
+ }
3125
+ function isErrorCode(error, code2) {
3126
+ return error?.code === code2;
3127
+ }
3128
+ function throwUnsafeDirectory() {
3129
+ throw new Error(UNSAFE_DIRECTORY_MESSAGE);
3130
+ }
3131
+ function realpathOrResolved(path) {
3132
+ try {
3133
+ return realpathSync(path);
3134
+ } catch {
3135
+ return resolve3(path);
3136
+ }
3137
+ }
3138
+ function sanitizeRequestPath(path) {
3139
+ const withoutFragment = path.split("#", 1)[0] ?? "";
3140
+ const withoutQuery = withoutFragment.split("?", 1)[0] ?? "";
3141
+ const normalized = withoutQuery.startsWith("/") ? withoutQuery : `/${withoutQuery}`;
3142
+ return scrubText(normalized, [], 300);
3143
+ }
3144
+ function diagnosticErrorData(error) {
3145
+ const candidate = typeof error === "object" && error !== null ? error : void 0;
3146
+ return {
3147
+ errorType: candidate && typeof candidate.name === "string" ? candidate.name.slice(0, 80) : typeof error,
3148
+ status: candidate && typeof candidate.status === "number" ? candidate.status : void 0,
3149
+ requestId: candidate && typeof candidate.requestId === "string" ? candidate.requestId.slice(0, 200) : void 0,
3150
+ errorCode: candidate && typeof candidate.code === "string" && /^[A-Za-z0-9][A-Za-z0-9_.-]{0,63}$/.test(candidate.code) ? candidate.code : void 0
3151
+ };
3152
+ }
3153
+ function assertOutsideRepository(candidate, repoPath) {
3154
+ const rel = relative3(repoPath, candidate);
3155
+ if (rel === "" || !rel.startsWith("..") && !isAbsolute2(rel)) {
3156
+ throw new Error("Wizard diagnostics directory must be outside the target repository.");
3157
+ }
3158
+ }
3159
+
2886
3160
  // src/core/report.ts
2887
3161
  function scrubSummary(text) {
2888
3162
  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(
@@ -2931,75 +3205,41 @@ function scrubTerminalControls(value) {
2931
3205
  return value.replace(/\x1b\[[0-9;?]*[ -/]*[@-~]/g, "").replace(/\x1b\][^\x07\x1b]*(?:\x07|\x1b\\)?/g, "").replace(/[\x00-\x1f\x7f]/g, " ").replace(/ {2,}/g, " ").trim();
2932
3206
  }
2933
3207
 
2934
- // src/core/resumeState.ts
2935
- import { createHash as createHash3 } from "crypto";
2936
- import { chmod, mkdir as mkdir3, readFile as readFile5, rename, rm as rm2, writeFile as writeFile3 } from "fs/promises";
2937
- import { homedir } from "os";
2938
- import { dirname as dirname3, join as join5 } from "path";
2939
- function resumeStatePath(identity, env = process.env) {
2940
- const root = env.WHISPERR_WIZARD_STATE_DIR ?? join5(env.XDG_STATE_HOME ?? join5(homedir(), ".local", "state"), "whisperr-wizard");
2941
- const key = createHash3("sha256").update(
2942
- [identity.apiBaseUrl.replace(/\/+$/, ""), identity.appId, identity.repoFingerprint].join(
2943
- "\n"
2944
- )
2945
- ).digest("hex");
2946
- return join5(root, `${key}.json`);
2947
- }
2948
- async function loadResumeState(path) {
2949
- try {
2950
- const parsed = JSON.parse(await readFile5(path, "utf8"));
2951
- if (typeof parsed.runId !== "string" || !parsed.runId.trim()) return null;
2952
- if (parsed.conversationId !== void 0 && (typeof parsed.conversationId !== "string" || !parsed.conversationId.trim())) {
2953
- return null;
2954
- }
2955
- return {
2956
- runId: parsed.runId,
2957
- ...typeof parsed.conversationId === "string" ? { conversationId: parsed.conversationId } : {}
2958
- };
2959
- } catch {
2960
- return null;
2961
- }
2962
- }
2963
- async function saveResumeState(path, state) {
2964
- await mkdir3(dirname3(path), { recursive: true, mode: 448 });
2965
- const tempPath = `${path}.${process.pid}.tmp`;
2966
- const serialized = `${JSON.stringify(state)}
2967
- `;
2968
- try {
2969
- await writeFile3(tempPath, serialized, { encoding: "utf8", mode: 384 });
2970
- await rename(tempPath, path);
2971
- await chmod(path, 384);
2972
- } finally {
2973
- await rm2(tempPath, { force: true });
2974
- }
2975
- }
2976
- async function clearResumeState(path) {
2977
- await rm2(path, { force: true });
2978
- }
2979
-
2980
3208
  // src/core/runtime.ts
3209
+ import { randomUUID as randomUUID2 } from "crypto";
2981
3210
  var RuntimeRequestError = class extends Error {
2982
- constructor(message, status) {
3211
+ constructor(message, status, requestId, code2) {
2983
3212
  super(message);
2984
3213
  this.status = status;
3214
+ this.requestId = requestId;
3215
+ this.code = code2;
2985
3216
  this.name = "RuntimeRequestError";
2986
3217
  }
2987
3218
  status;
3219
+ requestId;
3220
+ code;
2988
3221
  };
2989
3222
  var WizardRuntimeClient = class {
2990
- constructor(config, session, fetchImpl = globalThis.fetch) {
3223
+ constructor(config, session, fetchImpl = globalThis.fetch, diagnostics, signal) {
2991
3224
  this.config = config;
2992
3225
  this.session = session;
2993
3226
  this.fetchImpl = fetchImpl;
3227
+ this.diagnostics = diagnostics;
3228
+ this.signal = signal;
3229
+ this.registerSecrets(session.token, config.directOpenAIKey);
2994
3230
  }
2995
3231
  config;
2996
3232
  session;
2997
3233
  fetchImpl;
3234
+ diagnostics;
3235
+ signal;
2998
3236
  snapshots = /* @__PURE__ */ new Map();
3237
+ secrets = /* @__PURE__ */ new Set();
2999
3238
  async createOrResumeRun(input) {
3000
3239
  const snapshot = normalizeSnapshot(
3001
3240
  await this.request("POST", "/wizard/runs", input)
3002
3241
  );
3242
+ this.registerSecrets(snapshot.ingestion.apiKey);
3003
3243
  this.snapshots.set(snapshot.run.id, snapshot);
3004
3244
  return snapshot;
3005
3245
  }
@@ -3010,6 +3250,7 @@ var WizardRuntimeClient = class {
3010
3250
  `/wizard/runs/${encodeURIComponent(runId)}`
3011
3251
  )
3012
3252
  );
3253
+ this.registerSecrets(snapshot.ingestion.apiKey);
3013
3254
  this.snapshots.set(runId, snapshot);
3014
3255
  return snapshot;
3015
3256
  }
@@ -3066,33 +3307,123 @@ var WizardRuntimeClient = class {
3066
3307
  return snapshot;
3067
3308
  }
3068
3309
  async request(method, path, body) {
3310
+ const clientRequestId = randomUUID2();
3311
+ const safePath = sanitizeRequestPath(path);
3312
+ const startedAt = performance.now();
3069
3313
  let response;
3070
3314
  try {
3071
3315
  response = await this.fetchImpl(`${this.config.apiBaseUrl}${path}`, {
3072
3316
  method,
3317
+ signal: this.signal,
3073
3318
  headers: {
3074
3319
  accept: "application/json",
3075
3320
  authorization: `Bearer ${this.session.token}`,
3321
+ "x-request-id": clientRequestId,
3076
3322
  ...body === void 0 ? {} : { "content-type": "application/json" }
3077
3323
  },
3078
3324
  ...body === void 0 ? {} : { body: JSON.stringify(body) }
3079
3325
  });
3080
3326
  } catch (error) {
3327
+ this.logRequest(method, safePath, startedAt, clientRequestId);
3081
3328
  throw new RuntimeRequestError(
3082
- `Runtime request failed: ${scrubError(error, [this.session.token])}`
3329
+ `Runtime request failed: ${scrubError(error, [...this.secrets])}`,
3330
+ void 0,
3331
+ clientRequestId
3083
3332
  );
3084
3333
  }
3334
+ const headerRequestId = safeRequestId(response.headers.get("x-request-id"));
3085
3335
  if (!response.ok) {
3086
- const detail = await response.text().catch(() => "");
3087
- const safeDetail = scrubError(detail, [this.session.token]).slice(0, 500);
3336
+ const envelope = await readErrorEnvelope(response);
3337
+ const responseRequestId = headerRequestId ?? envelope.requestId;
3338
+ this.logRequest(
3339
+ method,
3340
+ safePath,
3341
+ startedAt,
3342
+ clientRequestId,
3343
+ response.status,
3344
+ responseRequestId,
3345
+ envelope.code
3346
+ );
3347
+ throw new RuntimeRequestError(
3348
+ `Runtime request failed: ${method} ${safePath} -> ${response.status}${envelope.code ? ` (${envelope.code})` : ""}`,
3349
+ response.status,
3350
+ responseRequestId ?? clientRequestId,
3351
+ envelope.code
3352
+ );
3353
+ }
3354
+ let result;
3355
+ try {
3356
+ result = await response.json();
3357
+ } catch {
3358
+ this.logRequest(
3359
+ method,
3360
+ safePath,
3361
+ startedAt,
3362
+ clientRequestId,
3363
+ response.status,
3364
+ headerRequestId
3365
+ );
3088
3366
  throw new RuntimeRequestError(
3089
- `Runtime request failed: ${method} ${path} -> ${response.status}${safeDetail ? `: ${safeDetail}` : ""}`,
3090
- response.status
3367
+ `Runtime request failed: ${method} ${safePath} -> ${response.status}: invalid JSON response`,
3368
+ response.status,
3369
+ headerRequestId ?? clientRequestId
3091
3370
  );
3092
3371
  }
3093
- return await response.json();
3372
+ this.logRequest(
3373
+ method,
3374
+ safePath,
3375
+ startedAt,
3376
+ clientRequestId,
3377
+ response.status,
3378
+ headerRequestId ?? envelopeRequestId(result)
3379
+ );
3380
+ return result;
3381
+ }
3382
+ logRequest(method, path, startedAt, clientRequestId, status, responseRequestId, errorCode) {
3383
+ this.diagnostics?.log("runtime_request", {
3384
+ method,
3385
+ path,
3386
+ status,
3387
+ durationMs: Math.max(0, Math.round(performance.now() - startedAt)),
3388
+ clientRequestId,
3389
+ responseRequestId,
3390
+ errorCode
3391
+ });
3392
+ }
3393
+ registerSecrets(...values) {
3394
+ for (const value of values) {
3395
+ if (!value) continue;
3396
+ this.secrets.add(value);
3397
+ this.diagnostics?.registerSecrets(value);
3398
+ }
3094
3399
  }
3095
3400
  };
3401
+ async function readErrorEnvelope(response) {
3402
+ try {
3403
+ const parsed = JSON.parse(await response.text());
3404
+ if (!isRecord(parsed)) return {};
3405
+ const nested = isRecord(parsed.error) ? parsed.error : void 0;
3406
+ return {
3407
+ requestId: safeRequestId(parsed.request_id) ?? safeRequestId(parsed.requestId) ?? (nested ? safeRequestId(nested.request_id) ?? safeRequestId(nested.requestId) : void 0),
3408
+ code: (nested ? safeErrorCode(nested.code) : void 0) ?? safeErrorCode(parsed.code)
3409
+ };
3410
+ } catch {
3411
+ return {};
3412
+ }
3413
+ }
3414
+ function envelopeRequestId(value) {
3415
+ if (!isRecord(value)) return void 0;
3416
+ return safeRequestId(value.request_id) ?? safeRequestId(value.requestId);
3417
+ }
3418
+ function safeRequestId(value) {
3419
+ return typeof value === "string" && /^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$/.test(value) ? value : void 0;
3420
+ }
3421
+ function safeErrorCode(value) {
3422
+ return typeof value === "string" && /^[A-Za-z0-9][A-Za-z0-9_.-]{0,63}$/.test(value) ? value : void 0;
3423
+ }
3424
+ function isRecord(value) {
3425
+ return typeof value === "object" && value !== null && !Array.isArray(value);
3426
+ }
3096
3427
  function normalizeSnapshot(response) {
3097
3428
  return {
3098
3429
  app: {
@@ -3261,21 +3592,38 @@ function banner() {
3261
3592
  }
3262
3593
 
3263
3594
  // src/cli.ts
3264
- async function run2(options) {
3265
- const repoPath = resolve3(options.path ?? process.cwd());
3595
+ async function run2(options, diagnostics, context = {}) {
3596
+ const repoPath = resolve4(options.path ?? process.cwd());
3266
3597
  const config = resolveConfig(options);
3598
+ const signal = context.signal ?? new AbortController().signal;
3599
+ let interruptedBy = signal.aborted ? handledSignal(signal.reason) : void 0;
3600
+ signal.addEventListener(
3601
+ "abort",
3602
+ () => {
3603
+ interruptedBy = handledSignal(signal.reason);
3604
+ },
3605
+ { once: true }
3606
+ );
3607
+ diagnostics?.registerSecrets(config.directOpenAIKey);
3267
3608
  console.log(banner());
3268
3609
  p.intro(theme.signal("Let's build and wire your Whisperr intervention model."));
3269
3610
  const detections = await withSpinner(
3270
3611
  "Scanning your repository",
3271
3612
  () => detectStack(repoPath)
3272
3613
  );
3614
+ if (signal.aborted) return signalExitCode(interruptedBy);
3273
3615
  const chosen = await chooseTarget(detections);
3274
3616
  if (!chosen) {
3617
+ diagnostics?.log("run_failed", { stage: "stack_selection", reason: "cancelled" });
3275
3618
  p.cancel("No supported stack selected.");
3276
3619
  return 1;
3277
3620
  }
3621
+ diagnostics?.log("stack_selected", {
3622
+ stack: chosen.playbook.target.id,
3623
+ detected: Boolean(chosen.detection)
3624
+ });
3278
3625
  if (chosen.playbook.target.availability === "planned") {
3626
+ diagnostics?.log("run_failed", { stage: "stack_selection", reason: "unavailable" });
3279
3627
  p.cancel(
3280
3628
  `The ${chosen.playbook.target.displayName} SDK is not available yet, so the wizard cannot safely instrument this repository.`
3281
3629
  );
@@ -3286,14 +3634,26 @@ async function run2(options) {
3286
3634
  );
3287
3635
  let session;
3288
3636
  try {
3289
- session = await withBrowserAuth(config);
3637
+ session = await withBrowserAuth(config, diagnostics, signal);
3638
+ diagnostics?.registerSecrets(session.token);
3290
3639
  } catch (error) {
3291
- p.cancel(theme.alert(scrubError(error)));
3292
- return 1;
3640
+ const safeError = scrubError(error, [config.directOpenAIKey ?? ""]);
3641
+ diagnostics?.log("run_failed", {
3642
+ stage: "authentication",
3643
+ ...diagnosticErrorData(error)
3644
+ });
3645
+ p.cancel(theme.alert(safeError));
3646
+ return interruptedBy ? signalExitCode(interruptedBy) : 1;
3293
3647
  }
3294
3648
  const stopKeepalive = startSessionKeepalive(config, session);
3295
3649
  const fingerprint = await repoFingerprint(repoPath);
3296
- const runtime = new WizardRuntimeClient(config, session);
3650
+ const runtime = new WizardRuntimeClient(
3651
+ config,
3652
+ session,
3653
+ globalThis.fetch,
3654
+ diagnostics,
3655
+ signal
3656
+ );
3297
3657
  let selected;
3298
3658
  try {
3299
3659
  selected = await withSpinner(
@@ -3309,17 +3669,21 @@ async function run2(options) {
3309
3669
  );
3310
3670
  } catch (error) {
3311
3671
  stopKeepalive();
3312
- p.cancel(theme.alert(scrubError(error, [session.token, config.directOpenAIKey ?? ""])));
3313
- return 1;
3672
+ const safeError = scrubError(error, [session.token, config.directOpenAIKey ?? ""]);
3673
+ diagnostics?.log("run_failed", {
3674
+ stage: "run_selection",
3675
+ ...diagnosticErrorData(error)
3676
+ });
3677
+ p.cancel(theme.alert(safeError));
3678
+ return interruptedBy ? signalExitCode(interruptedBy) : 1;
3314
3679
  }
3315
- const abortController = new AbortController();
3316
- let interruptedBy;
3317
- const interrupt = (signal) => {
3318
- interruptedBy = signal;
3319
- abortController.abort(new Error(`Interrupted by ${signal}`));
3320
- };
3321
- process.once("SIGINT", interrupt);
3322
- process.once("SIGTERM", interrupt);
3680
+ diagnostics?.registerSecrets(selected.snapshot.ingestion.apiKey);
3681
+ diagnostics?.log("run_selected", {
3682
+ resumed: selected.resumed,
3683
+ appId: selected.snapshot.app.id,
3684
+ projectId: selected.snapshot.project.id,
3685
+ runId: selected.snapshot.run.id
3686
+ });
3323
3687
  let phase = selected.resumed ? "resuming" : "exploring";
3324
3688
  const spin = p.spinner();
3325
3689
  const useSpinner = Boolean(process.stdout.isTTY);
@@ -3329,7 +3693,13 @@ async function run2(options) {
3329
3693
  try {
3330
3694
  checkpoint = await takeCheckpoint(repoPath);
3331
3695
  invocationSnapshot = checkpoint.isRepo ? await snapshotChanges(repoPath, checkpoint) : /* @__PURE__ */ new Map();
3332
- if (!await enforceGitSafety(repoPath, checkpoint, selected.resumed, options.force)) {
3696
+ if (!await enforceGitSafety(
3697
+ repoPath,
3698
+ checkpoint,
3699
+ selected.resumed,
3700
+ options.force,
3701
+ diagnostics
3702
+ )) {
3333
3703
  await runtime.updateRun(selected.snapshot.run.id, {
3334
3704
  status: "failed",
3335
3705
  error: "Repository is not in a safe state for automatic edits.",
@@ -3337,8 +3707,7 @@ async function run2(options) {
3337
3707
  }).catch(() => {
3338
3708
  });
3339
3709
  stopKeepalive();
3340
- process.removeListener("SIGINT", interrupt);
3341
- process.removeListener("SIGTERM", interrupt);
3710
+ diagnostics?.log("run_failed", { stage: "git_safety", runId: selected.snapshot.run.id });
3342
3711
  return 1;
3343
3712
  }
3344
3713
  p.note(
@@ -3351,6 +3720,7 @@ async function run2(options) {
3351
3720
  );
3352
3721
  if (useSpinner) spin.start(theme.bright(phase));
3353
3722
  else p.log.step(theme.bright(phase));
3723
+ diagnostics?.log("phase", { phase: safeProgressText(phase) });
3354
3724
  heartbeat = setInterval(() => {
3355
3725
  void runtime.updateRun(selected.snapshot.run.id, {}).catch(() => {
3356
3726
  });
@@ -3369,10 +3739,14 @@ async function run2(options) {
3369
3739
  }).catch(() => {
3370
3740
  });
3371
3741
  stopKeepalive();
3372
- process.removeListener("SIGINT", interrupt);
3373
- process.removeListener("SIGTERM", interrupt);
3742
+ diagnostics?.log("run_failed", {
3743
+ stage: "repository_setup",
3744
+ runId: selected.snapshot.run.id,
3745
+ interrupted: Boolean(interruptedBy),
3746
+ ...diagnosticErrorData(error)
3747
+ });
3374
3748
  p.cancel(theme.alert(safeError));
3375
- return interruptedBy === "SIGINT" ? 130 : 1;
3749
+ return interruptedBy ? signalExitCode(interruptedBy) : 1;
3376
3750
  }
3377
3751
  let runtimeCompleted = false;
3378
3752
  try {
@@ -3385,7 +3759,7 @@ async function run2(options) {
3385
3759
  runtime,
3386
3760
  checkpoint,
3387
3761
  conversationId: selected.snapshot.run.modelConversationId,
3388
- signal: abortController.signal,
3762
+ signal,
3389
3763
  async saveConversationId(conversationId) {
3390
3764
  await saveResumeState(selected.statePath, {
3391
3765
  runId: selected.snapshot.run.id,
@@ -3395,15 +3769,26 @@ async function run2(options) {
3395
3769
  progress: {
3396
3770
  onPhase(label) {
3397
3771
  phase = label;
3772
+ diagnostics?.log("phase", { phase: safeProgressText(label) });
3398
3773
  if (useSpinner) spin.message(theme.bright(label));
3399
3774
  else p.log.step(theme.bright(label));
3400
3775
  },
3401
3776
  onActivity(message) {
3777
+ diagnostics?.log("activity", {
3778
+ phase: safeProgressText(phase),
3779
+ activity: activityCategory(message)
3780
+ });
3402
3781
  if (useSpinner) spin.message(`${theme.bright(phase)}${theme.muted(` - ${message}`)}`);
3403
3782
  }
3404
3783
  }
3405
3784
  });
3406
3785
  runtimeCompleted = true;
3786
+ diagnostics?.log("integration_completed", {
3787
+ runId: selected.snapshot.run.id,
3788
+ durationMs: outcome.durationMs,
3789
+ wiredEventCount: outcome.wiredEvents.filter((event) => event.status === "wired").length,
3790
+ eventCount: outcome.wiredEvents.length
3791
+ });
3407
3792
  clearInterval(heartbeat);
3408
3793
  if (useSpinner) spin.stop(theme.success("Integration complete"));
3409
3794
  else p.log.success(theme.success("Integration complete"));
@@ -3439,22 +3824,29 @@ async function run2(options) {
3439
3824
  if (!report.ok) {
3440
3825
  p.log.warn(theme.warn("The integration completed, but coverage reporting failed."));
3441
3826
  }
3827
+ diagnostics?.log("coverage_report", { ok: report.ok });
3442
3828
  p.log.step(
3443
3829
  theme.bright("Run your app once") + theme.muted(" and trigger a tracked action so Whisperr can confirm ingestion.")
3444
3830
  );
3445
3831
  const first = await pollFirstEvent(config, session, {
3446
3832
  timeoutMs: 12e4,
3447
- signal: abortController.signal
3833
+ signal
3448
3834
  });
3449
3835
  if (interruptedBy) {
3836
+ diagnostics?.log("first_event", { outcome: "interrupted" });
3450
3837
  p.log.info(theme.muted("The integration is complete; first-event polling was interrupted."));
3451
- return 130;
3838
+ return signalExitCode(interruptedBy);
3452
3839
  }
3453
3840
  if (first.received) {
3841
+ diagnostics?.log("first_event", {
3842
+ outcome: "received",
3843
+ eventTypePresent: Boolean(first.eventType)
3844
+ });
3454
3845
  p.log.success(
3455
3846
  theme.success("Whisperr is receiving events") + (first.eventType ? theme.muted(` (${first.eventType})`) : "")
3456
3847
  );
3457
3848
  } else {
3849
+ diagnostics?.log("first_event", { outcome: "not_received" });
3458
3850
  p.log.info(theme.muted("No event received yet; events will flow when the app runs."));
3459
3851
  }
3460
3852
  const undo = invocationSnapshot.size === 0 ? revertHint(checkpoint) : void 0;
@@ -3478,6 +3870,13 @@ async function run2(options) {
3478
3870
  selected.snapshot.ingestion.apiKey
3479
3871
  ]);
3480
3872
  const failurePhase = interruptedBy ? "interrupted" : "failed";
3873
+ diagnostics?.log("run_failed", {
3874
+ stage: safeProgressText(phase),
3875
+ runId: selected.snapshot.run.id,
3876
+ interrupted: Boolean(interruptedBy),
3877
+ runtimeCompleted,
3878
+ ...diagnosticErrorData(error)
3879
+ });
3481
3880
  if (!runtimeCompleted) {
3482
3881
  const authoritative = await runtime.getRun(selected.snapshot.run.id).catch(() => null);
3483
3882
  if (authoritative?.run.status === "completed") {
@@ -3489,7 +3888,7 @@ async function run2(options) {
3489
3888
  p.log.error(
3490
3889
  theme.alert("The integration completed, but final reporting stopped: ") + safeError
3491
3890
  );
3492
- return interruptedBy === "SIGINT" ? 130 : 1;
3891
+ return interruptedBy ? signalExitCode(interruptedBy) : 1;
3493
3892
  }
3494
3893
  await runtime.updateRun(selected.snapshot.run.id, {
3495
3894
  status: "failed",
@@ -3510,27 +3909,32 @@ async function run2(options) {
3510
3909
  } else {
3511
3910
  p.log.info(theme.muted("Generated rows and repository edits were preserved for resume."));
3512
3911
  }
3513
- return interruptedBy === "SIGINT" ? 130 : 1;
3912
+ return interruptedBy ? signalExitCode(interruptedBy) : 1;
3514
3913
  } finally {
3515
3914
  clearInterval(heartbeat);
3516
3915
  stopKeepalive();
3517
- process.removeListener("SIGINT", interrupt);
3518
- process.removeListener("SIGTERM", interrupt);
3519
3916
  }
3520
3917
  }
3521
- async function enforceGitSafety(repoPath, checkpoint, resumed, force = false) {
3918
+ async function enforceGitSafety(repoPath, checkpoint, resumed, force = false, diagnostics) {
3522
3919
  if (!checkpoint.isRepo) {
3523
3920
  if (!force) {
3921
+ logGitSafety(diagnostics, checkpoint, resumed, force, false, false);
3524
3922
  p.cancel(
3525
3923
  "Not a git repository. Run `git init` and commit first, or use --force without automatic undo."
3526
3924
  );
3527
3925
  return false;
3528
3926
  }
3927
+ logGitSafety(diagnostics, checkpoint, resumed, force, false, true);
3529
3928
  p.log.warn("Not a git repository; automatic undo is unavailable.");
3530
3929
  return true;
3531
3930
  }
3532
- if (await isWorkingTreeClean(repoPath)) return true;
3931
+ const clean = await isWorkingTreeClean(repoPath);
3932
+ if (clean) {
3933
+ logGitSafety(diagnostics, checkpoint, resumed, force, clean, true);
3934
+ return true;
3935
+ }
3533
3936
  if (force) {
3937
+ logGitSafety(diagnostics, checkpoint, resumed, force, clean, true);
3534
3938
  p.log.warn(
3535
3939
  theme.warn("Uncommitted changes present") + theme.muted("; this invocation will preserve them if it is reverted.")
3536
3940
  );
@@ -3538,6 +3942,7 @@ async function enforceGitSafety(repoPath, checkpoint, resumed, force = false) {
3538
3942
  }
3539
3943
  if (resumed) {
3540
3944
  if (!process.stdin.isTTY || !process.stdout.isTTY) {
3945
+ logGitSafety(diagnostics, checkpoint, resumed, force, clean, false);
3541
3946
  p.cancel(
3542
3947
  "The resumed repository has uncommitted changes. Re-run with --force after confirming they are safe to edit."
3543
3948
  );
@@ -3547,9 +3952,14 @@ async function enforceGitSafety(repoPath, checkpoint, resumed, force = false) {
3547
3952
  message: "This resumed repository has uncommitted changes. Confirm they are the wizard edits you want to continue.",
3548
3953
  initialValue: false
3549
3954
  });
3550
- if (p.isCancel(confirmed) || !confirmed) return false;
3955
+ if (p.isCancel(confirmed) || !confirmed) {
3956
+ logGitSafety(diagnostics, checkpoint, resumed, force, clean, false);
3957
+ return false;
3958
+ }
3959
+ logGitSafety(diagnostics, checkpoint, resumed, force, clean, true);
3551
3960
  return true;
3552
3961
  }
3962
+ logGitSafety(diagnostics, checkpoint, resumed, force, clean, false);
3553
3963
  p.cancel(
3554
3964
  "Your working tree has uncommitted changes. Commit or stash them first, or use --force."
3555
3965
  );
@@ -3606,9 +4016,16 @@ async function chooseTarget(detections) {
3606
4016
  const playbook = playbookByTargetId(answer);
3607
4017
  return playbook ? { playbook, detection: detections.find((item) => item.target.id === answer) } : null;
3608
4018
  }
3609
- async function withBrowserAuth(config) {
4019
+ async function withBrowserAuth(config, diagnostics, signal) {
4020
+ diagnostics?.log("auth_stage", { stage: "authorization_started" });
3610
4021
  p.log.step(theme.bright("Authenticate") + theme.muted(" - opening device approval"));
3611
- const auth = await startDeviceAuth(config);
4022
+ const auth = await startDeviceAuth(config, signal);
4023
+ diagnostics?.registerSecrets(
4024
+ auth.userCode,
4025
+ auth.verificationUrl,
4026
+ auth.verificationUrlComplete
4027
+ );
4028
+ diagnostics?.log("auth_stage", { stage: "device_authorization_created" });
3612
4029
  p.note(
3613
4030
  [
3614
4031
  `Code: ${auth.userCode}`,
@@ -3619,19 +4036,64 @@ async function withBrowserAuth(config) {
3619
4036
  );
3620
4037
  try {
3621
4038
  await open3(auth.verificationUrlComplete ?? auth.verificationUrl);
4039
+ diagnostics?.log("auth_stage", { stage: "browser_opened" });
3622
4040
  } catch {
4041
+ diagnostics?.log("auth_stage", { stage: "browser_open_failed" });
3623
4042
  }
3624
4043
  const spin = p.spinner();
3625
4044
  spin.start("Waiting for browser approval");
3626
4045
  try {
3627
4046
  const session = await auth.poll();
4047
+ diagnostics?.registerSecrets(session.token);
4048
+ diagnostics?.log("auth_stage", { stage: "approved" });
3628
4049
  spin.stop(theme.success("Authenticated"));
3629
4050
  return session;
3630
4051
  } catch (error) {
4052
+ diagnostics?.log("auth_stage", {
4053
+ stage: "failed",
4054
+ ...diagnosticErrorData(error)
4055
+ });
3631
4056
  spin.stop(theme.alert("Authentication failed"));
3632
4057
  throw error;
3633
4058
  }
3634
4059
  }
4060
+ function handledSignal(value) {
4061
+ return value === "SIGINT" || value === "SIGTERM" ? value : void 0;
4062
+ }
4063
+ function signalExitCode(signal) {
4064
+ return signal === "SIGTERM" ? 143 : 130;
4065
+ }
4066
+ function logGitSafety(diagnostics, checkpoint, resumed, force, clean, allowed) {
4067
+ diagnostics?.log("git_safety", {
4068
+ isRepository: checkpoint.isRepo,
4069
+ clean,
4070
+ forced: force,
4071
+ resumed,
4072
+ allowed,
4073
+ automaticUndo: checkpoint.isRepo
4074
+ });
4075
+ }
4076
+ function safeProgressText(value) {
4077
+ return scrubError(value).replace(/\s+/g, " ").slice(0, 160);
4078
+ }
4079
+ function activityCategory(value) {
4080
+ const categories = [
4081
+ ["Edited ", "repository_edit"],
4082
+ ["Created ", "repository_create"],
4083
+ ["Configured ", "ingestion_configured"],
4084
+ ["Running ", "command_started"],
4085
+ ["Reading ", "repository_read"],
4086
+ ["Scanning ", "repository_scan"],
4087
+ ["Searching ", "repository_search"],
4088
+ ["Persisted ", "model_item_persisted"],
4089
+ ["Verification failed", "verification_failed"],
4090
+ ["The stable end-user identity", "identity_wiring_pending"],
4091
+ ["Generated events", "event_wiring_pending"],
4092
+ ["Runtime model", "runtime_completed"],
4093
+ ["The previous model conversation", "conversation_restarted"]
4094
+ ];
4095
+ return categories.find(([prefix]) => value.startsWith(prefix))?.[1] ?? "progress_update";
4096
+ }
3635
4097
  async function withSpinner(label, action) {
3636
4098
  const spin = p.spinner();
3637
4099
  spin.start(label);
@@ -3647,7 +4109,7 @@ async function withSpinner(label, action) {
3647
4109
  async function hasIdentifyCall2(repoPath, files) {
3648
4110
  for (const file of files) {
3649
4111
  try {
3650
- if (hasWhisperrMethodCall(await readFile6(join6(repoPath, file), "utf8"), "identify")) {
4112
+ if (hasWhisperrMethodCall(await readFile6(join7(repoPath, file), "utf8"), "identify")) {
3651
4113
  return true;
3652
4114
  }
3653
4115
  } catch {
@@ -3659,7 +4121,7 @@ async function hasIdentifyCall2(repoPath, files) {
3659
4121
  // src/index.ts
3660
4122
  var modulePath = fileURLToPath(import.meta.url);
3661
4123
  function packageVersion() {
3662
- const packagePath = join7(dirname4(modulePath), "..", "package.json");
4124
+ const packagePath = join8(dirname5(modulePath), "..", "package.json");
3663
4125
  const pkg = JSON.parse(readFileSync(packagePath, "utf8"));
3664
4126
  return pkg.version ?? "0.0.0";
3665
4127
  }
@@ -3725,18 +4187,81 @@ async function main() {
3725
4187
  console.log(packageVersion());
3726
4188
  return;
3727
4189
  }
4190
+ await runInvocation(parsed);
4191
+ }
4192
+ async function runInvocation(options, dependencies = {}) {
4193
+ const createDiagnostics = dependencies.createDiagnostics ?? ((repoPath2) => WizardDiagnostics.create(repoPath2));
4194
+ const runWizard = dependencies.run ?? run2;
4195
+ const addSignalListener = dependencies.addSignalListener ?? ((signal, listener) => process.once(signal, listener));
4196
+ const removeSignalListener = dependencies.removeSignalListener ?? ((signal, listener) => process.removeListener(signal, listener));
4197
+ const setExitCode = dependencies.setExitCode ?? ((code2) => process.exitCode = code2);
4198
+ const log2 = dependencies.log ?? console.log;
4199
+ const reportError = dependencies.error ?? console.error;
4200
+ const repoPath = resolve5(options.path ?? process.cwd());
4201
+ let diagnostics;
3728
4202
  try {
3729
- const code2 = await run2(parsed);
3730
- process.exit(code2);
4203
+ diagnostics = createDiagnostics(repoPath);
4204
+ } catch {
4205
+ reportError("Unable to create secure diagnostics. The wizard did not run.");
4206
+ setExitCode(1);
4207
+ return;
4208
+ }
4209
+ diagnostics.log("invocation_start", {
4210
+ version: packageVersion(),
4211
+ forced: Boolean(options.force)
4212
+ });
4213
+ log2(`Diagnostics: ${diagnostics.path}`);
4214
+ const abortController = new AbortController();
4215
+ let receivedSignal;
4216
+ const handlers = {
4217
+ SIGINT: () => handleSignal("SIGINT"),
4218
+ SIGTERM: () => handleSignal("SIGTERM")
4219
+ };
4220
+ function handleSignal(signal) {
4221
+ if (receivedSignal) return;
4222
+ receivedSignal = signal;
4223
+ diagnostics.log("termination_requested", { signal });
4224
+ abortController.abort(signal);
4225
+ }
4226
+ addSignalListener("SIGINT", handlers.SIGINT);
4227
+ addSignalListener("SIGTERM", handlers.SIGTERM);
4228
+ let exitCode = 1;
4229
+ try {
4230
+ const code2 = await runWizard(options, diagnostics, {
4231
+ signal: abortController.signal
4232
+ });
4233
+ exitCode = receivedSignal ? signalExitCode2(receivedSignal) : code2;
4234
+ diagnostics.log("invocation_end", {
4235
+ outcome: receivedSignal ? "signal" : code2 === 0 ? "completed" : "failed",
4236
+ exitCode,
4237
+ signal: receivedSignal
4238
+ });
3731
4239
  } catch (err) {
3732
- console.error("\n" + theme.alert("Unexpected error: ") + err.message);
3733
- process.exit(1);
4240
+ if (receivedSignal) {
4241
+ exitCode = signalExitCode2(receivedSignal);
4242
+ diagnostics.log("invocation_end", {
4243
+ outcome: "signal",
4244
+ exitCode,
4245
+ signal: receivedSignal
4246
+ });
4247
+ } else {
4248
+ diagnostics.log("invocation_failure", diagnosticErrorData(err));
4249
+ reportError("\n" + theme.alert("Unexpected error: ") + scrubError(err));
4250
+ }
4251
+ } finally {
4252
+ removeSignalListener("SIGINT", handlers.SIGINT);
4253
+ removeSignalListener("SIGTERM", handlers.SIGTERM);
4254
+ diagnostics.close();
4255
+ setExitCode(exitCode);
3734
4256
  }
3735
4257
  }
4258
+ function signalExitCode2(signal) {
4259
+ return signal === "SIGINT" ? 130 : 143;
4260
+ }
3736
4261
  function isCliEntry() {
3737
4262
  if (!process.argv[1]) return false;
3738
4263
  try {
3739
- return realpathSync(modulePath) === realpathSync(process.argv[1]);
4264
+ return realpathSync2(modulePath) === realpathSync2(process.argv[1]);
3740
4265
  } catch {
3741
4266
  return modulePath === process.argv[1];
3742
4267
  }
@@ -3747,5 +4272,6 @@ if (isCliEntry()) {
3747
4272
  export {
3748
4273
  main,
3749
4274
  packageVersion,
3750
- parseArgs
4275
+ parseArgs,
4276
+ runInvocation
3751
4277
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@whisperr/wizard",
3
- "version": "0.5.0",
3
+ "version": "0.5.1",
4
4
  "description": "Generate a codebase-aware Whisperr intervention model and integrate its SDK and events in one command.",
5
5
  "repository": {
6
6
  "type": "git",