@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.
- package/README.md +8 -1
- package/dist/index.js +649 -120
- 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`
|
|
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
|
|
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 = (
|
|
1922
|
-
|
|
1923
|
-
|
|
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.
|
|
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
|
+
);
|
|
3088
3347
|
throw new RuntimeRequestError(
|
|
3089
|
-
`Runtime request failed: ${method} ${
|
|
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
|
-
|
|
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 =
|
|
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
|
-
|
|
3292
|
-
|
|
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(
|
|
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
|
-
|
|
3313
|
-
|
|
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
|
-
|
|
3316
|
-
|
|
3317
|
-
|
|
3318
|
-
|
|
3319
|
-
|
|
3320
|
-
|
|
3321
|
-
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
3373
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
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)
|
|
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(
|
|
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 =
|
|
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
|
-
|
|
3730
|
-
|
|
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
|
-
|
|
3733
|
-
|
|
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
|
|
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
|
};
|