@hey-research-lab/check 0.0.0-stage → 0.1.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/dist/cli.js ADDED
@@ -0,0 +1,1627 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/card/render.ts
4
+ import { existsSync } from "fs";
5
+ import { createRequire } from "module";
6
+ import { dirname, join } from "path";
7
+ import { fileURLToPath } from "url";
8
+
9
+ // src/card/svg.ts
10
+ var CARD_WIDTH = 1200;
11
+ var CARD_HEIGHT = 675;
12
+ var CARD_FONT_FAMILY = "Geist";
13
+ var COLOR = {
14
+ background: "#07080b",
15
+ frame: "#1b1f26",
16
+ rule: "#1f242c",
17
+ ink: "#f1f3f6",
18
+ secondary: "#a2a9b4",
19
+ lime: "#c2fb03",
20
+ amber: "#f2b84b"
21
+ };
22
+ function escapeXml(value) {
23
+ return value.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&apos;");
24
+ }
25
+ var NOT_DRAWN = /[\u0000-\u001F\u007F-\u009F\u00AD\u061C\u200B-\u200F\u202A-\u202E\u2060-\u2069\uFE00-\uFE0F\uFEFF]/g;
26
+ var DRAWN = /[ -~\u00A0-\u024F\u0259\u02B0-\u02FF\u0370-\u03FF\u0400-\u04FF\u1E00-\u1EFF\u2010-\u2027\u2030-\u205E\u20A0-\u20C0\u2100-\u214F\u2190-\u2199\u2212]/u;
27
+ function cardText(value) {
28
+ let out = "";
29
+ for (const ch of value.toWellFormed().replace(NOT_DRAWN, "")) if (DRAWN.test(ch)) out += ch;
30
+ return out.replace(/\s+/g, " ").trim();
31
+ }
32
+ function wrap(text2, maxWidth, size, weight, maxLines, measure, letterSpacing = 0) {
33
+ const fits = (s) => measure(s, size, weight, letterSpacing) <= maxWidth;
34
+ const words = text2.split(" ").filter(Boolean);
35
+ const lines = [];
36
+ let current = "";
37
+ let i = 0;
38
+ for (; i < words.length; i += 1) {
39
+ const word = words[i];
40
+ const next = current ? `${current} ${word}` : word;
41
+ if (fits(next)) {
42
+ current = next;
43
+ continue;
44
+ }
45
+ if (current) {
46
+ lines.push(current);
47
+ current = "";
48
+ if (lines.length === maxLines) break;
49
+ }
50
+ if (fits(word)) current = word;
51
+ else {
52
+ let part = "";
53
+ for (const ch of word) {
54
+ if (fits(part + ch)) part += ch;
55
+ else {
56
+ lines.push(part);
57
+ if (lines.length === maxLines) break;
58
+ part = ch;
59
+ }
60
+ }
61
+ if (lines.length === maxLines) break;
62
+ current = part;
63
+ }
64
+ }
65
+ if (current && lines.length < maxLines) {
66
+ lines.push(current);
67
+ current = "";
68
+ }
69
+ const cut = i < words.length || current !== "";
70
+ if (cut && lines.length > 0) {
71
+ let last = lines[lines.length - 1];
72
+ while (last.length > 1 && !fits(`${last}\u2026`)) last = [...last].slice(0, -1).join("").trimEnd();
73
+ lines[lines.length - 1] = `${last.replace(/[\s,;:.·—-]+$/u, "")}\u2026`;
74
+ }
75
+ return lines;
76
+ }
77
+ function sizeToFit(text2, maxWidth, sizes, weight, measure, ls) {
78
+ for (const size of sizes) if (measure(text2, size, weight, ls(size)) <= maxWidth) return size;
79
+ return null;
80
+ }
81
+ function textEl(content, o) {
82
+ const anchor = o.anchor === "end" ? ' text-anchor="end"' : "";
83
+ const ls = o.letterSpacing ? ` letter-spacing="${o.letterSpacing}"` : "";
84
+ return `<text x="${o.x}" y="${o.y}" font-family="${CARD_FONT_FAMILY}" font-size="${o.size}" font-weight="${o.weight}" fill="${o.fill}"${anchor}${ls} xml:space="preserve">${escapeXml(content)}</text>`;
85
+ }
86
+ var PAD = 80;
87
+ var RIGHT = CARD_WIDTH - PAD;
88
+ var CONTENT = RIGHT - PAD;
89
+ function buildCardSvg(model, measure) {
90
+ const parts = [];
91
+ const titleLs = (s) => -Math.round(s * 0.03);
92
+ parts.push(
93
+ `<rect width="${CARD_WIDTH}" height="${CARD_HEIGHT}" fill="${COLOR.background}"/>`,
94
+ `<rect x="24" y="24" width="${CARD_WIDTH - 48}" height="${CARD_HEIGHT - 48}" rx="28" fill="none" stroke="${COLOR.frame}" stroke-width="2"/>`,
95
+ `<rect x="${PAD}" y="77" width="14" height="14" rx="3" fill="${COLOR.lime}"/>`,
96
+ textEl("HEY RESEARCH LAB \xB7 DEV CHECK", {
97
+ x: PAD + 28,
98
+ y: 91,
99
+ size: 22,
100
+ weight: 600,
101
+ fill: COLOR.secondary,
102
+ letterSpacing: 2.4
103
+ }),
104
+ textEl("Robinhood Chain", {
105
+ x: RIGHT,
106
+ y: 91,
107
+ size: 22,
108
+ weight: 400,
109
+ fill: COLOR.secondary,
110
+ anchor: "end"
111
+ })
112
+ );
113
+ const title = cardText(model.title) || "Untitled";
114
+ let y;
115
+ const one = sizeToFit(title, CONTENT, [92, 84, 76, 68], 700, measure, titleLs);
116
+ if (one !== null) {
117
+ y = 196;
118
+ parts.push(
119
+ textEl(title, {
120
+ x: PAD,
121
+ y,
122
+ size: one,
123
+ weight: 700,
124
+ fill: COLOR.ink,
125
+ letterSpacing: titleLs(one)
126
+ })
127
+ );
128
+ } else {
129
+ const lines = wrap(title, CONTENT, 64, 700, 2, measure, titleLs(64));
130
+ y = 176;
131
+ lines.forEach(
132
+ (line, i) => parts.push(
133
+ textEl(line, {
134
+ x: PAD,
135
+ y: y + i * 70,
136
+ size: 64,
137
+ weight: 700,
138
+ fill: COLOR.ink,
139
+ letterSpacing: titleLs(64)
140
+ })
141
+ )
142
+ );
143
+ y += (lines.length - 1) * 70;
144
+ }
145
+ const twoLineTitle = one === null;
146
+ const ticker = model.ticker ? cardText(model.ticker) : "";
147
+ const hasRow = ticker !== "" || model.pill !== null;
148
+ if (hasRow) {
149
+ y += 70;
150
+ let x = PAD;
151
+ if (ticker) {
152
+ const t = wrap(ticker, 360, 36, 600, 1, measure)[0] ?? "";
153
+ parts.push(textEl(t, { x, y, size: 36, weight: 600, fill: COLOR.secondary }));
154
+ x += measure(t, 36, 600) + 28;
155
+ }
156
+ if (model.pill) {
157
+ const pillLabel = cardText(model.pill.label);
158
+ const pillText = 26;
159
+ const pillW = measure(pillLabel, pillText, 600) + 30 + 26 + 26;
160
+ const pillH = 50;
161
+ const pillY = y - 37;
162
+ const lime = model.pill.tone === "lime";
163
+ const pillFill = lime ? "rgba(194,251,3,0.10)" : "rgba(162,169,180,0.08)";
164
+ const pillStroke = lime ? COLOR.lime : "#3a404a";
165
+ const pillInk = lime ? COLOR.lime : COLOR.secondary;
166
+ parts.push(
167
+ `<rect x="${x}" y="${pillY}" width="${Math.round(pillW)}" height="${pillH}" rx="${pillH / 2}" fill="${pillFill}" stroke="${pillStroke}" stroke-width="2"/>`,
168
+ `<circle cx="${x + 28}" cy="${pillY + pillH / 2}" r="7" fill="${pillInk}"/>`,
169
+ textEl(pillLabel, { x: x + 48, y: pillY + 34, size: pillText, weight: 600, fill: pillInk })
170
+ );
171
+ }
172
+ }
173
+ y += hasRow ? twoLineTitle ? 88 : 96 : 98;
174
+ const headline = cardText(model.headline);
175
+ const hSize = sizeToFit(headline, CONTENT, [62, 56, 50, 44], 700, measure, (s) => -Math.round(s * 0.02)) ?? 44;
176
+ const hLines = wrap(headline, CONTENT, hSize, 700, 1, measure, -Math.round(hSize * 0.02));
177
+ parts.push(
178
+ textEl(hLines[0] ?? "", {
179
+ x: PAD,
180
+ y,
181
+ size: hSize,
182
+ weight: 700,
183
+ fill: COLOR.ink,
184
+ letterSpacing: -Math.round(hSize * 0.02)
185
+ })
186
+ );
187
+ const ruleY = 562;
188
+ parts.push(
189
+ `<line x1="${PAD}" y1="${ruleY}" x2="${RIGHT}" y2="${ruleY}" stroke="${COLOR.rule}" stroke-width="2"/>`
190
+ );
191
+ const checked = cardText(model.checkedOn);
192
+ const checkedW = measure(checked, 22, 400);
193
+ const urlMax = CONTENT - checkedW - 32;
194
+ const url = cardText(model.footerUrl);
195
+ const urlSize = sizeToFit(url, urlMax, [26, 24, 22, 20], 600, measure, () => 0) ?? 20;
196
+ parts.push(
197
+ textEl(wrap(url, urlMax, urlSize, 600, 1, measure)[0] ?? "", {
198
+ x: PAD,
199
+ y: 606,
200
+ size: urlSize,
201
+ weight: 600,
202
+ fill: COLOR.ink
203
+ }),
204
+ textEl(checked, {
205
+ x: RIGHT,
206
+ y: 606,
207
+ size: 22,
208
+ weight: 400,
209
+ fill: COLOR.secondary,
210
+ anchor: "end"
211
+ }),
212
+ textEl("Builder evidence, not a safety verdict or investment advice.", {
213
+ x: PAD,
214
+ y: 640,
215
+ size: 22,
216
+ weight: 400,
217
+ fill: COLOR.secondary
218
+ })
219
+ );
220
+ const facts = model.facts.map(cardText).filter(Boolean);
221
+ const factsY = ruleY - 34;
222
+ const bottom = facts.length > 0 ? factsY - 44 : ruleY - 22;
223
+ if (facts.length > 0) {
224
+ const joined = facts.join(" \xB7 ");
225
+ const fSize = sizeToFit(joined, CONTENT, [28, 26, 24], 400, measure, () => 0) ?? 22;
226
+ const line = wrap(joined, CONTENT, fSize, 400, 1, measure)[0] ?? "";
227
+ parts.push(textEl(line, { x: PAD, y: factsY, size: fSize, weight: 400, fill: COLOR.ink }));
228
+ }
229
+ if (model.notice) {
230
+ const top = y + 36;
231
+ const inner = CONTENT - 56;
232
+ const lineH = 36;
233
+ const foot = cardText(model.notice.foot);
234
+ const footH = foot ? 40 : 0;
235
+ const room = Math.max(1, Math.floor((bottom - top - 54 - footH) / lineH));
236
+ const body = wrap(cardText(model.notice.body), inner, 26, 400, Math.min(3, room), measure);
237
+ const boxH = 54 + body.length * lineH + footH + (foot ? 14 : 6);
238
+ parts.push(
239
+ `<rect x="${PAD}" y="${top}" width="${CONTENT}" height="${boxH}" rx="18" fill="rgba(242,184,75,0.08)" stroke="${COLOR.amber}" stroke-width="2"/>`,
240
+ textEl(cardText(model.notice.heading).toUpperCase(), {
241
+ x: PAD + 28,
242
+ y: top + 40,
243
+ size: 20,
244
+ weight: 700,
245
+ fill: COLOR.amber,
246
+ letterSpacing: 2
247
+ })
248
+ );
249
+ body.forEach(
250
+ (line, i) => parts.push(
251
+ textEl(line, {
252
+ x: PAD + 28,
253
+ y: top + 80 + i * lineH,
254
+ size: 26,
255
+ weight: 400,
256
+ fill: COLOR.ink
257
+ })
258
+ )
259
+ );
260
+ if (foot) {
261
+ parts.push(
262
+ textEl(wrap(foot, inner, 22, 400, 1, measure)[0] ?? "", {
263
+ x: PAD + 28,
264
+ y: top + 80 + (body.length - 1) * lineH + 40,
265
+ size: 22,
266
+ weight: 400,
267
+ fill: COLOR.secondary
268
+ })
269
+ );
270
+ }
271
+ } else if (model.detail) {
272
+ const detail = cardText(model.detail);
273
+ const first = y + 56;
274
+ const lineH = 42;
275
+ const room = Math.max(0, Math.floor((bottom - first) / lineH) + 1);
276
+ const lines = wrap(detail, CONTENT, 31, 400, Math.min(2, room), measure);
277
+ lines.forEach(
278
+ (line, i) => parts.push(
279
+ textEl(line, {
280
+ x: PAD,
281
+ y: first + i * lineH,
282
+ size: 31,
283
+ weight: 400,
284
+ fill: COLOR.secondary
285
+ })
286
+ )
287
+ );
288
+ }
289
+ return `<svg xmlns="http://www.w3.org/2000/svg" width="${CARD_WIDTH}" height="${CARD_HEIGHT}" viewBox="0 0 ${CARD_WIDTH} ${CARD_HEIGHT}">${parts.join("")}</svg>`;
290
+ }
291
+
292
+ // src/card/render.ts
293
+ var FONT_FILES = ["Geist-Regular.ttf", "Geist-SemiBold.ttf", "Geist-Bold.ttf"];
294
+ function defaultFontDir() {
295
+ let dir = dirname(fileURLToPath(import.meta.url));
296
+ for (let i = 0; i < 4; i += 1) {
297
+ const candidate = join(dir, "assets", "fonts");
298
+ if (existsSync(join(candidate, FONT_FILES[0]))) return candidate;
299
+ dir = dirname(dir);
300
+ }
301
+ throw new Error("The card font (assets/fonts/Geist-*.ttf) is missing from this install.");
302
+ }
303
+ var resvgModule;
304
+ function loadResvg(load = createRequire(import.meta.url)) {
305
+ if (resvgModule) return resvgModule;
306
+ try {
307
+ resvgModule = load("@resvg/resvg-js");
308
+ } catch (error) {
309
+ const reason = error instanceof Error ? error.message.split("\n")[0] ?? "" : String(error);
310
+ throw new Error(
311
+ `the card renderer (@resvg/resvg-js) could not load on ${process.platform}-${process.arch}; run with --no-card for the text answer (${reason})`,
312
+ { cause: error }
313
+ );
314
+ }
315
+ return resvgModule;
316
+ }
317
+ function loadFonts(fontDir = defaultFontDir()) {
318
+ const files = FONT_FILES.map((f) => join(fontDir, f));
319
+ for (const file of files) {
320
+ if (!existsSync(file)) throw new Error(`The card font ${file} is missing from this install.`);
321
+ }
322
+ return { files };
323
+ }
324
+ function options(fonts) {
325
+ return {
326
+ font: {
327
+ fontFiles: fonts.files,
328
+ loadSystemFonts: false,
329
+ defaultFontFamily: CARD_FONT_FAMILY
330
+ },
331
+ fitTo: { mode: "original" },
332
+ shapeRendering: 2,
333
+ textRendering: 1,
334
+ imageRendering: 0
335
+ };
336
+ }
337
+ var escapeForMeasure = (s) => s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
338
+ function createMeasure(fonts) {
339
+ const cache = /* @__PURE__ */ new Map();
340
+ return (text2, size, weight, letterSpacing = 0) => {
341
+ const key = `${size}|${weight}|${letterSpacing}|${text2}`;
342
+ const hit = cache.get(key);
343
+ if (hit !== void 0) return hit;
344
+ const svg = (t) => `<svg xmlns="http://www.w3.org/2000/svg" width="8000" height="400"><text x="0" y="300" font-family="${CARD_FONT_FAMILY}" font-size="${size}" font-weight="${weight}" letter-spacing="${letterSpacing}" xml:space="preserve">${escapeForMeasure(t)}</text></svg>`;
345
+ const { Resvg } = loadResvg();
346
+ const width = (t) => new Resvg(svg(t), options(fonts)).getBBox()?.width ?? 0;
347
+ const bars = width("||");
348
+ const value = text2 === "" ? 0 : Math.max(0, width(`|${text2}|`) - bars);
349
+ cache.set(key, value);
350
+ return value;
351
+ };
352
+ }
353
+ function renderPng(svg, fonts) {
354
+ const { Resvg } = loadResvg();
355
+ return new Resvg(svg, options(fonts)).render().asPng();
356
+ }
357
+
358
+ // src/run.ts
359
+ import { isAbsolute, relative } from "path";
360
+ import { parseArgs } from "util";
361
+
362
+ // src/chain.ts
363
+ var CHAIN_NAME = "Robinhood Chain";
364
+ var CHAIN_ID = 4663;
365
+ var CAIP2 = "eip155:4663";
366
+
367
+ // src/exit-codes.ts
368
+ var EXIT = {
369
+ /** HEY answered: a published project, with its builder evidence. */
370
+ OK: 0,
371
+ /** The answer printed, but the card could not be written (bad path, symlink, no permission). */
372
+ CARD_FAILED: 1,
373
+ /** Usage error: a bad flag, input that is not an address/ticker/name/handle, `unsupported_chain`. */
374
+ USAGE: 2,
375
+ /** HEY refused the request (an `invalid_request` other than `ambiguous`, a 4xx other than 404/429). */
376
+ REFUSED: 3,
377
+ /** HEY holds no published project for the input (`not_found`). */
378
+ NOT_FOUND: 4,
379
+ /** Rate limited (429): retry after the printed delay. */
380
+ RATE_LIMITED: 5,
381
+ /** Network failure, timeout, redirect, an unreadable or oversized answer, or a HEY 5xx. */
382
+ NETWORK: 6,
383
+ /** More than one published project matches: HEY lists them and does not guess; pick one. */
384
+ AMBIGUOUS: 7
385
+ };
386
+
387
+ // src/errors.ts
388
+ var CheckError = class extends Error {
389
+ constructor(code2, message, exitCode2, details = {}) {
390
+ super(message);
391
+ this.code = code2;
392
+ this.exitCode = exitCode2;
393
+ this.details = details;
394
+ this.name = "CheckError";
395
+ }
396
+ code;
397
+ exitCode;
398
+ details;
399
+ };
400
+ var usageError = (code2, message) => new CheckError(code2, message, EXIT.USAGE);
401
+ var networkError = (code2, message, details = {}) => new CheckError(code2, message, EXIT.NETWORK, { retryable: true, ...details });
402
+
403
+ // src/evm.ts
404
+ var ADDRESS_RE = /^0x[0-9a-fA-F]{40}$/;
405
+ var ZERO_ADDRESS = "0x0000000000000000000000000000000000000000";
406
+ var DEAD_ADDRESS = "0x000000000000000000000000000000000000dead";
407
+
408
+ // src/version.ts
409
+ var CHECK_VERSION = true ? "0.1.1" : "0.0.0-dev";
410
+
411
+ // src/http.ts
412
+ var DEFAULT_BASE_URL = "https://heyresearch.xyz";
413
+ var MAX_RESPONSE_BYTES = 1024 * 1024;
414
+ var TIMEOUT_MS = 15e3;
415
+ var MAX_REQUESTS = 2;
416
+ var USER_AGENT = `hey-check/${CHECK_VERSION} (+https://github.com/hey-research-lab/hey-check)`;
417
+ var FORBIDDEN_KEYS = /* @__PURE__ */ new Set(["__proto__", "constructor", "prototype"]);
418
+ function forbiddenKeyPath(value, path = "$", depth = 0) {
419
+ if (depth > 64) return path;
420
+ if (Array.isArray(value)) {
421
+ for (let i = 0; i < value.length; i += 1) {
422
+ const hit = forbiddenKeyPath(value[i], `${path}[${i}]`, depth + 1);
423
+ if (hit) return hit;
424
+ }
425
+ return null;
426
+ }
427
+ if (value && typeof value === "object") {
428
+ for (const key of Object.keys(value)) {
429
+ if (FORBIDDEN_KEYS.has(key)) return `${path}.${key}`;
430
+ const hit = forbiddenKeyPath(
431
+ value[key],
432
+ `${path}.${key}`,
433
+ depth + 1
434
+ );
435
+ if (hit) return hit;
436
+ }
437
+ }
438
+ return null;
439
+ }
440
+ async function readCapped(response, max) {
441
+ const declared = Number(response.headers.get("content-length"));
442
+ if (Number.isFinite(declared) && declared > max) throw tooLarge(max);
443
+ if (!response.body) return "";
444
+ const reader = response.body.getReader();
445
+ const chunks = [];
446
+ let total = 0;
447
+ for (; ; ) {
448
+ const { done, value } = await reader.read();
449
+ if (done) break;
450
+ total += value.byteLength;
451
+ if (total > max) {
452
+ await reader.cancel().catch(() => void 0);
453
+ throw tooLarge(max);
454
+ }
455
+ chunks.push(value);
456
+ }
457
+ return new TextDecoder("utf-8", { fatal: false }).decode(Buffer.concat(chunks));
458
+ }
459
+ var tooLarge = (max) => new CheckError(
460
+ "response_too_large",
461
+ `HEY's answer was larger than ${max} bytes; hey-check does not read it.`,
462
+ EXIT.NETWORK,
463
+ { retryable: false }
464
+ );
465
+ function parseBaseUrl(raw) {
466
+ let url;
467
+ try {
468
+ url = new URL(raw);
469
+ } catch {
470
+ throw new CheckError("invalid_base_url", "The base URL is not a URL.", EXIT.USAGE);
471
+ }
472
+ const local = url.protocol === "http:" && ["localhost", "127.0.0.1"].includes(url.hostname);
473
+ const bare = url.pathname === "/" && !url.search && !url.hash && !raw.includes("#");
474
+ if (url.protocol !== "https:" && !local || url.username || url.password || !bare) {
475
+ throw new CheckError(
476
+ "invalid_base_url",
477
+ "The base URL must be an https origin (or http on localhost) with no path or query.",
478
+ EXIT.USAGE
479
+ );
480
+ }
481
+ return url.origin;
482
+ }
483
+ var HeyHttp = class {
484
+ constructor(fetchImpl, baseUrl = DEFAULT_BASE_URL) {
485
+ this.fetchImpl = fetchImpl;
486
+ this.baseUrl = baseUrl;
487
+ }
488
+ fetchImpl;
489
+ baseUrl;
490
+ requests = [];
491
+ async getJson(path, params) {
492
+ if (this.requests.length >= MAX_REQUESTS) {
493
+ throw new CheckError(
494
+ "request_budget",
495
+ "hey-check makes at most two requests a run.",
496
+ EXIT.NETWORK
497
+ );
498
+ }
499
+ const url = `${this.baseUrl}${path}?${new URLSearchParams(params).toString()}`;
500
+ const record = { url, status: null, requestId: null };
501
+ this.requests.push(record);
502
+ let response;
503
+ try {
504
+ response = await this.fetchImpl(url, {
505
+ method: "GET",
506
+ redirect: "manual",
507
+ credentials: "omit",
508
+ headers: {
509
+ accept: "application/json",
510
+ "user-agent": USER_AGENT,
511
+ "x-hey-integration": `hey-check/${CHECK_VERSION}`
512
+ },
513
+ signal: AbortSignal.timeout(TIMEOUT_MS)
514
+ });
515
+ } catch (error) {
516
+ const name = error instanceof Error ? error.name : "";
517
+ if (name === "TimeoutError" || name === "AbortError") {
518
+ throw networkError("timeout", `HEY did not answer within ${TIMEOUT_MS / 1e3} seconds.`);
519
+ }
520
+ throw networkError("network", "Could not reach heyresearch.xyz. Check your connection.");
521
+ }
522
+ record.status = response.status;
523
+ record.requestId = response.headers.get("x-request-id");
524
+ const requestId = record.requestId ?? void 0;
525
+ const details = requestId ? { requestId } : {};
526
+ if (response.status === 0 || response.status >= 300 && response.status < 400) {
527
+ throw networkError("redirect", "HEY answered with a redirect; hey-check follows none.", {
528
+ ...details,
529
+ retryable: false
530
+ });
531
+ }
532
+ if (response.status === 429) {
533
+ const header = Number(response.headers.get("retry-after"));
534
+ const retryAfterSeconds = Number.isFinite(header) && header > 0 ? Math.ceil(header) : 60;
535
+ throw new CheckError(
536
+ "rate_limited",
537
+ `HEY is rate limiting requests from here. Wait ${retryAfterSeconds} seconds and try again.`,
538
+ EXIT.RATE_LIMITED,
539
+ { ...details, retryable: true, retryAfterSeconds }
540
+ );
541
+ }
542
+ if (response.status >= 500) {
543
+ throw networkError(
544
+ "server_error",
545
+ `HEY answered ${response.status}. Try again shortly.`,
546
+ details
547
+ );
548
+ }
549
+ const text2 = await readCapped(response, MAX_RESPONSE_BYTES);
550
+ let body;
551
+ try {
552
+ body = JSON.parse(text2);
553
+ } catch {
554
+ throw networkError("invalid_response", `HEY's answer (${response.status}) was not JSON.`, {
555
+ ...details,
556
+ retryable: false
557
+ });
558
+ }
559
+ const bad = forbiddenKeyPath(body);
560
+ if (bad) {
561
+ throw networkError("invalid_response", `HEY's answer carries a forbidden key at ${bad}.`, {
562
+ ...details,
563
+ retryable: false
564
+ });
565
+ }
566
+ return { status: response.status, body, requestId: record.requestId };
567
+ }
568
+ };
569
+
570
+ // src/input.ts
571
+ var MAX_INPUT_LENGTH = 200;
572
+ var HANDLE = /^[A-Za-z0-9_]{1,15}$/;
573
+ var TICKER = /^\$[A-Za-z0-9][A-Za-z0-9._-]{0,19}$/;
574
+ var NAME = /^[\p{L}\p{N}][\p{L}\p{N} ._'&+-]{0,79}$/u;
575
+ var X_RESERVED = /* @__PURE__ */ new Set([
576
+ "home",
577
+ "i",
578
+ "intent",
579
+ "share",
580
+ "search",
581
+ "explore",
582
+ "hashtag",
583
+ "settings",
584
+ "messages",
585
+ "notifications",
586
+ "login",
587
+ "signup",
588
+ "tos",
589
+ "privacy"
590
+ ]);
591
+ var X_HOSTS = /* @__PURE__ */ new Set([
592
+ "x.com",
593
+ "www.x.com",
594
+ "mobile.x.com",
595
+ "twitter.com",
596
+ "www.twitter.com",
597
+ "mobile.twitter.com"
598
+ ]);
599
+ var HELP = "hey-check takes a Robinhood Chain contract address (0x + 40 hex), a $TICKER, a project name or slug, an @handle, or an x.com profile URL.";
600
+ var EMPTY_HINT = "A $TICKER must be quoted with single quotes in a shell ('$CHIT'): unquoted or in double quotes, the shell replaces it with nothing.";
601
+ var WRAPPED_IN_QUOTES = /^(['"\u2018\u2019\u201C\u201D]).+(['"\u2018\u2019\u201C\u201D])$/u;
602
+ function handleFromXUrl(raw) {
603
+ const withScheme = /^https?:\/\//i.test(raw) ? raw : `https://${raw}`;
604
+ let url;
605
+ try {
606
+ url = new URL(withScheme);
607
+ } catch {
608
+ throw usageError("invalid_input", `That is not a URL hey-check can read. ${HELP}`);
609
+ }
610
+ if (!X_HOSTS.has(url.hostname.toLowerCase()) || url.username || url.password || url.port) {
611
+ throw usageError(
612
+ "invalid_input",
613
+ `hey-check reads x.com and twitter.com profile URLs only. ${HELP}`
614
+ );
615
+ }
616
+ const first = url.pathname.split("/").filter(Boolean)[0] ?? "";
617
+ if (!HANDLE.test(first) || X_RESERVED.has(first.toLowerCase())) {
618
+ throw usageError("invalid_input", "That x.com URL does not name a profile (x.com/<handle>).");
619
+ }
620
+ return first;
621
+ }
622
+ function parseInput(value) {
623
+ if (value === void 0) throw usageError("missing_argument", `Nothing to check. ${HELP}`);
624
+ if (/[\u0000-\u001F\u007F-\u009F\u200B-\u200F\u202A-\u202E\u2066-\u2069]/.test(value)) {
625
+ throw usageError("invalid_input", "The input carries control or invisible characters.");
626
+ }
627
+ const raw = value.trim();
628
+ if (raw === "") throw usageError("missing_argument", `Nothing to check. ${EMPTY_HINT}`);
629
+ if (raw.length > MAX_INPUT_LENGTH) {
630
+ throw usageError("invalid_input", `The input is longer than ${MAX_INPUT_LENGTH} characters.`);
631
+ }
632
+ if (WRAPPED_IN_QUOTES.test(raw)) {
633
+ throw usageError(
634
+ "invalid_input",
635
+ `The input arrived with its quotes (${raw}). Your shell passed them on as part of it \u2014 cmd.exe does \u2014 so leave them out there: hey-check ${raw.slice(1, -1)}`
636
+ );
637
+ }
638
+ const caip = /^eip155:(\d+):(.+)$/i.exec(raw);
639
+ if (caip) {
640
+ if (Number(caip[1]) !== CHAIN_ID || caip[1] !== String(CHAIN_ID)) {
641
+ throw usageError(
642
+ "unsupported_chain",
643
+ `HEY supports Robinhood Chain (4663) only; chain ${caip[1]} is not supported.`
644
+ );
645
+ }
646
+ return parseAddress(raw, caip[2] ?? "");
647
+ }
648
+ if (/^0x/i.test(raw)) return parseAddress(raw, raw);
649
+ if (/^(https?:\/\/)?([a-z0-9-]+\.)*(x|twitter)\.com(\/|$)/i.test(raw)) {
650
+ const handle = handleFromXUrl(raw);
651
+ return { kind: "x_profile", raw, q: `@${handle}` };
652
+ }
653
+ if (/^[a-z][a-z0-9+.-]*:/i.test(raw) || raw.includes("/")) {
654
+ throw usageError("invalid_input", `hey-check does not read that kind of URL. ${HELP}`);
655
+ }
656
+ if (raw.startsWith("@")) {
657
+ const handle = raw.slice(1);
658
+ if (!HANDLE.test(handle)) {
659
+ throw usageError("invalid_input", "An @handle is 1\u201315 letters, digits or underscores.");
660
+ }
661
+ return { kind: "handle", raw, q: `@${handle}` };
662
+ }
663
+ if (raw.startsWith("$")) {
664
+ if (!TICKER.test(raw)) {
665
+ throw usageError("invalid_input", "A $TICKER is $ followed by 1\u201320 letters or digits.");
666
+ }
667
+ return { kind: "ticker", raw, q: raw };
668
+ }
669
+ if (!NAME.test(raw)) throw usageError("invalid_input", `That does not look like a name. ${HELP}`);
670
+ return { kind: "name", raw, q: raw };
671
+ }
672
+ function parseAddress(raw, candidate) {
673
+ const normalised = /^0X/.test(candidate) ? `0x${candidate.slice(2)}` : candidate;
674
+ if (!ADDRESS_RE.test(normalised)) {
675
+ throw usageError(
676
+ "invalid_address",
677
+ "A contract address is 0x followed by exactly 40 hexadecimal characters."
678
+ );
679
+ }
680
+ const address2 = normalised.toLowerCase();
681
+ if (address2 === ZERO_ADDRESS || address2 === DEAD_ADDRESS) {
682
+ throw usageError(
683
+ "not_a_contract_identity",
684
+ "The zero and burn addresses are not a contract identity."
685
+ );
686
+ }
687
+ return { kind: "address", raw, address: address2 };
688
+ }
689
+ function queryFor(input) {
690
+ return input.kind === "address" ? { key: "address", value: input.address } : { key: "q", value: input.q };
691
+ }
692
+
693
+ // src/text.ts
694
+ var CONTROL = /[\u0000-\u0008\u000B-\u001F\u007F-\u009F]/g;
695
+ var INVISIBLE = /[\u00AD\u061C\u180E\u200B-\u200F\u202A-\u202E\u2060-\u2064\u2066-\u2069\u3164\uFE00-\uFE0F\uFEFF\uFFA0\u{E0000}-\u{E007F}\u{E0100}-\u{E01EF}]/gu;
696
+ var WHITESPACE = /\s+/g;
697
+ function cleanText(value, max = 280) {
698
+ const folded = value.toWellFormed().replace(/[\t\n\r]/g, " ").replace(CONTROL, "").replace(INVISIBLE, "").replace(/\uFFFD/g, "").replace(WHITESPACE, " ").trim();
699
+ const chars = [...folded];
700
+ return chars.length > max ? `${chars.slice(0, max - 1).join("")}\u2026` : folded;
701
+ }
702
+ function cleanUrl(value) {
703
+ if (typeof value !== "string" || value.length > 600) return null;
704
+ const cleaned = cleanText(value, 600);
705
+ if (!/^https:\/\/\S+$/.test(cleaned)) return null;
706
+ try {
707
+ const url = new URL(cleaned);
708
+ if (url.protocol !== "https:" || url.username || url.password) return null;
709
+ return cleaned;
710
+ } catch {
711
+ return null;
712
+ }
713
+ }
714
+ function firstSentence(text2) {
715
+ const match = /^(.+?[.!?])(?=\s|$)/u.exec(text2);
716
+ return (match?.[1] ?? text2).trim();
717
+ }
718
+ var plainStyle = { strong: (s) => s, dim: (s) => s };
719
+ var ansiStyle = {
720
+ strong: (s) => `\x1B[1m${s}\x1B[22m`,
721
+ dim: (s) => `\x1B[2m${s}\x1B[22m`
722
+ };
723
+
724
+ // src/hey.ts
725
+ var RESEARCH_PATH = "/api/agent/research_project";
726
+ var SCAN_PATH = "/api/v1/scan";
727
+ var SLUG = /^[a-z0-9][a-z0-9-]{0,119}$/;
728
+ var obj = (v) => v !== null && typeof v === "object" && !Array.isArray(v) ? v : null;
729
+ var arr = (v) => Array.isArray(v) ? v : [];
730
+ var rawStr = (v) => typeof v === "string" ? v : null;
731
+ var text = (v, max = 280) => {
732
+ const s = rawStr(v) ?? rawStr(obj(v)?.text);
733
+ if (s === null) return null;
734
+ const cleaned = cleanText(s, max);
735
+ return cleaned === "" ? null : cleaned;
736
+ };
737
+ var code = (v) => typeof v === "string" && /^[A-Z][A-Z0-9_]{0,40}$/.test(v) ? v : null;
738
+ var lowerCode = (v) => typeof v === "string" && /^[a-z][a-z0-9_]{0,60}$/.test(v) ? v : null;
739
+ var instant = (v) => typeof v === "string" && /^\d{4}-\d{2}-\d{2}(T[\d:.]+(Z|[+-]\d{2}:?\d{2}))?$/.test(v) && !Number.isNaN(Date.parse(v)) ? v : null;
740
+ var address = (v) => typeof v === "string" && ADDRESS_RE.test(v) ? v.toLowerCase() : null;
741
+ var slug = (v) => typeof v === "string" && SLUG.test(v) ? v : null;
742
+ var count = (v) => typeof v === "number" && Number.isInteger(v) && v >= 0 ? v : null;
743
+ var evidenceId = (v) => typeof v === "string" && /^[a-z0-9]{2,20}:[A-Za-z0-9:._-]{1,200}$/.test(v) ? v : null;
744
+ function disclaimerOf(body) {
745
+ return text(obj(body.boundaries)?.disclaimer, 600);
746
+ }
747
+ function parseAnswered(body, input) {
748
+ const data = obj(body.data);
749
+ const identity = obj(data?.identity);
750
+ const subject = obj(obj(body.subject)?.project);
751
+ const projectSlug = slug(identity?.slug) ?? slug(subject?.slug);
752
+ if (!data || !identity || !projectSlug) {
753
+ throw networkError("invalid_response", "HEY's answer did not carry the project it names.", {
754
+ retryable: false
755
+ });
756
+ }
757
+ const page = cleanUrl(obj(body.links)?.page) ?? cleanUrl(identity.url) ?? `https://heyresearch.xyz/project/${projectSlug}`;
758
+ const state = obj(data.builderState);
759
+ const change = obj(data.latestMeaningfulChange);
760
+ const evidence = obj(arr(change?.evidence)[0]);
761
+ const token = obj(obj(data.contractIdentity)?.token);
762
+ const tokenAddress = address(token?.address);
763
+ const seen = /* @__PURE__ */ new Set();
764
+ const unknowns = [];
765
+ for (const entry of arr(body.unknowns)) {
766
+ const u = obj(entry);
767
+ const category = code(u?.category);
768
+ const statement = text(u?.statement);
769
+ if (!u || !category || !statement) continue;
770
+ const key = `${category}|${statement}`;
771
+ if (seen.has(key)) continue;
772
+ seen.add(key);
773
+ unknowns.push({
774
+ category,
775
+ dimension: rawStr(u.dimension) ? cleanText(String(u.dimension), 60) : null,
776
+ text: statement,
777
+ reason: lowerCode(u.reason)
778
+ });
779
+ }
780
+ const asked = input.kind === "address" ? input.address : null;
781
+ const answerText = text(body.answer, 1200);
782
+ return {
783
+ outcome: "answered",
784
+ answer: answerText ? firstSentence(answerText) : null,
785
+ project: {
786
+ slug: projectSlug,
787
+ name: text(identity.name, 120) ?? text(subject?.name, 120) ?? projectSlug,
788
+ symbol: text(identity.symbol, 24),
789
+ url: cleanUrl(identity.url) ?? page,
790
+ researchLevel: code(identity.researchLevel),
791
+ catalogStatus: code(identity.catalogStatus)
792
+ },
793
+ builder: {
794
+ activityStatus: code(state?.activityStatus) ?? "UNKNOWN",
795
+ activityMeasured: typeof state?.activityMeasured === "boolean" ? state.activityMeasured : null,
796
+ lastMeaningfulShipAt: instant(state?.lastMeaningfulShipAt),
797
+ meaningfulEvents30d: count(state?.meaningfulEvents30d)
798
+ },
799
+ latestShip: change ? {
800
+ id: evidenceId(change.id),
801
+ title: text(change.summary, 200),
802
+ occurredAt: instant(change.occurredAt),
803
+ precision: code(change.precision),
804
+ sourceUrl: cleanUrl(evidence?.sourceUrl) ?? cleanUrl(obj(change.summary)?.sourceUrl),
805
+ receiptUrl: cleanUrl(evidence?.receiptUrl)
806
+ } : null,
807
+ contract: tokenAddress ? { address: tokenAddress, verification: code(token?.verification) } : null,
808
+ askedAddress: asked && asked !== tokenAddress ? asked : null,
809
+ unknowns,
810
+ citation: text(obj(body.citation)?.text, 400),
811
+ page,
812
+ asOf: instant(body.asOf),
813
+ disclaimer: disclaimerOf(body)
814
+ };
815
+ }
816
+ function parseAmbiguous(body, error) {
817
+ const candidates = [];
818
+ for (const entry of arr(error.candidates)) {
819
+ const c = obj(entry);
820
+ const s = slug(c?.slug);
821
+ if (!c || !s) continue;
822
+ const token = obj(c.token);
823
+ candidates.push({
824
+ slug: s,
825
+ name: text(c.name, 120),
826
+ symbol: text(c.symbol, 24),
827
+ url: cleanUrl(c.url),
828
+ activityStatus: code(c.activityStatus),
829
+ catalogStatus: code(c.catalogStatus),
830
+ tokenAddress: token && token.chainId === CHAIN_ID ? address(token.address) : null
831
+ });
832
+ }
833
+ const total = obj(error.candidatesTotal);
834
+ const totalCount = count(total?.count);
835
+ return {
836
+ outcome: "ambiguous",
837
+ message: text(error.message, 1200) ?? "More than one published project matches. HEY does not guess: pick one.",
838
+ candidates,
839
+ candidatesTotal: totalCount === null ? null : { count: totalCount, atLeast: total?.atLeast === true },
840
+ asOf: instant(body.asOf),
841
+ disclaimer: disclaimerOf(body)
842
+ };
843
+ }
844
+ function parseNamesakes(scan) {
845
+ const block = obj(obj(scan)?.publishedNamesakes);
846
+ if (!block) return null;
847
+ const items = [];
848
+ for (const entry of arr(block.items)) {
849
+ const n = obj(entry);
850
+ const name = text(n?.name, 120);
851
+ if (!n || !name) continue;
852
+ items.push({
853
+ name,
854
+ slug: slug(n.slug),
855
+ symbol: text(n.symbol, 24),
856
+ contractAddress: address(n.contractAddress),
857
+ shares: lowerCode(n.shares),
858
+ sharesAddressEnding: typeof n.sharesAddressEnding === "boolean" ? n.sharesAddressEnding : null,
859
+ url: cleanUrl(n.url)
860
+ });
861
+ }
862
+ const total = count(block.total) ?? items.length;
863
+ if (total === 0 && items.length === 0) return null;
864
+ return { total, items, line: text(block.line, 600) };
865
+ }
866
+ function bodyOf(answer) {
867
+ const body = obj(answer.body);
868
+ if (!body || typeof body.status !== "string") {
869
+ throw networkError("invalid_response", "HEY's answer was not an agent answer.", {
870
+ retryable: false
871
+ });
872
+ }
873
+ return body;
874
+ }
875
+ async function check(input, http) {
876
+ const first = queryFor(input);
877
+ let body = bodyOf(await http.getJson(RESEARCH_PATH, { [first.key]: first.value }));
878
+ if (body.status === "invalid_request" && obj(body.error)?.code === "ambiguous") {
879
+ const typed = input.kind === "name" ? input.q.toLowerCase() : null;
880
+ const exact = typed && SLUG.test(typed) ? arr(obj(body.error)?.candidates).some((c) => obj(c)?.slug === typed) : false;
881
+ if (typed && exact) {
882
+ body = bodyOf(await http.getJson(RESEARCH_PATH, { project: typed }));
883
+ }
884
+ }
885
+ const error = obj(body.error);
886
+ switch (body.status) {
887
+ case "ok":
888
+ return parseAnswered(body, input);
889
+ case "invalid_request":
890
+ if (error?.code === "ambiguous") return parseAmbiguous(body, error);
891
+ throw new CheckError(
892
+ lowerCode(error?.code) ?? "invalid_request",
893
+ text(error?.message, 600) ?? "HEY refused the request.",
894
+ EXIT.REFUSED
895
+ );
896
+ case "not_found": {
897
+ const message = text(error?.message, 1200) ?? text(body.answer, 1200) ?? "HEY holds no published project for that.";
898
+ if (input.kind !== "address") {
899
+ return {
900
+ outcome: "not_found",
901
+ message,
902
+ address: null,
903
+ scanUrl: null,
904
+ namesakes: null,
905
+ namesakesRead: false,
906
+ asOf: instant(body.asOf),
907
+ disclaimer: disclaimerOf(body)
908
+ };
909
+ }
910
+ let scanBody = null;
911
+ try {
912
+ const scan = await http.getJson(SCAN_PATH, {
913
+ chain: String(CHAIN_ID),
914
+ token: input.address
915
+ });
916
+ scanBody = scan.status < 400 ? obj(scan.body) : null;
917
+ } catch {
918
+ scanBody = null;
919
+ }
920
+ return {
921
+ outcome: "not_found",
922
+ message,
923
+ address: input.address,
924
+ scanUrl: cleanUrl(scanBody?.scan_url) ?? `https://heyresearch.xyz/scan/${input.address}`,
925
+ namesakes: parseNamesakes(scanBody),
926
+ namesakesRead: scanBody !== null,
927
+ asOf: instant(body.asOf),
928
+ disclaimer: disclaimerOf(body)
929
+ };
930
+ }
931
+ case "unavailable":
932
+ throw networkError(
933
+ "unavailable",
934
+ text(error?.message, 600) ?? "HEY could not answer right now. Try again shortly."
935
+ );
936
+ case "moved":
937
+ throw new CheckError(
938
+ "moved",
939
+ text(body.answer, 600) ?? "HEY says this project moved; check its new name or slug.",
940
+ EXIT.REFUSED
941
+ );
942
+ default:
943
+ throw networkError("invalid_response", `HEY answered an unknown status.`, {
944
+ retryable: false
945
+ });
946
+ }
947
+ }
948
+
949
+ // src/words.ts
950
+ var STATUS_LABEL = {
951
+ SHIPPING: "Shipping",
952
+ ACTIVE: "Active",
953
+ RESUMED: "Resumed",
954
+ QUIET: "Quiet",
955
+ DORMANT: "Dormant",
956
+ UNKNOWN: "Unknown"
957
+ };
958
+ var statusLabel = (code2) => STATUS_LABEL[code2] ?? "Unknown";
959
+ var isBuilding = (code2) => code2 === "SHIPPING" || code2 === "ACTIVE" || code2 === "RESUMED";
960
+ var BOUNDARY = "Builder evidence, not a safety verdict or investment advice.";
961
+ var shortAddress = (address2) => `${address2.slice(0, 6)}\u2026${address2.slice(-4)}`;
962
+ var HOUR = 36e5;
963
+ var DAY = 24 * HOUR;
964
+ function ago(at, now) {
965
+ const ms = now.getTime() - Date.parse(at);
966
+ if (!Number.isFinite(ms) || ms < HOUR) return ms < 0 ? "just now" : "under an hour ago";
967
+ if (ms < DAY) {
968
+ const h = Math.floor(ms / HOUR);
969
+ return `${h} hour${h === 1 ? "" : "s"} ago`;
970
+ }
971
+ const d = Math.floor(ms / DAY);
972
+ return `${d} day${d === 1 ? "" : "s"} ago`;
973
+ }
974
+ var isoDay = (at) => new Date(Date.parse(at)).toISOString().slice(0, 10);
975
+ var MONTHS = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"];
976
+ function humanDay(at) {
977
+ return `${at.getUTCDate()} ${MONTHS[at.getUTCMonth()]} ${at.getUTCFullYear()}`;
978
+ }
979
+ function lastShipLine(builder, now) {
980
+ if (builder.lastMeaningfulShipAt) return `Last shipped ${ago(builder.lastMeaningfulShipAt, now)}`;
981
+ if (builder.meaningfulEvents30d === 0) return "No meaningful ship recorded in 30 days";
982
+ return "Last ship not known to HEY";
983
+ }
984
+ function eventsLine(n) {
985
+ if (n === null) return "Meaningful events in 30 days: not measured";
986
+ return `${n} meaningful event${n === 1 ? "" : "s"} in 30 days`;
987
+ }
988
+ function verificationWords(v) {
989
+ switch (v) {
990
+ case "VERIFIED":
991
+ return "Project names this contract";
992
+ case "UNVERIFIED":
993
+ return "Not named by the project";
994
+ case "MISMATCH":
995
+ return "Project names another contract";
996
+ default:
997
+ return "token check unknown";
998
+ }
999
+ }
1000
+ function sharesPhrase(shares) {
1001
+ switch (shares) {
1002
+ case "name_and_ticker":
1003
+ return "carries the same name and ticker";
1004
+ case "name":
1005
+ return "carries the same name";
1006
+ case "ticker":
1007
+ return "carries the same ticker";
1008
+ default:
1009
+ return "carries its name or ticker";
1010
+ }
1011
+ }
1012
+ function namesakeSentence(n) {
1013
+ const where = n.contractAddress ? `on a different contract, ${shortAddress(n.contractAddress)}` : "on a different contract";
1014
+ const ending = n.sharesAddressEnding ? ", and its address ends in the same four characters" : "";
1015
+ return `${n.name} is published on HEY ${where}; this token ${sharesPhrase(n.shares)}${ending}.`;
1016
+ }
1017
+ var CONTRACT_IS_IDENTITY = "On chain a token is its contract, never its name or ticker.";
1018
+
1019
+ // src/card/model.ts
1020
+ var host = (url) => url.replace(/^https:\/\//, "").replace(/\/$/, "");
1021
+ function cardModel(result, input, now) {
1022
+ const checkedOn = `Checked ${humanDay(now)} UTC`;
1023
+ switch (result.outcome) {
1024
+ case "answered": {
1025
+ const { project, builder, contract } = result;
1026
+ const facts = [eventsLine(builder.meaningfulEvents30d)];
1027
+ if (contract) {
1028
+ facts.push(
1029
+ `${shortAddress(contract.address)}${contract.verification === "VERIFIED" ? ` \xB7 ${verificationWords("VERIFIED")}` : ""}`
1030
+ );
1031
+ }
1032
+ return {
1033
+ // A name the card's font cannot draw at all (emoji, CJK …) falls back to the slug.
1034
+ title: cardText(project.name) ? project.name : project.slug,
1035
+ ticker: project.symbol ? `$${project.symbol.replace(/^\$/, "")}` : null,
1036
+ pill: {
1037
+ label: statusLabel(builder.activityStatus),
1038
+ tone: isBuilding(builder.activityStatus) ? "lime" : "grey"
1039
+ },
1040
+ headline: lastShipLine(builder, now),
1041
+ detail: result.latestShip?.title ?? null,
1042
+ facts,
1043
+ notice: null,
1044
+ footerUrl: host(result.page),
1045
+ checkedOn
1046
+ };
1047
+ }
1048
+ case "not_found": {
1049
+ if (result.address) {
1050
+ const n = result.namesakes?.items[0];
1051
+ const others = (result.namesakes?.total ?? 0) - 1;
1052
+ return {
1053
+ title: shortAddress(result.address),
1054
+ ticker: null,
1055
+ pill: null,
1056
+ headline: "Not a published project on HEY",
1057
+ detail: n ? null : "HEY holds no published project for this contract. That is a statement about HEY's index, not about the token.",
1058
+ facts: [],
1059
+ notice: n ? {
1060
+ heading: "Same name on another contract",
1061
+ body: `${namesakeSentence(n)}${others > 0 ? ` ${others} more published project${others === 1 ? "" : "s"} share it.` : ""}`,
1062
+ foot: CONTRACT_IS_IDENTITY
1063
+ } : null,
1064
+ footerUrl: host(result.scanUrl ?? `https://heyresearch.xyz/scan/${result.address}`),
1065
+ checkedOn
1066
+ };
1067
+ }
1068
+ return {
1069
+ title: input.raw,
1070
+ ticker: null,
1071
+ pill: null,
1072
+ headline: "Not a published project on HEY",
1073
+ detail: "HEY holds no published project matching that name or ticker. That is a statement about HEY's index, not about the project.",
1074
+ facts: [],
1075
+ notice: null,
1076
+ footerUrl: "heyresearch.xyz",
1077
+ checkedOn
1078
+ };
1079
+ }
1080
+ case "ambiguous":
1081
+ return null;
1082
+ }
1083
+ }
1084
+
1085
+ // src/card/write.ts
1086
+ import { closeSync, constants, lstatSync, openSync, statSync, writeSync } from "fs";
1087
+ import { dirname as dirname2, resolve } from "path";
1088
+ var cardError = (message) => new CheckError("card_not_written", message, EXIT.CARD_FAILED);
1089
+ function defaultCardName(result, input) {
1090
+ let stem;
1091
+ if (result.outcome === "answered") stem = result.project.slug;
1092
+ else if (input.kind === "address")
1093
+ stem = `${input.address.slice(0, 6)}-${input.address.slice(-4)}`;
1094
+ else {
1095
+ stem = input.q.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 40);
1096
+ }
1097
+ return `hey-check-${stem || "result"}.png`;
1098
+ }
1099
+ function writeCard(path, png, cwd) {
1100
+ if (!path.toLowerCase().endsWith(".png")) throw cardError("The card path must end in .png.");
1101
+ const target = resolve(cwd, path);
1102
+ let parent;
1103
+ try {
1104
+ parent = statSync(dirname2(target));
1105
+ } catch {
1106
+ throw cardError(`The directory ${dirname2(target)} does not exist.`);
1107
+ }
1108
+ if (!parent.isDirectory()) throw cardError(`${dirname2(target)} is not a directory.`);
1109
+ let existing;
1110
+ try {
1111
+ existing = lstatSync(target);
1112
+ } catch {
1113
+ existing = null;
1114
+ }
1115
+ if (existing?.isSymbolicLink()) {
1116
+ throw cardError(`${target} is a symbolic link; hey-check does not write through links.`);
1117
+ }
1118
+ if (existing && !existing.isFile()) throw cardError(`${target} exists and is not a file.`);
1119
+ const noFollow = typeof constants.O_NOFOLLOW === "number" ? constants.O_NOFOLLOW : 0;
1120
+ let fd;
1121
+ try {
1122
+ fd = openSync(
1123
+ target,
1124
+ constants.O_WRONLY | constants.O_CREAT | constants.O_TRUNC | noFollow,
1125
+ 420
1126
+ );
1127
+ } catch (error) {
1128
+ const code2 = error.code;
1129
+ if (code2 === "ELOOP")
1130
+ throw cardError(`${target} is a symbolic link; hey-check does not write through links.`);
1131
+ throw cardError(`Could not write ${target}${code2 ? ` (${code2})` : ""}.`);
1132
+ }
1133
+ try {
1134
+ writeSync(fd, png);
1135
+ } finally {
1136
+ closeSync(fd);
1137
+ }
1138
+ return target;
1139
+ }
1140
+
1141
+ // src/json.ts
1142
+ var JSON_SCHEMA = "hey.check/v1";
1143
+ function emptyDocument(now, input, requests) {
1144
+ const sent = input ? queryFor(input) : null;
1145
+ return {
1146
+ schema: JSON_SCHEMA,
1147
+ ok: false,
1148
+ outcome: null,
1149
+ chain: { name: CHAIN_NAME, chainId: CHAIN_ID, caip2: CAIP2 },
1150
+ input: input && sent ? { raw: input.raw, kind: input.kind, sent: { [sent.key]: sent.value } } : null,
1151
+ answer: null,
1152
+ project: null,
1153
+ builder: null,
1154
+ latestShip: null,
1155
+ contract: null,
1156
+ unknowns: null,
1157
+ candidates: null,
1158
+ candidatesTotal: null,
1159
+ notFound: null,
1160
+ citation: null,
1161
+ page: null,
1162
+ card: null,
1163
+ tweet: null,
1164
+ boundary: BOUNDARY,
1165
+ disclaimer: null,
1166
+ error: null,
1167
+ source: { requests, asOf: null, fetchedAt: now.toISOString() }
1168
+ };
1169
+ }
1170
+ function resultDocument(result, base, extra) {
1171
+ const doc = {
1172
+ ...base,
1173
+ ok: true,
1174
+ outcome: result.outcome,
1175
+ card: extra.cardPath ? { path: extra.cardPath } : null,
1176
+ tweet: extra.tweet,
1177
+ disclaimer: result.disclaimer,
1178
+ source: { ...base.source, asOf: result.asOf }
1179
+ };
1180
+ switch (result.outcome) {
1181
+ case "answered":
1182
+ return {
1183
+ ...doc,
1184
+ answer: result.answer,
1185
+ project: { ...result.project },
1186
+ builder: { ...result.builder, activityLabel: statusLabel(result.builder.activityStatus) },
1187
+ latestShip: result.latestShip ? { ...result.latestShip } : null,
1188
+ contract: result.contract ? { ...result.contract } : null,
1189
+ unknowns: result.unknowns.map((u) => ({ ...u })),
1190
+ citation: result.citation,
1191
+ page: result.page
1192
+ };
1193
+ case "ambiguous":
1194
+ return {
1195
+ ...doc,
1196
+ answer: result.message,
1197
+ candidates: result.candidates.map((c) => ({ ...c })),
1198
+ candidatesTotal: result.candidatesTotal
1199
+ };
1200
+ case "not_found":
1201
+ return {
1202
+ ...doc,
1203
+ answer: result.message,
1204
+ notFound: {
1205
+ message: result.message,
1206
+ address: result.address,
1207
+ scanUrl: result.scanUrl,
1208
+ namesakesRead: result.namesakesRead,
1209
+ namesakes: result.namesakes ? {
1210
+ total: result.namesakes.total,
1211
+ line: result.namesakes.line,
1212
+ items: result.namesakes.items.map((n) => ({ ...n }))
1213
+ } : null
1214
+ },
1215
+ page: result.scanUrl
1216
+ };
1217
+ }
1218
+ }
1219
+ function errorDocument(error, base) {
1220
+ return {
1221
+ ...base,
1222
+ error: {
1223
+ code: error.code,
1224
+ message: error.message,
1225
+ retryable: error.details.retryable ?? false,
1226
+ ...error.details.requestId ? { requestId: error.details.requestId } : {},
1227
+ ...error.details.retryAfterSeconds !== void 0 ? { retryAfterSeconds: error.details.retryAfterSeconds } : {}
1228
+ }
1229
+ };
1230
+ }
1231
+
1232
+ // src/terminal.ts
1233
+ var LABEL_WIDTH = 14;
1234
+ var row = (s, label, value) => ` ${s.dim(label.padEnd(LABEL_WIDTH))}${value}`;
1235
+ var more = (value) => ` ${" ".repeat(LABEL_WIDTH)}${value}`;
1236
+ function answered(r, ctx) {
1237
+ const s = ctx.style;
1238
+ const lines = [];
1239
+ if (r.answer) lines.push(s.strong(r.answer), "");
1240
+ const ticker = r.project.symbol ? ` $${r.project.symbol.replace(/^\$/, "")}` : "";
1241
+ lines.push(` ${s.strong(r.project.name)}${ticker}`);
1242
+ lines.push(row(s, "Status", s.strong(statusLabel(r.builder.activityStatus))));
1243
+ const at = r.builder.lastMeaningfulShipAt;
1244
+ if (at) {
1245
+ const title = r.latestShip?.title ? ` \u2014 ${r.latestShip.title}` : "";
1246
+ lines.push(row(s, "Last ship", `${ago(at, ctx.now)} (${isoDay(at)})${title}`));
1247
+ if (r.latestShip?.sourceUrl) lines.push(more(s.dim(r.latestShip.sourceUrl)));
1248
+ } else {
1249
+ lines.push(row(s, "Last ship", lastShipLine(r.builder, ctx.now)));
1250
+ }
1251
+ const n = r.builder.meaningfulEvents30d;
1252
+ lines.push(
1253
+ row(
1254
+ s,
1255
+ "Last 30 days",
1256
+ n === null ? "not measured" : `${n} meaningful event${n === 1 ? "" : "s"}`
1257
+ )
1258
+ );
1259
+ if (r.contract) {
1260
+ const v = r.contract.verification;
1261
+ lines.push(row(s, "Contract", `${r.contract.address} \xB7 ${verificationWords(v)}`));
1262
+ } else {
1263
+ lines.push(row(s, "Contract", "no token on HEY\u2019s record for this project"));
1264
+ }
1265
+ if (r.askedAddress) lines.push(row(s, "You asked", r.askedAddress));
1266
+ const shown = r.unknowns.slice(0, 2);
1267
+ shown.forEach((u, i) => {
1268
+ const value = `${u.category} \u2014 ${u.text}`;
1269
+ lines.push(i === 0 ? row(s, "Unknowns", value) : more(value));
1270
+ });
1271
+ if (r.unknowns.length > shown.length) {
1272
+ lines.push(more(s.dim(`+${r.unknowns.length - shown.length} more (--json lists them all)`)));
1273
+ }
1274
+ lines.push(row(s, "Page", r.page));
1275
+ if (ctx.cardPath) lines.push(row(s, "Card", ctx.cardPath));
1276
+ lines.push("");
1277
+ if (r.citation) lines.push(s.dim(r.citation));
1278
+ lines.push(s.dim(BOUNDARY));
1279
+ return lines;
1280
+ }
1281
+ function ambiguous(r, ctx) {
1282
+ const s = ctx.style;
1283
+ const lines = [s.strong("More than one published project matches. HEY does not guess."), ""];
1284
+ const width = Math.min(44, Math.max(12, ...r.candidates.map((c) => c.slug.length)));
1285
+ for (const c of r.candidates) {
1286
+ const name = c.name ?? c.slug;
1287
+ const sym = c.symbol ? ` ($${c.symbol.replace(/^\$/, "")})` : "";
1288
+ const status = c.activityStatus ? ` \xB7 ${statusLabel(c.activityStatus)}` : "";
1289
+ const token = c.tokenAddress ? ` \xB7 ${shortAddress(c.tokenAddress)}` : "";
1290
+ lines.push(` ${s.strong(c.slug.padEnd(width))} ${name}${sym}${status}${token}`);
1291
+ }
1292
+ const total = r.candidatesTotal;
1293
+ if (total && total.count > r.candidates.length) {
1294
+ lines.push(
1295
+ "",
1296
+ s.dim(
1297
+ ` Showing ${r.candidates.length} of ${total.atLeast ? "at least " : ""}${total.count} matches.`
1298
+ )
1299
+ );
1300
+ }
1301
+ const example = r.candidates[0]?.slug ?? "<slug>";
1302
+ lines.push(
1303
+ "",
1304
+ `Pick one: hey-check ${example} (or pass the token's 0x contract address)`,
1305
+ "",
1306
+ s.dim(BOUNDARY)
1307
+ );
1308
+ return lines;
1309
+ }
1310
+ function notFound(r, ctx) {
1311
+ const s = ctx.style;
1312
+ const subject = r.address ?? ctx.input.raw;
1313
+ const lines = [s.strong(`${subject} is not a published project on HEY.`), "", ` ${r.message}`];
1314
+ if (r.namesakes && r.namesakes.items.length > 0) {
1315
+ lines.push("", ` ${s.strong("Same name or ticker on another contract")}`);
1316
+ if (!/different contract/i.test(r.message)) {
1317
+ lines.push(` ${r.namesakes.line ?? namesakeSentence(r.namesakes.items[0])}`);
1318
+ }
1319
+ for (const n of r.namesakes.items) {
1320
+ const sym = n.symbol ? ` ($${n.symbol.replace(/^\$/, "")})` : "";
1321
+ const addr = n.contractAddress ? ` \xB7 ${n.contractAddress}` : "";
1322
+ lines.push(` ${n.name}${sym}${addr}${n.url ? ` \xB7 ${n.url}` : ""}`);
1323
+ }
1324
+ if (r.namesakes.total > r.namesakes.items.length) {
1325
+ lines.push(` ${s.dim(`+${r.namesakes.total - r.namesakes.items.length} more`)}`);
1326
+ }
1327
+ } else if (r.address && !r.namesakesRead) {
1328
+ lines.push("", s.dim(" HEY\u2019s list of published namesakes could not be read this time."));
1329
+ }
1330
+ lines.push("");
1331
+ if (r.scanUrl) lines.push(row(s, "Scan", r.scanUrl));
1332
+ if (ctx.cardPath) lines.push(row(s, "Card", ctx.cardPath));
1333
+ if (r.scanUrl || ctx.cardPath) lines.push("");
1334
+ if (r.address && !r.message.includes(CONTRACT_IS_IDENTITY))
1335
+ lines.push(s.dim(CONTRACT_IS_IDENTITY));
1336
+ lines.push(s.dim(BOUNDARY));
1337
+ return lines;
1338
+ }
1339
+ function renderTerminal(result, ctx) {
1340
+ switch (result.outcome) {
1341
+ case "answered":
1342
+ return answered(result, ctx);
1343
+ case "ambiguous":
1344
+ return ambiguous(result, ctx);
1345
+ case "not_found":
1346
+ return notFound(result, ctx);
1347
+ }
1348
+ }
1349
+ function quietLine(result, now) {
1350
+ switch (result.outcome) {
1351
+ case "answered":
1352
+ return `${result.project.slug} ${statusLabel(result.builder.activityStatus)} ${lastShipLine(result.builder, now)}`;
1353
+ case "ambiguous":
1354
+ return result.candidates.map((c) => c.slug).join("\n");
1355
+ case "not_found":
1356
+ return "not_published";
1357
+ }
1358
+ }
1359
+
1360
+ // src/tweet.ts
1361
+ var TWEET_LIMIT = 280;
1362
+ var TWEET_URL_WEIGHT = 23;
1363
+ var URL_RE = /https?:\/\/\S+/g;
1364
+ function tweetLength(text2) {
1365
+ let total = 0;
1366
+ const rest = text2.replace(URL_RE, () => {
1367
+ total += TWEET_URL_WEIGHT;
1368
+ return "";
1369
+ });
1370
+ for (const ch of rest) {
1371
+ const cp = ch.codePointAt(0) ?? 0;
1372
+ const light = cp <= 4351 || cp >= 8192 && cp <= 8205 || cp >= 8208 && cp <= 8223 || cp >= 8242 && cp <= 8247;
1373
+ total += light ? 1 : 2;
1374
+ }
1375
+ return total;
1376
+ }
1377
+ function fit(build, name) {
1378
+ let n = [...name];
1379
+ let out = build(name);
1380
+ while (tweetLength(out) > TWEET_LIMIT && n.length > 4) {
1381
+ n = n.slice(0, -1);
1382
+ out = build(`${n.join("").trimEnd()}\u2026`);
1383
+ }
1384
+ return out;
1385
+ }
1386
+ function tweetText(result, input, now) {
1387
+ switch (result.outcome) {
1388
+ case "answered": {
1389
+ const { project, builder } = result;
1390
+ const sym = project.symbol ? ` ($${project.symbol.replace(/^\$/, "")})` : "";
1391
+ const label = statusLabel(builder.activityStatus);
1392
+ const facts = `${lastShipLine(builder, now)}; ${eventsLine(builder.meaningfulEvents30d)}.`;
1393
+ return fit(
1394
+ (name) => `Dev check on ${name}${sym}, Robinhood Chain: ${label}. ${facts}
1395
+
1396
+ Builder evidence from HEY Research Lab, not a safety verdict: ${result.page}`,
1397
+ project.name
1398
+ );
1399
+ }
1400
+ case "not_found": {
1401
+ if (result.address) {
1402
+ const n = result.namesakes?.items[0];
1403
+ const namesake = n ? ` ${n.name} is published on a different contract${n.contractAddress ? `, ${shortAddress(n.contractAddress)}` : ""}. On chain a token is its contract, never its name or ticker.` : "";
1404
+ const url = result.scanUrl ?? `https://heyresearch.xyz/scan/${result.address}`;
1405
+ return fit(
1406
+ (name) => `Dev check on ${shortAddress(result.address)}, Robinhood Chain: not a published project on HEY Research Lab.${name}
1407
+
1408
+ ${url}`,
1409
+ namesake
1410
+ );
1411
+ }
1412
+ return fit(
1413
+ (name) => `Dev check on "${name}", Robinhood Chain: no published project on HEY Research Lab matches it. That is a statement about HEY's index, not a verdict.
1414
+
1415
+ https://heyresearch.xyz`,
1416
+ input.raw
1417
+ );
1418
+ }
1419
+ case "ambiguous":
1420
+ return null;
1421
+ }
1422
+ }
1423
+
1424
+ // src/run.ts
1425
+ var OPTIONS = {
1426
+ json: { type: "boolean" },
1427
+ quiet: { type: "boolean" },
1428
+ tweet: { type: "boolean" },
1429
+ "no-card": { type: "boolean" },
1430
+ out: { type: "string", short: "o" },
1431
+ "no-color": { type: "boolean" },
1432
+ help: { type: "boolean", short: "h" },
1433
+ version: { type: "boolean", short: "v" }
1434
+ };
1435
+ var NETWORK_FLAGS = /^--(chain|chain-id|chainid|network|net|rpc|rpc-url)(=|$)/i;
1436
+ var HELP2 = `hey-check \u2014 a dev check for a Robinhood Chain token or project
1437
+
1438
+ Is anyone building it, and what did they ship? Answered from HEY Research Lab's public record
1439
+ (heyresearch.xyz), printed here and drawn as a 1200\xD7675 PNG card.
1440
+
1441
+ Usage
1442
+ hey-check <input> [options]
1443
+
1444
+ Input
1445
+ 0x\u2026 a token or contract address (0x + 40 hex)
1446
+ $TICKER a ticker, e.g. $CHIT (in a shell, quote it: '$CHIT')
1447
+ name or slug a project name, e.g. loom
1448
+ @handle an X handle
1449
+ https://x.com/<handle> an X profile URL
1450
+
1451
+ Options
1452
+ --json print one JSON document (schema hey.check/v1) on stdout
1453
+ --no-card do not draw the PNG card
1454
+ -o, --out <file.png> where to write the card (default ./hey-check-<slug>.png)
1455
+ --tweet print a ready-to-post text (under 280 characters) instead of the report
1456
+ --quiet print one line
1457
+ --no-color no ANSI styling (also NO_COLOR)
1458
+ -h, --help this help
1459
+ -v, --version the version
1460
+
1461
+ Exit codes
1462
+ 0 answered \xB7 1 card not written \xB7 2 bad input \xB7 3 HEY refused \xB7 4 not published on HEY
1463
+ 5 rate limited \xB7 6 network or HEY error \xB7 7 more than one match (pick one)
1464
+
1465
+ Builder evidence, not a safety verdict or investment advice. Robinhood Chain (4663) only.
1466
+ HEY Research Lab is independent and not affiliated with Robinhood.`;
1467
+ async function run(argv, io) {
1468
+ const wantsJson = argv.includes("--json");
1469
+ let input = null;
1470
+ let http = null;
1471
+ const fail = (error) => {
1472
+ if (wantsJson) {
1473
+ const doc = errorDocument(error, emptyDocument(io.now(), input, http?.requests ?? []));
1474
+ io.stdout(`${JSON.stringify(doc, null, 2)}
1475
+ `);
1476
+ } else {
1477
+ io.stderr(`hey-check: ${error.message} (${error.code})
1478
+ `);
1479
+ if (error.details.requestId) io.stderr(`hey-check: request id ${error.details.requestId}
1480
+ `);
1481
+ if (error.exitCode === EXIT.USAGE) io.stderr("hey-check: see `hey-check --help`\n");
1482
+ }
1483
+ return error.exitCode;
1484
+ };
1485
+ try {
1486
+ if (argv.some((a) => NETWORK_FLAGS.test(a))) {
1487
+ throw usageError(
1488
+ "unsupported_chain",
1489
+ "HEY supports Robinhood Chain (4663) only, and it is implicit: hey-check takes no chain or network option."
1490
+ );
1491
+ }
1492
+ let parsed;
1493
+ try {
1494
+ parsed = parseArgs({
1495
+ args: [...argv],
1496
+ options: OPTIONS,
1497
+ allowPositionals: true,
1498
+ strict: true
1499
+ });
1500
+ } catch (error) {
1501
+ throw usageError("invalid_option", error instanceof Error ? error.message : String(error));
1502
+ }
1503
+ const { values, positionals } = parsed;
1504
+ if (values.version) {
1505
+ io.stdout(`${CHECK_VERSION}
1506
+ `);
1507
+ return EXIT.OK;
1508
+ }
1509
+ if (values.help) {
1510
+ io.stdout(`${HELP2}
1511
+ `);
1512
+ return EXIT.OK;
1513
+ }
1514
+ if (positionals.length === 0) {
1515
+ if (wantsJson)
1516
+ throw usageError("missing_argument", "Nothing to check; see `hey-check --help`.");
1517
+ io.stderr(`${HELP2}
1518
+ `);
1519
+ return EXIT.USAGE;
1520
+ }
1521
+ if (positionals.length > 1) {
1522
+ positionals.splice(0, positionals.length, positionals.join(" "));
1523
+ }
1524
+ if (values.out !== void 0 && values["no-card"]) {
1525
+ throw usageError("invalid_option", "--out and --no-card cannot be used together.");
1526
+ }
1527
+ if (values.out !== void 0 && !values.out.toLowerCase().endsWith(".png")) {
1528
+ throw usageError("invalid_option", "--out takes a path ending in .png.");
1529
+ }
1530
+ input = parseInput(positionals[0]);
1531
+ const baseUrl = io.env.HEY_BASE_URL ? parseBaseUrl(io.env.HEY_BASE_URL) : DEFAULT_BASE_URL;
1532
+ http = new HeyHttp(io.fetchImpl, baseUrl);
1533
+ const now = io.now();
1534
+ const result = await check(input, http);
1535
+ let cardPath = null;
1536
+ let cardFailure = null;
1537
+ const model = values["no-card"] ? null : cardModel(result, input, now);
1538
+ if (model) {
1539
+ try {
1540
+ const renderer2 = io.cardRenderer();
1541
+ const png = renderer2.render(buildCardSvg(model, renderer2.measure));
1542
+ const written = writeCard(values.out ?? defaultCardName(result, input), png, io.cwd);
1543
+ const rel = relative(io.cwd, written);
1544
+ cardPath = rel && !rel.startsWith("..") && !isAbsolute(rel) ? `./${rel}` : written;
1545
+ } catch (error) {
1546
+ cardFailure = error instanceof CheckError ? error : new CheckError(
1547
+ "card_not_written",
1548
+ `The card could not be drawn: ${error instanceof Error ? error.message : String(error)}`,
1549
+ EXIT.CARD_FAILED
1550
+ );
1551
+ }
1552
+ }
1553
+ const tweet = values.tweet ? tweetText(result, input, now) : null;
1554
+ const exitCode2 = result.outcome === "ambiguous" ? EXIT.AMBIGUOUS : result.outcome === "not_found" ? EXIT.NOT_FOUND : cardFailure ? EXIT.CARD_FAILED : EXIT.OK;
1555
+ if (values.json) {
1556
+ let doc = resultDocument(result, emptyDocument(now, input, http.requests), {
1557
+ cardPath,
1558
+ tweet
1559
+ });
1560
+ if (cardFailure) doc = { ...errorDocument(cardFailure, doc), ok: true };
1561
+ io.stdout(`${JSON.stringify(doc, null, 2)}
1562
+ `);
1563
+ return exitCode2;
1564
+ }
1565
+ if (cardFailure) io.stderr(`hey-check: ${cardFailure.message} (${cardFailure.code})
1566
+ `);
1567
+ if (values.tweet) {
1568
+ if (tweet) io.stdout(`${tweet}
1569
+ `);
1570
+ else io.stderr("hey-check: nothing to post until one project is picked.\n");
1571
+ if (cardPath) io.stderr(`hey-check: card written to ${cardPath}
1572
+ `);
1573
+ if (result.outcome === "ambiguous") {
1574
+ io.stderr(
1575
+ `${renderTerminal(result, { input, now, style: plainStyle, cardPath }).join("\n")}
1576
+ `
1577
+ );
1578
+ }
1579
+ return exitCode2;
1580
+ }
1581
+ if (values.quiet) {
1582
+ io.stdout(`${quietLine(result, now)}
1583
+ `);
1584
+ return exitCode2;
1585
+ }
1586
+ const colour = io.isTTY && !values["no-color"] && !(io.env.NO_COLOR !== void 0 && io.env.NO_COLOR !== "");
1587
+ const lines = renderTerminal(result, {
1588
+ input,
1589
+ now,
1590
+ style: colour ? ansiStyle : plainStyle,
1591
+ cardPath
1592
+ });
1593
+ io.stdout(`${lines.join("\n")}
1594
+ `);
1595
+ return exitCode2;
1596
+ } catch (error) {
1597
+ if (error instanceof CheckError) return fail(error);
1598
+ return fail(
1599
+ new CheckError(
1600
+ "internal_error",
1601
+ error instanceof Error ? error.message : String(error),
1602
+ EXIT.NETWORK,
1603
+ { retryable: false }
1604
+ )
1605
+ );
1606
+ }
1607
+ }
1608
+
1609
+ // src/cli.ts
1610
+ var renderer = null;
1611
+ var exitCode = await run(process.argv.slice(2), {
1612
+ fetchImpl: (input, init) => fetch(input, init),
1613
+ stdout: (text2) => process.stdout.write(text2),
1614
+ stderr: (text2) => process.stderr.write(text2),
1615
+ env: process.env,
1616
+ isTTY: process.stdout.isTTY === true,
1617
+ now: () => /* @__PURE__ */ new Date(),
1618
+ cwd: process.cwd(),
1619
+ cardRenderer: () => {
1620
+ if (!renderer) {
1621
+ const fonts = loadFonts();
1622
+ renderer = { measure: createMeasure(fonts), render: (svg) => renderPng(svg, fonts) };
1623
+ }
1624
+ return renderer;
1625
+ }
1626
+ });
1627
+ process.exitCode = exitCode;