skalpel 4.0.42 → 4.0.43
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/anchor-shadow.mjs +20 -8
- package/autopsy.mjs +38 -8
- package/catches.mjs +40 -6
- package/incremental-ingest.mjs +14 -2
- package/ledger.mjs +16 -3
- package/login.mjs +52 -12
- package/package.json +1 -1
package/anchor-shadow.mjs
CHANGED
|
@@ -187,18 +187,30 @@ export function detectShipStatusSpiral(turns) {
|
|
|
187
187
|
// (2) reconstructTarget — pull the concrete anchor from recent turns + repo context. Returns a STRUCTURED
|
|
188
188
|
// target or null (null = "not confidently reconstructable" → the caller logs a miss and does NOT fire).
|
|
189
189
|
// ---------------------------------------------------------------------------------------------------
|
|
190
|
+
// The context passed in (recentContextText) is joined OLDEST→NEWEST, so a plain non-global .exec() would
|
|
191
|
+
// return the STALEST match. A ship-status probe must target the claim the user is doubting NOW — so scan
|
|
192
|
+
// ALL matches globally and keep the LAST (most-recent) valid one, not the first (a stale PR/URL from many
|
|
193
|
+
// turns ago).
|
|
190
194
|
function findPr(text) {
|
|
191
195
|
// "#123", "PR 123", "PR#123", "pull/123", "pull request 123"
|
|
192
|
-
const
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
+
const re = /(?:\bpr\s*#?\s*|\bpull(?:\s*request)?\s*#?\s*|\/pull\/|#)(\d{1,6})\b/gi;
|
|
197
|
+
let last = null;
|
|
198
|
+
let m;
|
|
199
|
+
while ((m = re.exec(text))) {
|
|
200
|
+
const n = Number(m[1]);
|
|
201
|
+
if (Number.isInteger(n) && n > 0 && n < 1e7) last = n;
|
|
202
|
+
}
|
|
203
|
+
return last;
|
|
196
204
|
}
|
|
197
205
|
function findUrl(text) {
|
|
198
|
-
const
|
|
199
|
-
|
|
200
|
-
let
|
|
201
|
-
|
|
206
|
+
const re = /\bhttps?:\/\/[^\s'")<>`]+/gi;
|
|
207
|
+
let last = null;
|
|
208
|
+
let m;
|
|
209
|
+
while ((m = re.exec(text))) {
|
|
210
|
+
const url = m[0].replace(/[.,);]+$/, ""); // trim trailing punctuation from prose
|
|
211
|
+
if (isSafeUrl(url)) last = url;
|
|
212
|
+
}
|
|
213
|
+
return last;
|
|
202
214
|
}
|
|
203
215
|
function findSha(text) {
|
|
204
216
|
// Prefer a sha that appears near commit-ish words; otherwise the longest lone hex token in [7,40].
|
package/autopsy.mjs
CHANGED
|
@@ -49,6 +49,7 @@ import fs from "node:fs";
|
|
|
49
49
|
import path from "node:path";
|
|
50
50
|
import os from "node:os";
|
|
51
51
|
import readline from "node:readline";
|
|
52
|
+
import { fileURLToPath } from "node:url";
|
|
52
53
|
|
|
53
54
|
const HOME = os.homedir();
|
|
54
55
|
const PROJECTS_DIR = path.join(HOME, ".claude", "projects");
|
|
@@ -212,10 +213,23 @@ function extractAnchors(text) {
|
|
|
212
213
|
return out;
|
|
213
214
|
}
|
|
214
215
|
|
|
215
|
-
|
|
216
|
+
// Captured transcript prose + tool output become quoted/shareable snippets via span() and vb(). Strip ESC
|
|
217
|
+
// + other C0 control chars so a hostile log line can't inject ANSI/CSI/OSC escapes (color, cursor moves,
|
|
218
|
+
// title/clipboard spoofing) into the receipt or a paste of it. redact() still runs on top for PII on the
|
|
219
|
+
// shareable surfaces; this is the terminal-safety layer beneath it (same defense as card.mjs `clean`).
|
|
220
|
+
export function stripCtrl(s) {
|
|
221
|
+
return (
|
|
222
|
+
String(s == null ? "" : s)
|
|
223
|
+
// eslint-disable-next-line no-control-regex
|
|
224
|
+
.replace(/\x1b\[[0-9;?]*[ -/]*[@-~]/g, "") // ANSI CSI sequences
|
|
225
|
+
// eslint-disable-next-line no-control-regex
|
|
226
|
+
.replace(/[\x00-\x08\x0b\x0c\x0e-\x1f\x7f]/g, " ")
|
|
227
|
+
); // other control chars (incl. ESC/BEL) → space
|
|
228
|
+
}
|
|
229
|
+
export function span(text, at, len) {
|
|
216
230
|
const a = Math.max(0, at - SPAN),
|
|
217
231
|
b = Math.min(text.length, at + len + SPAN);
|
|
218
|
-
return text.slice(a, b).replace(/\s+/g, " ").trim().slice(0, 200);
|
|
232
|
+
return stripCtrl(text.slice(a, b)).replace(/\s+/g, " ").trim().slice(0, 200);
|
|
219
233
|
}
|
|
220
234
|
|
|
221
235
|
// ~/.claude/projects encodes cwd as a dash-flattened path. Every vendor's project root is normalized to
|
|
@@ -1282,7 +1296,7 @@ async function main() {
|
|
|
1282
1296
|
const full = t.text || t.toolErrText || "";
|
|
1283
1297
|
const pos = full.toLowerCase().indexOf(rec.anchor.toLowerCase());
|
|
1284
1298
|
return pos < 0
|
|
1285
|
-
? full.replace(/\s+/g, " ").trim().slice(0, 200)
|
|
1299
|
+
? stripCtrl(full).replace(/\s+/g, " ").trim().slice(0, 200)
|
|
1286
1300
|
: span(full, pos, rec.anchor.length);
|
|
1287
1301
|
};
|
|
1288
1302
|
cellC.push({
|
|
@@ -1618,8 +1632,24 @@ function renderFooter(P, presentVendors) {
|
|
|
1618
1632
|
P();
|
|
1619
1633
|
}
|
|
1620
1634
|
|
|
1621
|
-
|
|
1622
|
-
|
|
1623
|
-
|
|
1624
|
-
|
|
1625
|
-
|
|
1635
|
+
// CLI guard: run the autopsy only when executed directly (`node autopsy.mjs`), never on import — so tests
|
|
1636
|
+
// and siblings can import helpers (span/stripCtrl) without triggering a full disk scan. autopsy is always
|
|
1637
|
+
// launched as a subprocess (`spawnSync node autopsy.mjs`), so direct-run behavior is unchanged. Mirrors the
|
|
1638
|
+
// isMain idiom in verify-shadow.mjs / ledger.mjs.
|
|
1639
|
+
const isMain = (() => {
|
|
1640
|
+
try {
|
|
1641
|
+
return (
|
|
1642
|
+
Boolean(process.argv[1]) &&
|
|
1643
|
+
fs.realpathSync(process.argv[1]) === fs.realpathSync(fileURLToPath(import.meta.url))
|
|
1644
|
+
);
|
|
1645
|
+
} catch {
|
|
1646
|
+
return false;
|
|
1647
|
+
}
|
|
1648
|
+
})();
|
|
1649
|
+
if (isMain) {
|
|
1650
|
+
main().catch((e) => {
|
|
1651
|
+
// fail closed and quiet: never crash a user's terminal, never invent output.
|
|
1652
|
+
process.stderr.write(`skalpel autopsy: ${e && e.message ? e.message : e}\n`);
|
|
1653
|
+
process.exit(0);
|
|
1654
|
+
});
|
|
1655
|
+
}
|
package/catches.mjs
CHANGED
|
@@ -215,17 +215,39 @@ function indexResults(entries) {
|
|
|
215
215
|
// result (we never ran the process, so there is no live err). This keeps HARNESS_ERROR vs GENUINE_FAIL
|
|
216
216
|
// identical to the live shadow: a timeout / command-not-found / missing-script is harness noise and is
|
|
217
217
|
// NEVER counted as a lie.
|
|
218
|
+
// A real Claude/Anthropic tool_result block is {tool_use_id, content, is_error} — it carries NO structured
|
|
219
|
+
// exit_code field (only Codex rollouts, normalized in loadCodexEntries, synthesize one). So indexResults
|
|
220
|
+
// hands us exitCode=null for EVERY Claude result, which made the numeric-exit-code branch below dead for
|
|
221
|
+
// Claude: a 127/126 harness failure fell through to `{code:1}` and cried wolf as a GENUINE_FAIL, and a real
|
|
222
|
+
// non-zero exit Claude didn't flag via is_error was silently dropped as PASS. For a Claude proof the code
|
|
223
|
+
// lives in the recorded text — but read it ONLY from the LEADING line. Claude Code prepends the failing
|
|
224
|
+
// process's outcome as the first line of the Bash tool_result ("Exit code 1\n…", verified across real
|
|
225
|
+
// transcripts: 142/142 non-zero-exit results start with "Exit code N"; a PASSING command has no such line).
|
|
226
|
+
// An UNANCHORED full-body scan would misread an unrelated "exit code N" the command itself printed — a
|
|
227
|
+
// grep/cat of source, a pasted CI log like "##[error]Process completed with exit code 123." — and cry wolf
|
|
228
|
+
// on a genuinely-passing proof. Anchoring to the first line reads the real outcome and nothing else.
|
|
229
|
+
function exitCodeFromText(text) {
|
|
230
|
+
const firstLine = String(text || "").split("\n", 1)[0];
|
|
231
|
+
const m = firstLine.match(/^\s*exit(?:ed)?(?:\s+with)?\s+(?:code|status)\s+(\d{1,3})\b/i);
|
|
232
|
+
if (!m) return null;
|
|
233
|
+
const n = Number.parseInt(m[1], 10);
|
|
234
|
+
return Number.isFinite(n) ? n : null;
|
|
235
|
+
}
|
|
218
236
|
function classifyRecorded({ isError, exitCode, text }) {
|
|
219
|
-
if (exitCode === 0) return "PASS";
|
|
220
|
-
if (exitCode == null && !isError) return "PASS"; // no failure signal recorded → treat as pass (never invent)
|
|
221
237
|
const ev = String(text || "");
|
|
238
|
+
// Prefer the structured exit code (Codex); else recover a real code from the text (Claude). This makes
|
|
239
|
+
// the numeric-exit-code branch reachable for Claude, so 127/126 route to HARNESS_ERROR (no cry-wolf) and
|
|
240
|
+
// a textual non-zero exit is caught even when is_error wasn't set.
|
|
241
|
+
const code = typeof exitCode === "number" ? exitCode : exitCodeFromText(ev);
|
|
242
|
+
if (code === 0) return "PASS";
|
|
243
|
+
if (code == null && !isError) return "PASS"; // no failure signal at all → never invent a lie
|
|
222
244
|
let err;
|
|
223
245
|
if (/\btimed?\s*out\b|timeout after|\bSIGKILL\b|\bSIGTERM\b/i.test(ev)) {
|
|
224
246
|
err = { killed: true, signal: "SIGTERM" }; // classifyOutcome → HARNESS_ERROR
|
|
225
|
-
} else if (typeof
|
|
226
|
-
err = { code
|
|
247
|
+
} else if (typeof code === "number" && code !== 0) {
|
|
248
|
+
err = { code }; // real recorded non-zero exit (127/126 → HARNESS_ERROR in classifyOutcome)
|
|
227
249
|
} else {
|
|
228
|
-
err = { code: 1 }; //
|
|
250
|
+
err = { code: 1 }; // is_error flagged but no numeric code recorded → generic non-zero
|
|
229
251
|
}
|
|
230
252
|
return classifyOutcome(err, ev);
|
|
231
253
|
}
|
|
@@ -315,8 +337,16 @@ export function runScan() {
|
|
|
315
337
|
}
|
|
316
338
|
|
|
317
339
|
// ---------- (6) render the honest report ----------
|
|
340
|
+
// claim_text / proof_command / evidence are UNTRUSTED captured strings (agent transcript + test output)
|
|
341
|
+
// rendered straight to a live terminal. Strip ESC + other C0 control chars FIRST (same defense as
|
|
342
|
+
// card.mjs / analytics.mjs `clean`) so a hostile transcript row can't inject ANSI/CSI/OSC escapes
|
|
343
|
+
// (color, cursor moves, title/clipboard spoofing) — then collapse whitespace and cap length.
|
|
318
344
|
function clip(s, n) {
|
|
319
345
|
const t = String(s || "")
|
|
346
|
+
// eslint-disable-next-line no-control-regex
|
|
347
|
+
.replace(/\x1b\[[0-9;?]*[ -/]*[@-~]/g, "") // ANSI CSI sequences
|
|
348
|
+
// eslint-disable-next-line no-control-regex
|
|
349
|
+
.replace(/[\x00-\x08\x0b\x0c\x0e-\x1f\x7f]/g, " ") // other control chars (incl. ESC/BEL) → space
|
|
320
350
|
.replace(/\s+/g, " ")
|
|
321
351
|
.trim();
|
|
322
352
|
return t.length > n ? t.slice(0, n - 1) + "…" : t;
|
|
@@ -348,7 +378,11 @@ export function renderReport({ scanned, catches }) {
|
|
|
348
378
|
return L.join("\n");
|
|
349
379
|
}
|
|
350
380
|
|
|
351
|
-
|
|
381
|
+
// clip() here too: the headline command phrase is built from the untrusted proof_command and rendered
|
|
382
|
+
// to the terminal, so it needs the same ESC/control-char stripping as the receipt bodies below.
|
|
383
|
+
const cmds = [
|
|
384
|
+
...new Set(catches.map((c) => clip(c.proof_command, 64).split(/\s+/).slice(0, 3).join(" "))),
|
|
385
|
+
];
|
|
352
386
|
const cmdPhrase = cmds.length === 1 ? `\`${cmds[0]}\`` : "its own test/build";
|
|
353
387
|
L.push("");
|
|
354
388
|
L.push(` skalpel scanned ${N} of your recent sessions and found`);
|
package/incremental-ingest.mjs
CHANGED
|
@@ -215,11 +215,23 @@ async function sleep(ms) {
|
|
|
215
215
|
await new Promise((resolveSleep) => setTimeout(resolveSleep, ms));
|
|
216
216
|
}
|
|
217
217
|
|
|
218
|
-
|
|
218
|
+
// fetch() resolves as soon as the response HEADERS arrive; the BODY then streams separately. Clearing the
|
|
219
|
+
// abort timer the instant fetch() resolves (the old `finally`) left every body read — `res.json()` at the
|
|
220
|
+
// call sites — with NO deadline, so a slow or hung response body could stall the drain loop forever. Keep
|
|
221
|
+
// the timer armed and read the FULL body HERE, under the same timeout; hand callers a tiny response-like
|
|
222
|
+
// shim exposing only what they use (status, ok, json()). A stalled body now aborts and rejects instead of
|
|
223
|
+
// hanging. Callers only ever read status/ok and `await response.json().catch(...)`, so the shim suffices.
|
|
224
|
+
export async function request(fetchFn, url, init, timeoutMs) {
|
|
219
225
|
const ctrl = new AbortController();
|
|
220
226
|
const timer = setTimeout(() => ctrl.abort(), timeoutMs);
|
|
221
227
|
try {
|
|
222
|
-
|
|
228
|
+
const res = await fetchFn(url, { ...init, signal: ctrl.signal });
|
|
229
|
+
const body = await res.text(); // fully consume the body while the timeout is still armed
|
|
230
|
+
return {
|
|
231
|
+
status: res.status,
|
|
232
|
+
ok: res.ok,
|
|
233
|
+
json: async () => JSON.parse(body), // async so callers' `.json().catch(...)` catches parse errors
|
|
234
|
+
};
|
|
223
235
|
} finally {
|
|
224
236
|
clearTimeout(timer);
|
|
225
237
|
}
|
package/ledger.mjs
CHANGED
|
@@ -106,8 +106,10 @@ function firstGreater(sorted, x) {
|
|
|
106
106
|
// even in a tampered log its own ts equals t0, which the strict `> t0` search already excludes — so the
|
|
107
107
|
// self-skip was a no-op and omitting it changes nothing (proven by the equivalence fixtures).
|
|
108
108
|
// opts.excludeNullSession — when true, a false row with a null session yields null (the statusline's
|
|
109
|
-
// Finding-3 guard). ledger.mjs
|
|
110
|
-
//
|
|
109
|
+
// Finding-3 guard). Both live views (ledger.mjs AND the statusline) now pass it true: a NULL session
|
|
110
|
+
// carries no real identity, so `(r.session ?? null) === (falseRow.session ?? null)` would read null===null
|
|
111
|
+
// and pair a caught-false row with an UNRELATED null-session PASS of the same proof — a bogus, cross-
|
|
112
|
+
// attributed fix delta. Requiring a real session id to measure a fix is the honest floor.
|
|
111
113
|
export function buildFixDeltas(rows, { excludeNullSession = false } = {}) {
|
|
112
114
|
const index = new Map(); // (session ?? null) → Map<proof_command, ascending ts[]>
|
|
113
115
|
for (const r of rows || []) {
|
|
@@ -180,7 +182,10 @@ export function buildLedger(rows) {
|
|
|
180
182
|
const falseEntries = entries.filter((e) => e.verdict === "FALSE");
|
|
181
183
|
// Attach the MEASURED fix delta where — and only where — a real later same-proof PASS exists. ONE index
|
|
182
184
|
// pass (buildFixDeltas) then a binary-search per false row: O(n log n), not the old O(falseCount × rows).
|
|
183
|
-
|
|
185
|
+
// excludeNullSession: a caught-false row with a NULL session can't be tied to a fix (null carries no
|
|
186
|
+
// identity), so it never cross-attributes another null-session run's PASS — the same guard the statusline
|
|
187
|
+
// sibling applies. Count-only for such rows, never a fabricated delta.
|
|
188
|
+
const fixDeltaFor = buildFixDeltas(rows || [], { excludeNullSession: true });
|
|
184
189
|
for (const e of falseEntries) {
|
|
185
190
|
const d = fixDeltaFor(e._row);
|
|
186
191
|
e.fix_delta_ms = d ? d.ms : null;
|
|
@@ -198,8 +203,16 @@ export function buildLedger(rows) {
|
|
|
198
203
|
}
|
|
199
204
|
|
|
200
205
|
// ---------- (4) render ----------
|
|
206
|
+
// claim_text / proof_command / evidence are UNTRUSTED captured strings (agent transcript + test output)
|
|
207
|
+
// rendered straight to a live terminal. Strip ESC + other C0 control chars FIRST (same defense as
|
|
208
|
+
// card.mjs / analytics.mjs `clean`) so a tampered/hostile log row can't inject ANSI/CSI/OSC escapes
|
|
209
|
+
// (color, cursor moves, title/clipboard spoofing) — then collapse whitespace and cap length.
|
|
201
210
|
function clip(s, n) {
|
|
202
211
|
const t = String(s || "")
|
|
212
|
+
// eslint-disable-next-line no-control-regex
|
|
213
|
+
.replace(/\x1b\[[0-9;?]*[ -/]*[@-~]/g, "") // ANSI CSI sequences
|
|
214
|
+
// eslint-disable-next-line no-control-regex
|
|
215
|
+
.replace(/[\x00-\x08\x0b\x0c\x0e-\x1f\x7f]/g, " ") // other control chars (incl. ESC/BEL) → space
|
|
203
216
|
.replace(/\s+/g, " ")
|
|
204
217
|
.trim();
|
|
205
218
|
return t.length > n ? t.slice(0, n - 1) + "…" : t;
|
package/login.mjs
CHANGED
|
@@ -4,11 +4,12 @@
|
|
|
4
4
|
// the token → save it. The server holds the client secret and does the code exchange; we only ever
|
|
5
5
|
// see the finished Google id_token. Branded onboarding reproduces the original prosumer `setup.mjs`
|
|
6
6
|
// red block wordmark 1:1. Run: `node skalpel-login.mjs`.
|
|
7
|
-
import { readFileSync, writeFileSync, mkdirSync } from "node:fs";
|
|
7
|
+
import { readFileSync, writeFileSync, mkdirSync, realpathSync } from "node:fs";
|
|
8
8
|
import { homedir } from "node:os";
|
|
9
9
|
import { join, dirname } from "node:path";
|
|
10
10
|
import { randomBytes } from "node:crypto";
|
|
11
11
|
import { spawn } from "node:child_process";
|
|
12
|
+
import { fileURLToPath } from "node:url";
|
|
12
13
|
|
|
13
14
|
const API = process.env.SKALPEL_API || "https://graph.skalpel.ai";
|
|
14
15
|
const CLIENT_ID =
|
|
@@ -51,15 +52,38 @@ function authPath() {
|
|
|
51
52
|
return join(process.env.XDG_CONFIG_HOME || join(homedir(), ".config"), "skalpel", "auth.json");
|
|
52
53
|
}
|
|
53
54
|
|
|
55
|
+
// Build the spawn recipe to open a URL in the default browser, safely, per-platform. Windows is the sharp
|
|
56
|
+
// edge: `start` is a cmd builtin (so it needs cmd.exe), and the OAuth URL is full of `&` param separators.
|
|
57
|
+
// The old path — spawn("start", [url], { shell: true }) — let cmd SPLIT the URL on its unquoted `&`, so
|
|
58
|
+
// sign-in silently broke (URL truncated at the first `&`) and any `&<token>` ran as a separate command
|
|
59
|
+
// (injection surface). Here we spawn cmd.exe with NO shell and windowsVerbatimArguments, quoting the URL
|
|
60
|
+
// ourselves so cmd treats the whole thing as one literal token. `'""'` is start's window title — it must be
|
|
61
|
+
// a LITERAL empty quote-pair (a zero-length JS string would contribute 0 chars to the verbatim command
|
|
62
|
+
// line, so start would consume the URL itself as the title and never open the browser). Any stray
|
|
63
|
+
// double-quote in the URL is stripped so it can't break out of its quotes (URLSearchParams never emits one
|
|
64
|
+
// anyway). macOS/Linux never use a shell, so their URL already rides as a single argv entry — unchanged.
|
|
65
|
+
export function browserOpenCommand(url, platform = process.platform) {
|
|
66
|
+
if (platform === "win32") {
|
|
67
|
+
const safe = String(url).replace(/"/g, "");
|
|
68
|
+
return {
|
|
69
|
+
cmd: "cmd.exe",
|
|
70
|
+
args: ["/c", "start", '""', `"${safe}"`],
|
|
71
|
+
opts: {
|
|
72
|
+
stdio: "ignore",
|
|
73
|
+
detached: true,
|
|
74
|
+
windowsHide: true,
|
|
75
|
+
windowsVerbatimArguments: true,
|
|
76
|
+
},
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
const cmd = platform === "darwin" ? "open" : "xdg-open";
|
|
80
|
+
return { cmd, args: [url], opts: { stdio: "ignore", detached: true } };
|
|
81
|
+
}
|
|
82
|
+
|
|
54
83
|
function openBrowser(url) {
|
|
55
|
-
const cmd =
|
|
56
|
-
process.platform === "darwin" ? "open" : process.platform === "win32" ? "start" : "xdg-open";
|
|
57
84
|
try {
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
detached: true,
|
|
61
|
-
shell: process.platform === "win32",
|
|
62
|
-
}).unref();
|
|
85
|
+
const { cmd, args, opts } = browserOpenCommand(url);
|
|
86
|
+
spawn(cmd, args, opts).unref();
|
|
63
87
|
} catch {
|
|
64
88
|
/* manual-paste fallback below */
|
|
65
89
|
}
|
|
@@ -158,7 +182,23 @@ async function main() {
|
|
|
158
182
|
process.exit(0);
|
|
159
183
|
}
|
|
160
184
|
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
185
|
+
// CLI guard: run the sign-in flow only when executed directly (`node login.mjs`, how skalpel-setup.mjs
|
|
186
|
+
// always spawns it), never on import — so browserOpenCommand can be imported/tested without launching a
|
|
187
|
+
// real OAuth flow. Mirrors the isMain idiom in verify-shadow.mjs / ledger.mjs.
|
|
188
|
+
const isMain = (() => {
|
|
189
|
+
try {
|
|
190
|
+
return (
|
|
191
|
+
Boolean(process.argv[1]) &&
|
|
192
|
+
realpathSync(process.argv[1]) === realpathSync(fileURLToPath(import.meta.url))
|
|
193
|
+
);
|
|
194
|
+
} catch {
|
|
195
|
+
return false;
|
|
196
|
+
}
|
|
197
|
+
})();
|
|
198
|
+
|
|
199
|
+
if (isMain) {
|
|
200
|
+
main().catch((e) => {
|
|
201
|
+
console.log(` ${R}✗${X} login error: ${D}${e?.message || String(e)}${X}\x1b[?25h`);
|
|
202
|
+
process.exit(1);
|
|
203
|
+
});
|
|
204
|
+
}
|