@whisperr/wizard 0.5.0 → 0.5.2

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 +8 -1
  2. package/dist/index.js +649 -120
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -59,7 +59,14 @@ Development overrides:
59
59
  - `WHISPERR_WIZARD_SERVICE_TIER`
60
60
  - `WHISPERR_WIZARD_EXPLORER_MODEL`
61
61
  - `WHISPERR_WIZARD_EXPLORER_EFFORT`
62
- - `WHISPERR_WIZARD_DIRECT_OPENAI_KEY` or `OPENAI_API_KEY`
62
+ - `WHISPERR_WIZARD_DIRECT_OPENAI_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(/\/+$/, "");
1922
- 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(/\/+$/, "");
1950
+ const apiBaseUrl = stripTrailingSlashes(
1951
+ flags.apiBaseUrl ?? process.env.WHISPERR_WIZARD_API_BASE ?? DEFAULT_API_BASE
1952
+ );
1953
+ const directOpenAIKey = process.env.WHISPERR_WIZARD_DIRECT_OPENAI_KEY;
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
+ );
3088
3347
  throw new RuntimeRequestError(
3089
- `Runtime request failed: ${method} ${path} -> ${response.status}${safeDetail ? `: ${safeDetail}` : ""}`,
3090
- response.status
3348
+ `Runtime request failed: ${method} ${safePath} -> ${response.status}${envelope.code ? ` (${envelope.code})` : ""}`,
3349
+ response.status,
3350
+ responseRequestId ?? clientRequestId,
3351
+ envelope.code
3091
3352
  );
3092
3353
  }
3093
- return await response.json();
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
+ );
3366
+ throw new RuntimeRequestError(
3367
+ `Runtime request failed: ${method} ${safePath} -> ${response.status}: invalid JSON response`,
3368
+ response.status,
3369
+ headerRequestId ?? clientRequestId
3370
+ );
3371
+ }
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,41 @@ 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);
3608
+ diagnostics?.log("model_transport", {
3609
+ transport: config.directOpenAIKey ? "direct_openai" : "runtime_gateway"
3610
+ });
3267
3611
  console.log(banner());
3268
3612
  p.intro(theme.signal("Let's build and wire your Whisperr intervention model."));
3269
3613
  const detections = await withSpinner(
3270
3614
  "Scanning your repository",
3271
3615
  () => detectStack(repoPath)
3272
3616
  );
3617
+ if (signal.aborted) return signalExitCode(interruptedBy);
3273
3618
  const chosen = await chooseTarget(detections);
3274
3619
  if (!chosen) {
3620
+ diagnostics?.log("run_failed", { stage: "stack_selection", reason: "cancelled" });
3275
3621
  p.cancel("No supported stack selected.");
3276
3622
  return 1;
3277
3623
  }
3624
+ diagnostics?.log("stack_selected", {
3625
+ stack: chosen.playbook.target.id,
3626
+ detected: Boolean(chosen.detection)
3627
+ });
3278
3628
  if (chosen.playbook.target.availability === "planned") {
3629
+ diagnostics?.log("run_failed", { stage: "stack_selection", reason: "unavailable" });
3279
3630
  p.cancel(
3280
3631
  `The ${chosen.playbook.target.displayName} SDK is not available yet, so the wizard cannot safely instrument this repository.`
3281
3632
  );
@@ -3286,14 +3637,26 @@ async function run2(options) {
3286
3637
  );
3287
3638
  let session;
3288
3639
  try {
3289
- session = await withBrowserAuth(config);
3640
+ session = await withBrowserAuth(config, diagnostics, signal);
3641
+ diagnostics?.registerSecrets(session.token);
3290
3642
  } catch (error) {
3291
- p.cancel(theme.alert(scrubError(error)));
3292
- return 1;
3643
+ const safeError = scrubError(error, [config.directOpenAIKey ?? ""]);
3644
+ diagnostics?.log("run_failed", {
3645
+ stage: "authentication",
3646
+ ...diagnosticErrorData(error)
3647
+ });
3648
+ p.cancel(theme.alert(safeError));
3649
+ return interruptedBy ? signalExitCode(interruptedBy) : 1;
3293
3650
  }
3294
3651
  const stopKeepalive = startSessionKeepalive(config, session);
3295
3652
  const fingerprint = await repoFingerprint(repoPath);
3296
- const runtime = new WizardRuntimeClient(config, session);
3653
+ const runtime = new WizardRuntimeClient(
3654
+ config,
3655
+ session,
3656
+ globalThis.fetch,
3657
+ diagnostics,
3658
+ signal
3659
+ );
3297
3660
  let selected;
3298
3661
  try {
3299
3662
  selected = await withSpinner(
@@ -3309,17 +3672,21 @@ async function run2(options) {
3309
3672
  );
3310
3673
  } catch (error) {
3311
3674
  stopKeepalive();
3312
- p.cancel(theme.alert(scrubError(error, [session.token, config.directOpenAIKey ?? ""])));
3313
- return 1;
3675
+ const safeError = scrubError(error, [session.token, config.directOpenAIKey ?? ""]);
3676
+ diagnostics?.log("run_failed", {
3677
+ stage: "run_selection",
3678
+ ...diagnosticErrorData(error)
3679
+ });
3680
+ p.cancel(theme.alert(safeError));
3681
+ return interruptedBy ? signalExitCode(interruptedBy) : 1;
3314
3682
  }
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);
3683
+ diagnostics?.registerSecrets(selected.snapshot.ingestion.apiKey);
3684
+ diagnostics?.log("run_selected", {
3685
+ resumed: selected.resumed,
3686
+ appId: selected.snapshot.app.id,
3687
+ projectId: selected.snapshot.project.id,
3688
+ runId: selected.snapshot.run.id
3689
+ });
3323
3690
  let phase = selected.resumed ? "resuming" : "exploring";
3324
3691
  const spin = p.spinner();
3325
3692
  const useSpinner = Boolean(process.stdout.isTTY);
@@ -3329,7 +3696,13 @@ async function run2(options) {
3329
3696
  try {
3330
3697
  checkpoint = await takeCheckpoint(repoPath);
3331
3698
  invocationSnapshot = checkpoint.isRepo ? await snapshotChanges(repoPath, checkpoint) : /* @__PURE__ */ new Map();
3332
- if (!await enforceGitSafety(repoPath, checkpoint, selected.resumed, options.force)) {
3699
+ if (!await enforceGitSafety(
3700
+ repoPath,
3701
+ checkpoint,
3702
+ selected.resumed,
3703
+ options.force,
3704
+ diagnostics
3705
+ )) {
3333
3706
  await runtime.updateRun(selected.snapshot.run.id, {
3334
3707
  status: "failed",
3335
3708
  error: "Repository is not in a safe state for automatic edits.",
@@ -3337,8 +3710,7 @@ async function run2(options) {
3337
3710
  }).catch(() => {
3338
3711
  });
3339
3712
  stopKeepalive();
3340
- process.removeListener("SIGINT", interrupt);
3341
- process.removeListener("SIGTERM", interrupt);
3713
+ diagnostics?.log("run_failed", { stage: "git_safety", runId: selected.snapshot.run.id });
3342
3714
  return 1;
3343
3715
  }
3344
3716
  p.note(
@@ -3351,6 +3723,7 @@ async function run2(options) {
3351
3723
  );
3352
3724
  if (useSpinner) spin.start(theme.bright(phase));
3353
3725
  else p.log.step(theme.bright(phase));
3726
+ diagnostics?.log("phase", { phase: safeProgressText(phase) });
3354
3727
  heartbeat = setInterval(() => {
3355
3728
  void runtime.updateRun(selected.snapshot.run.id, {}).catch(() => {
3356
3729
  });
@@ -3369,10 +3742,14 @@ async function run2(options) {
3369
3742
  }).catch(() => {
3370
3743
  });
3371
3744
  stopKeepalive();
3372
- process.removeListener("SIGINT", interrupt);
3373
- process.removeListener("SIGTERM", interrupt);
3745
+ diagnostics?.log("run_failed", {
3746
+ stage: "repository_setup",
3747
+ runId: selected.snapshot.run.id,
3748
+ interrupted: Boolean(interruptedBy),
3749
+ ...diagnosticErrorData(error)
3750
+ });
3374
3751
  p.cancel(theme.alert(safeError));
3375
- return interruptedBy === "SIGINT" ? 130 : 1;
3752
+ return interruptedBy ? signalExitCode(interruptedBy) : 1;
3376
3753
  }
3377
3754
  let runtimeCompleted = false;
3378
3755
  try {
@@ -3385,7 +3762,7 @@ async function run2(options) {
3385
3762
  runtime,
3386
3763
  checkpoint,
3387
3764
  conversationId: selected.snapshot.run.modelConversationId,
3388
- signal: abortController.signal,
3765
+ signal,
3389
3766
  async saveConversationId(conversationId) {
3390
3767
  await saveResumeState(selected.statePath, {
3391
3768
  runId: selected.snapshot.run.id,
@@ -3395,15 +3772,26 @@ async function run2(options) {
3395
3772
  progress: {
3396
3773
  onPhase(label) {
3397
3774
  phase = label;
3775
+ diagnostics?.log("phase", { phase: safeProgressText(label) });
3398
3776
  if (useSpinner) spin.message(theme.bright(label));
3399
3777
  else p.log.step(theme.bright(label));
3400
3778
  },
3401
3779
  onActivity(message) {
3780
+ diagnostics?.log("activity", {
3781
+ phase: safeProgressText(phase),
3782
+ activity: activityCategory(message)
3783
+ });
3402
3784
  if (useSpinner) spin.message(`${theme.bright(phase)}${theme.muted(` - ${message}`)}`);
3403
3785
  }
3404
3786
  }
3405
3787
  });
3406
3788
  runtimeCompleted = true;
3789
+ diagnostics?.log("integration_completed", {
3790
+ runId: selected.snapshot.run.id,
3791
+ durationMs: outcome.durationMs,
3792
+ wiredEventCount: outcome.wiredEvents.filter((event) => event.status === "wired").length,
3793
+ eventCount: outcome.wiredEvents.length
3794
+ });
3407
3795
  clearInterval(heartbeat);
3408
3796
  if (useSpinner) spin.stop(theme.success("Integration complete"));
3409
3797
  else p.log.success(theme.success("Integration complete"));
@@ -3439,22 +3827,29 @@ async function run2(options) {
3439
3827
  if (!report.ok) {
3440
3828
  p.log.warn(theme.warn("The integration completed, but coverage reporting failed."));
3441
3829
  }
3830
+ diagnostics?.log("coverage_report", { ok: report.ok });
3442
3831
  p.log.step(
3443
3832
  theme.bright("Run your app once") + theme.muted(" and trigger a tracked action so Whisperr can confirm ingestion.")
3444
3833
  );
3445
3834
  const first = await pollFirstEvent(config, session, {
3446
3835
  timeoutMs: 12e4,
3447
- signal: abortController.signal
3836
+ signal
3448
3837
  });
3449
3838
  if (interruptedBy) {
3839
+ diagnostics?.log("first_event", { outcome: "interrupted" });
3450
3840
  p.log.info(theme.muted("The integration is complete; first-event polling was interrupted."));
3451
- return 130;
3841
+ return signalExitCode(interruptedBy);
3452
3842
  }
3453
3843
  if (first.received) {
3844
+ diagnostics?.log("first_event", {
3845
+ outcome: "received",
3846
+ eventTypePresent: Boolean(first.eventType)
3847
+ });
3454
3848
  p.log.success(
3455
3849
  theme.success("Whisperr is receiving events") + (first.eventType ? theme.muted(` (${first.eventType})`) : "")
3456
3850
  );
3457
3851
  } else {
3852
+ diagnostics?.log("first_event", { outcome: "not_received" });
3458
3853
  p.log.info(theme.muted("No event received yet; events will flow when the app runs."));
3459
3854
  }
3460
3855
  const undo = invocationSnapshot.size === 0 ? revertHint(checkpoint) : void 0;
@@ -3478,6 +3873,13 @@ async function run2(options) {
3478
3873
  selected.snapshot.ingestion.apiKey
3479
3874
  ]);
3480
3875
  const failurePhase = interruptedBy ? "interrupted" : "failed";
3876
+ diagnostics?.log("run_failed", {
3877
+ stage: safeProgressText(phase),
3878
+ runId: selected.snapshot.run.id,
3879
+ interrupted: Boolean(interruptedBy),
3880
+ runtimeCompleted,
3881
+ ...diagnosticErrorData(error)
3882
+ });
3481
3883
  if (!runtimeCompleted) {
3482
3884
  const authoritative = await runtime.getRun(selected.snapshot.run.id).catch(() => null);
3483
3885
  if (authoritative?.run.status === "completed") {
@@ -3489,7 +3891,7 @@ async function run2(options) {
3489
3891
  p.log.error(
3490
3892
  theme.alert("The integration completed, but final reporting stopped: ") + safeError
3491
3893
  );
3492
- return interruptedBy === "SIGINT" ? 130 : 1;
3894
+ return interruptedBy ? signalExitCode(interruptedBy) : 1;
3493
3895
  }
3494
3896
  await runtime.updateRun(selected.snapshot.run.id, {
3495
3897
  status: "failed",
@@ -3510,27 +3912,32 @@ async function run2(options) {
3510
3912
  } else {
3511
3913
  p.log.info(theme.muted("Generated rows and repository edits were preserved for resume."));
3512
3914
  }
3513
- return interruptedBy === "SIGINT" ? 130 : 1;
3915
+ return interruptedBy ? signalExitCode(interruptedBy) : 1;
3514
3916
  } finally {
3515
3917
  clearInterval(heartbeat);
3516
3918
  stopKeepalive();
3517
- process.removeListener("SIGINT", interrupt);
3518
- process.removeListener("SIGTERM", interrupt);
3519
3919
  }
3520
3920
  }
3521
- async function enforceGitSafety(repoPath, checkpoint, resumed, force = false) {
3921
+ async function enforceGitSafety(repoPath, checkpoint, resumed, force = false, diagnostics) {
3522
3922
  if (!checkpoint.isRepo) {
3523
3923
  if (!force) {
3924
+ logGitSafety(diagnostics, checkpoint, resumed, force, false, false);
3524
3925
  p.cancel(
3525
3926
  "Not a git repository. Run `git init` and commit first, or use --force without automatic undo."
3526
3927
  );
3527
3928
  return false;
3528
3929
  }
3930
+ logGitSafety(diagnostics, checkpoint, resumed, force, false, true);
3529
3931
  p.log.warn("Not a git repository; automatic undo is unavailable.");
3530
3932
  return true;
3531
3933
  }
3532
- if (await isWorkingTreeClean(repoPath)) return true;
3934
+ const clean = await isWorkingTreeClean(repoPath);
3935
+ if (clean) {
3936
+ logGitSafety(diagnostics, checkpoint, resumed, force, clean, true);
3937
+ return true;
3938
+ }
3533
3939
  if (force) {
3940
+ logGitSafety(diagnostics, checkpoint, resumed, force, clean, true);
3534
3941
  p.log.warn(
3535
3942
  theme.warn("Uncommitted changes present") + theme.muted("; this invocation will preserve them if it is reverted.")
3536
3943
  );
@@ -3538,6 +3945,7 @@ async function enforceGitSafety(repoPath, checkpoint, resumed, force = false) {
3538
3945
  }
3539
3946
  if (resumed) {
3540
3947
  if (!process.stdin.isTTY || !process.stdout.isTTY) {
3948
+ logGitSafety(diagnostics, checkpoint, resumed, force, clean, false);
3541
3949
  p.cancel(
3542
3950
  "The resumed repository has uncommitted changes. Re-run with --force after confirming they are safe to edit."
3543
3951
  );
@@ -3547,9 +3955,14 @@ async function enforceGitSafety(repoPath, checkpoint, resumed, force = false) {
3547
3955
  message: "This resumed repository has uncommitted changes. Confirm they are the wizard edits you want to continue.",
3548
3956
  initialValue: false
3549
3957
  });
3550
- if (p.isCancel(confirmed) || !confirmed) return false;
3958
+ if (p.isCancel(confirmed) || !confirmed) {
3959
+ logGitSafety(diagnostics, checkpoint, resumed, force, clean, false);
3960
+ return false;
3961
+ }
3962
+ logGitSafety(diagnostics, checkpoint, resumed, force, clean, true);
3551
3963
  return true;
3552
3964
  }
3965
+ logGitSafety(diagnostics, checkpoint, resumed, force, clean, false);
3553
3966
  p.cancel(
3554
3967
  "Your working tree has uncommitted changes. Commit or stash them first, or use --force."
3555
3968
  );
@@ -3606,9 +4019,16 @@ async function chooseTarget(detections) {
3606
4019
  const playbook = playbookByTargetId(answer);
3607
4020
  return playbook ? { playbook, detection: detections.find((item) => item.target.id === answer) } : null;
3608
4021
  }
3609
- async function withBrowserAuth(config) {
4022
+ async function withBrowserAuth(config, diagnostics, signal) {
4023
+ diagnostics?.log("auth_stage", { stage: "authorization_started" });
3610
4024
  p.log.step(theme.bright("Authenticate") + theme.muted(" - opening device approval"));
3611
- const auth = await startDeviceAuth(config);
4025
+ const auth = await startDeviceAuth(config, signal);
4026
+ diagnostics?.registerSecrets(
4027
+ auth.userCode,
4028
+ auth.verificationUrl,
4029
+ auth.verificationUrlComplete
4030
+ );
4031
+ diagnostics?.log("auth_stage", { stage: "device_authorization_created" });
3612
4032
  p.note(
3613
4033
  [
3614
4034
  `Code: ${auth.userCode}`,
@@ -3619,19 +4039,64 @@ async function withBrowserAuth(config) {
3619
4039
  );
3620
4040
  try {
3621
4041
  await open3(auth.verificationUrlComplete ?? auth.verificationUrl);
4042
+ diagnostics?.log("auth_stage", { stage: "browser_opened" });
3622
4043
  } catch {
4044
+ diagnostics?.log("auth_stage", { stage: "browser_open_failed" });
3623
4045
  }
3624
4046
  const spin = p.spinner();
3625
4047
  spin.start("Waiting for browser approval");
3626
4048
  try {
3627
4049
  const session = await auth.poll();
4050
+ diagnostics?.registerSecrets(session.token);
4051
+ diagnostics?.log("auth_stage", { stage: "approved" });
3628
4052
  spin.stop(theme.success("Authenticated"));
3629
4053
  return session;
3630
4054
  } catch (error) {
4055
+ diagnostics?.log("auth_stage", {
4056
+ stage: "failed",
4057
+ ...diagnosticErrorData(error)
4058
+ });
3631
4059
  spin.stop(theme.alert("Authentication failed"));
3632
4060
  throw error;
3633
4061
  }
3634
4062
  }
4063
+ function handledSignal(value) {
4064
+ return value === "SIGINT" || value === "SIGTERM" ? value : void 0;
4065
+ }
4066
+ function signalExitCode(signal) {
4067
+ return signal === "SIGTERM" ? 143 : 130;
4068
+ }
4069
+ function logGitSafety(diagnostics, checkpoint, resumed, force, clean, allowed) {
4070
+ diagnostics?.log("git_safety", {
4071
+ isRepository: checkpoint.isRepo,
4072
+ clean,
4073
+ forced: force,
4074
+ resumed,
4075
+ allowed,
4076
+ automaticUndo: checkpoint.isRepo
4077
+ });
4078
+ }
4079
+ function safeProgressText(value) {
4080
+ return scrubError(value).replace(/\s+/g, " ").slice(0, 160);
4081
+ }
4082
+ function activityCategory(value) {
4083
+ const categories = [
4084
+ ["Edited ", "repository_edit"],
4085
+ ["Created ", "repository_create"],
4086
+ ["Configured ", "ingestion_configured"],
4087
+ ["Running ", "command_started"],
4088
+ ["Reading ", "repository_read"],
4089
+ ["Scanning ", "repository_scan"],
4090
+ ["Searching ", "repository_search"],
4091
+ ["Persisted ", "model_item_persisted"],
4092
+ ["Verification failed", "verification_failed"],
4093
+ ["The stable end-user identity", "identity_wiring_pending"],
4094
+ ["Generated events", "event_wiring_pending"],
4095
+ ["Runtime model", "runtime_completed"],
4096
+ ["The previous model conversation", "conversation_restarted"]
4097
+ ];
4098
+ return categories.find(([prefix]) => value.startsWith(prefix))?.[1] ?? "progress_update";
4099
+ }
3635
4100
  async function withSpinner(label, action) {
3636
4101
  const spin = p.spinner();
3637
4102
  spin.start(label);
@@ -3647,7 +4112,7 @@ async function withSpinner(label, action) {
3647
4112
  async function hasIdentifyCall2(repoPath, files) {
3648
4113
  for (const file of files) {
3649
4114
  try {
3650
- if (hasWhisperrMethodCall(await readFile6(join6(repoPath, file), "utf8"), "identify")) {
4115
+ if (hasWhisperrMethodCall(await readFile6(join7(repoPath, file), "utf8"), "identify")) {
3651
4116
  return true;
3652
4117
  }
3653
4118
  } catch {
@@ -3659,7 +4124,7 @@ async function hasIdentifyCall2(repoPath, files) {
3659
4124
  // src/index.ts
3660
4125
  var modulePath = fileURLToPath(import.meta.url);
3661
4126
  function packageVersion() {
3662
- const packagePath = join7(dirname4(modulePath), "..", "package.json");
4127
+ const packagePath = join8(dirname5(modulePath), "..", "package.json");
3663
4128
  const pkg = JSON.parse(readFileSync(packagePath, "utf8"));
3664
4129
  return pkg.version ?? "0.0.0";
3665
4130
  }
@@ -3725,18 +4190,81 @@ async function main() {
3725
4190
  console.log(packageVersion());
3726
4191
  return;
3727
4192
  }
4193
+ await runInvocation(parsed);
4194
+ }
4195
+ async function runInvocation(options, dependencies = {}) {
4196
+ const createDiagnostics = dependencies.createDiagnostics ?? ((repoPath2) => WizardDiagnostics.create(repoPath2));
4197
+ const runWizard = dependencies.run ?? run2;
4198
+ const addSignalListener = dependencies.addSignalListener ?? ((signal, listener) => process.once(signal, listener));
4199
+ const removeSignalListener = dependencies.removeSignalListener ?? ((signal, listener) => process.removeListener(signal, listener));
4200
+ const setExitCode = dependencies.setExitCode ?? ((code2) => process.exitCode = code2);
4201
+ const log2 = dependencies.log ?? console.log;
4202
+ const reportError = dependencies.error ?? console.error;
4203
+ const repoPath = resolve5(options.path ?? process.cwd());
4204
+ let diagnostics;
3728
4205
  try {
3729
- const code2 = await run2(parsed);
3730
- process.exit(code2);
4206
+ diagnostics = createDiagnostics(repoPath);
4207
+ } catch {
4208
+ reportError("Unable to create secure diagnostics. The wizard did not run.");
4209
+ setExitCode(1);
4210
+ return;
4211
+ }
4212
+ diagnostics.log("invocation_start", {
4213
+ version: packageVersion(),
4214
+ forced: Boolean(options.force)
4215
+ });
4216
+ log2(`Diagnostics: ${diagnostics.path}`);
4217
+ const abortController = new AbortController();
4218
+ let receivedSignal;
4219
+ const handlers = {
4220
+ SIGINT: () => handleSignal("SIGINT"),
4221
+ SIGTERM: () => handleSignal("SIGTERM")
4222
+ };
4223
+ function handleSignal(signal) {
4224
+ if (receivedSignal) return;
4225
+ receivedSignal = signal;
4226
+ diagnostics.log("termination_requested", { signal });
4227
+ abortController.abort(signal);
4228
+ }
4229
+ addSignalListener("SIGINT", handlers.SIGINT);
4230
+ addSignalListener("SIGTERM", handlers.SIGTERM);
4231
+ let exitCode = 1;
4232
+ try {
4233
+ const code2 = await runWizard(options, diagnostics, {
4234
+ signal: abortController.signal
4235
+ });
4236
+ exitCode = receivedSignal ? signalExitCode2(receivedSignal) : code2;
4237
+ diagnostics.log("invocation_end", {
4238
+ outcome: receivedSignal ? "signal" : code2 === 0 ? "completed" : "failed",
4239
+ exitCode,
4240
+ signal: receivedSignal
4241
+ });
3731
4242
  } catch (err) {
3732
- console.error("\n" + theme.alert("Unexpected error: ") + err.message);
3733
- process.exit(1);
4243
+ if (receivedSignal) {
4244
+ exitCode = signalExitCode2(receivedSignal);
4245
+ diagnostics.log("invocation_end", {
4246
+ outcome: "signal",
4247
+ exitCode,
4248
+ signal: receivedSignal
4249
+ });
4250
+ } else {
4251
+ diagnostics.log("invocation_failure", diagnosticErrorData(err));
4252
+ reportError("\n" + theme.alert("Unexpected error: ") + scrubError(err));
4253
+ }
4254
+ } finally {
4255
+ removeSignalListener("SIGINT", handlers.SIGINT);
4256
+ removeSignalListener("SIGTERM", handlers.SIGTERM);
4257
+ diagnostics.close();
4258
+ setExitCode(exitCode);
3734
4259
  }
3735
4260
  }
4261
+ function signalExitCode2(signal) {
4262
+ return signal === "SIGINT" ? 130 : 143;
4263
+ }
3736
4264
  function isCliEntry() {
3737
4265
  if (!process.argv[1]) return false;
3738
4266
  try {
3739
- return realpathSync(modulePath) === realpathSync(process.argv[1]);
4267
+ return realpathSync2(modulePath) === realpathSync2(process.argv[1]);
3740
4268
  } catch {
3741
4269
  return modulePath === process.argv[1];
3742
4270
  }
@@ -3747,5 +4275,6 @@ if (isCliEntry()) {
3747
4275
  export {
3748
4276
  main,
3749
4277
  packageVersion,
3750
- parseArgs
4278
+ parseArgs,
4279
+ runInvocation
3751
4280
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@whisperr/wizard",
3
- "version": "0.5.0",
3
+ "version": "0.5.2",
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",