@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.
- package/README.md +7 -0
- package/dist/index.js +645 -119
- 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
|
|
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
|
|
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((
|
|
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) =>
|
|
283
|
-
child.on("error", () =>
|
|
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
|
-
/\
|
|
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
|
-
|
|
298
|
-
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
|
-
|
|
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-\
|
|
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((
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 = (
|
|
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 = (
|
|
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.
|
|
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
|
|
3087
|
-
const
|
|
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} ${
|
|
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
|
-
|
|
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 =
|
|
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
|
-
|
|
3292
|
-
|
|
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(
|
|
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
|
-
|
|
3313
|
-
|
|
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
|
-
|
|
3316
|
-
|
|
3317
|
-
|
|
3318
|
-
|
|
3319
|
-
|
|
3320
|
-
|
|
3321
|
-
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
3373
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
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)
|
|
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(
|
|
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 =
|
|
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
|
-
|
|
3730
|
-
|
|
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
|
-
|
|
3733
|
-
|
|
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
|
|
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
|
};
|