prehello 0.0.0-stage → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (51) hide show
  1. package/CHANGELOG.md +40 -0
  2. package/LICENSE +21 -0
  3. package/README.md +515 -2
  4. package/THIRD_PARTY_NOTICES.md +33 -0
  5. package/assets/fonts/Inter-ExtraBold.subset.ttf +0 -0
  6. package/assets/fonts/Inter-Regular.subset.ttf +0 -0
  7. package/assets/fonts/Inter-SemiBold.subset.ttf +0 -0
  8. package/assets/fonts/OFL.txt +94 -0
  9. package/dist/adapters/claude-code.js +480 -0
  10. package/dist/adapters/codex.js +184 -0
  11. package/dist/adapters/common.js +157 -0
  12. package/dist/adapters/cursor.js +257 -0
  13. package/dist/adapters/skills.js +155 -0
  14. package/dist/adapters/types.js +2 -0
  15. package/dist/analyze/findings.js +82 -0
  16. package/dist/analyze/grade.js +55 -0
  17. package/dist/analyze/savings.js +111 -0
  18. package/dist/analyze/summary.js +57 -0
  19. package/dist/analyze/totals.js +21 -0
  20. package/dist/cli.js +454 -0
  21. package/dist/context.js +331 -0
  22. package/dist/fixtures.js +52 -0
  23. package/dist/fs-safe.js +222 -0
  24. package/dist/mcp/catalog.js +44 -0
  25. package/dist/mcp/configs.js +42 -0
  26. package/dist/model.js +11 -0
  27. package/dist/platform.js +115 -0
  28. package/dist/redact.js +39 -0
  29. package/dist/render/card.js +305 -0
  30. package/dist/render/fix.js +22 -0
  31. package/dist/render/json.js +97 -0
  32. package/dist/render/markdown.js +182 -0
  33. package/dist/render/png.js +162 -0
  34. package/dist/render/sarif.js +472 -0
  35. package/dist/render/terminal.js +329 -0
  36. package/dist/render/treemap.js +58 -0
  37. package/dist/safe-write.js +120 -0
  38. package/dist/scan.js +104 -0
  39. package/dist/spec.js +27 -0
  40. package/dist/text/comments.js +60 -0
  41. package/dist/text/frontmatter.js +90 -0
  42. package/dist/text/imports.js +81 -0
  43. package/dist/tokens/counter.js +41 -0
  44. package/dist/tokens/families.js +47 -0
  45. package/dist/util/fmt.js +102 -0
  46. package/dist/util/secrets.js +35 -0
  47. package/package.json +66 -4
  48. package/spec/claude-code.json +22 -0
  49. package/spec/codex.json +14 -0
  50. package/spec/cursor.json +15 -0
  51. package/spec/mcp-weights.json +62 -0
package/dist/cli.js ADDED
@@ -0,0 +1,454 @@
1
+ #!/usr/bin/env node
2
+ "use strict";
3
+ var __importDefault = (this && this.__importDefault) || function (mod) {
4
+ return (mod && mod.__esModule) ? mod : { "default": mod };
5
+ };
6
+ Object.defineProperty(exports, "__esModule", { value: true });
7
+ const node_fs_1 = __importDefault(require("node:fs"));
8
+ const node_os_1 = __importDefault(require("node:os"));
9
+ const node_path_1 = __importDefault(require("node:path"));
10
+ const scan_1 = require("./scan");
11
+ const model_1 = require("./model");
12
+ const terminal_1 = require("./render/terminal");
13
+ const fix_1 = require("./render/fix");
14
+ const json_1 = require("./render/json");
15
+ const card_1 = require("./render/card");
16
+ const png_1 = require("./render/png");
17
+ const markdown_1 = require("./render/markdown");
18
+ const sarif_1 = require("./render/sarif");
19
+ const platform_1 = require("./platform");
20
+ const fmt_1 = require("./util/fmt");
21
+ const fs_safe_1 = require("./fs-safe");
22
+ const safe_write_1 = require("./safe-write");
23
+ const spec_1 = require("./spec");
24
+ const HELP = `prehello ${scan_1.VERSION} — what your coding agent loads before you say hello.
25
+ Local-only, read-only scan. No network, no telemetry, no API key. Token counts are ESTIMATES.
26
+
27
+ Usage:
28
+ prehello [path] [options]
29
+ prehello explain grade print the Context Grade rubric
30
+
31
+ Options:
32
+ --agent <list> comma list of: claude-code, cursor, codex (forces them "in use")
33
+ --window <n|200k|1m> context window for percentages (default 200k)
34
+ --json print the report as JSON to stdout (versioned schema)
35
+ --json=<file> also write the JSON to <file>
36
+ --redact strict JSON: no paths or names (share-safe)
37
+ --card[=<file>] write a share card (no paths or file contents). The file extension picks the format:
38
+ .svg (default name prehello-card.svg) or .png (prehello-card.png)
39
+ write the file name with "=": --card=card.png (a space, "--card card.png", is not accepted)
40
+ --markdown[=<file>] a shareable Markdown report (title, headline, tables, what to cut, measured vs not measured) for a
41
+ GitHub issue / PR / README / Slack. Without a file it is printed to stdout INSTEAD of the terminal
42
+ report (pipe it: prehello --markdown | pbcopy); with a file (.md or .markdown) it is written there
43
+ and the terminal report is still printed. No paths, file contents or finding texts, ever.
44
+ --markdown-compact the short version for a PR comment: headline, one line per agent, the first things to cut
45
+ (implies --markdown; combine with --markdown=<file> to save it)
46
+ --sarif[=<file>] SARIF 2.1.0 for GitHub code scanning / IDEs (rules PH001-PH011: large always-on files, heavy plugins and MCP servers,
47
+ over --max-tokens, unmeasured items, duplicates, over-long / truncated / ignored files, possible secrets).
48
+ Without a file it is printed to stdout INSTEAD of the terminal report; with a file (.sarif or .sarif.json)
49
+ it is written there and the terminal report is still printed. File locations only for files inside the
50
+ scanned folder (relative to %SRCROOT%: run it from the repository root); never home or user-config paths.
51
+ --card-anonymize show plugin and MCP server names on the card and in the Markdown report as "plugin 1" / "MCP server 1"
52
+ (implied by --redact; --anonymize is the same flag). In SARIF, --redact also drops every file location.
53
+ --card-format svg|png format when --card has no file name (default svg); must match the extension if both are given
54
+ --card-theme dark|light --card-size og|square --card-agent <id>
55
+ --card-scale 1|2 PNG only: 1 = 1200x630 (square 1080x1080), 2 = double that, sharper (default 2)
56
+ --card-headline cut|total (card and Markdown report) lead with "skills/plugins cost ~X before hello; cutting the top N saves ~Y"
57
+ (default; falls back to the total when there is no meaningful saving) or always with
58
+ the always-on total
59
+ --force let --json=<file> / --card=<file> / --markdown=<file> replace an existing file that prehello did not write.
60
+ Symlinks are never written through, with or without --force.
61
+ --max-tokens <n|16k> CI gate: exit 2 if the always-on token count (a lower bound when anything is
62
+ unmeasured) of any agent in use is above n. Compares the central estimate.
63
+ --fix print a dry-run fix plan (changes nothing)
64
+ --tokenizer auto|heuristic --claude-profile current|legacy
65
+ --no-bundled-catalog ignore the measured MCP weights that ship with prehello
66
+ --mcp-catalog <file> extra pre-measured MCP weights (see spec/mcp-weights.json)
67
+ --paste-user-rules <file|-> count text you pasted for rules that live inside an app
68
+ (Cursor "User Rules"). Read-only; the text is counted, never printed.
69
+ --scan-budget-mb <n> stop reading after n MiB in total (default 64); the report is then marked PARTIAL
70
+ --scan-budget-ms <n> stop walking/reading after n ms (default 20000); a safety net, so it is the one
71
+ limit that can vary run to run on a slow disk
72
+ --include-fixtures also look inside test-fixture folders (fixtures, __fixtures__, fixture, testdata, test-data, __mocks__,
73
+ __snapshots__, and snapshots/golden under test/spec/e2e folders) for nested AGENTS.md, CLAUDE.md,
74
+ .cursor/rules and skills. By default they are skipped (no agent loads them) and the report says how many.
75
+ Scanning a folder that is itself inside a fixtures folder never skips anything.
76
+ --cwd <dir> simulate launching the agent from <dir>
77
+ --ascii plain ASCII output (no block characters or arrows); automatic in terminals that cannot draw them
78
+ (Linux console, classic Windows console). PREHELLO_ASCII=1 does the same.
79
+ --no-grade --no-color --quiet --verbose --about-specs --version --help
80
+
81
+ Output files (--json=<file>, --card=<file>, --markdown=<file>, --sarif=<file>): a symlink is never followed; an existing file is only replaced
82
+ if it is a previous prehello output (or with --force).
83
+
84
+ Scan budget: at most ~64 MiB read and ~20 s of walking per run, then the scan stops, warns, and every total is a
85
+ lower bound. Files over 4 MiB are skipped (Claude Code skips them at startup too).
86
+
87
+ PNG cards need the optional package @resvg/resvg-wasm. If it is not installed, prehello prints how to install it, writes no card,
88
+ exits 1, and everything else you asked for (report, --json, --markdown) is still produced. An SVG card never needs it.
89
+
90
+ Windows: the home folder is %USERPROFILE%; use --markdown=<file> or --json=<file> rather than shell redirection (Windows PowerShell 5
91
+ re-encodes redirected output as UTF-16). Works the same on Linux, macOS and Windows.
92
+
93
+ Exit codes: 0 ok · 1 error (incl. PNG engine missing) · 2 --max-tokens exceeded · 3 partial (some sources unreadable/skipped/budget hit; report still printed)
94
+ (2 wins over 3 when both apply)
95
+ `;
96
+ const VALUE_FLAGS = new Set(["agent", "window", "tokenizer", "claude-profile", "card-theme", "card-size", "card-agent", "cwd", "mcp-catalog", "ceiling", "managed-dir", "system-root", "paste-user-rules", "card-headline", "card-format", "card-scale", "png-engine", "max-tokens", "scan-budget-mb", "scan-budget-ms"]);
97
+ const BOOL_FLAGS = new Set(["json", "card", "redact", "fix", "no-grade", "no-color", "quiet", "verbose", "about-specs", "version", "help", "no-redact", "no-bundled-catalog", "force", "card-anonymize", "anonymize", "markdown", "markdown-compact", "sarif", "ascii", "include-fixtures"]);
98
+ function parseArgs(argv) {
99
+ const flags = new Map();
100
+ const positional = [];
101
+ for (let i = 0; i < argv.length; i++) {
102
+ const a = argv[i];
103
+ if (a === "--") {
104
+ positional.push(...argv.slice(i + 1));
105
+ break;
106
+ }
107
+ if (a === "-h") {
108
+ flags.set("help", true);
109
+ continue;
110
+ }
111
+ if (a === "-v" || a === "-V") {
112
+ flags.set("version", true);
113
+ continue;
114
+ }
115
+ if (a.length > 1 && a.startsWith("-") && !a.startsWith("--"))
116
+ return `unknown option ${a} (try --help)`;
117
+ if (!a.startsWith("--")) {
118
+ positional.push(a);
119
+ continue;
120
+ }
121
+ const eq = a.indexOf("=");
122
+ const name = eq === -1 ? a.slice(2) : a.slice(2, eq);
123
+ const inline = eq === -1 ? undefined : a.slice(eq + 1);
124
+ if (VALUE_FLAGS.has(name)) {
125
+ const v = inline ?? argv[++i];
126
+ if (v === undefined || (inline === undefined && v.startsWith("--")))
127
+ return `option --${name} needs a value`;
128
+ if (v === "" && name !== "paste-user-rules")
129
+ return `option --${name} needs a value`;
130
+ flags.set(name, v);
131
+ }
132
+ else if (BOOL_FLAGS.has(name)) {
133
+ if (inline !== undefined && name !== "json" && name !== "card" && name !== "markdown" && name !== "sarif")
134
+ return `option --${name} does not take a value`;
135
+ if (inline === "" && name === "json")
136
+ return "--json= needs a file name (for stdout, write --json with no =)";
137
+ flags.set(name, inline ?? true);
138
+ }
139
+ else
140
+ return `unknown option --${name} (try --help)`;
141
+ }
142
+ return { flags, positional };
143
+ }
144
+ function fail(msg) {
145
+ process.stderr.write(`prehello: ${msg}\n`);
146
+ process.exit(1);
147
+ }
148
+ async function main() {
149
+ const argv = process.argv.slice(2);
150
+ const parsed = parseArgs(argv);
151
+ if (typeof parsed === "string")
152
+ return fail(parsed);
153
+ const { flags, positional } = parsed;
154
+ if (flags.has("help"))
155
+ return void process.stdout.write(HELP);
156
+ if (flags.has("version"))
157
+ return void process.stdout.write(scan_1.VERSION + "\n");
158
+ if (flags.has("about-specs")) {
159
+ for (const id of model_1.ALL_AGENTS) {
160
+ const s = (0, spec_1.loadSpec)(id);
161
+ process.stdout.write(`${model_1.AGENT_NAMES[id]}: verifiedAt ${s.verifiedAt}\n ${s.verifiedNote}\n docs: ${Object.values(s.docs).join(" ")}\n`);
162
+ }
163
+ return;
164
+ }
165
+ if (positional[0] === "explain") {
166
+ if (positional[1] === "grade")
167
+ return void process.stdout.write((0, terminal_1.renderRubric)());
168
+ return fail("usage: prehello explain grade");
169
+ }
170
+ if (positional[0] === "ci" || positional[0] === "diff" || positional[0] === "calibrate")
171
+ return fail(`'${positional[0]}' is not part of 0.1 (planned for 0.2)`);
172
+ if (positional.length > 1)
173
+ return fail("at most one path argument is supported");
174
+ const env = process.env;
175
+ const target = flags.get("cwd") ?? positional[0] ?? process.cwd();
176
+ const cwdAbs = node_path_1.default.resolve(target);
177
+ if (!(0, fs_safe_1.isDir)(cwdAbs)) {
178
+ if (flags.get("card") === true && positional[0] && /\.(svg|png)$/i.test(positional[0]))
179
+ return fail(`not a directory: ${target}. To name the card file write --card=${positional[0]} (with "=", no space)`);
180
+ return fail(`not a directory: ${target}`);
181
+ }
182
+ const cwd = (0, fs_safe_1.realpathOrSelf)(cwdAbs);
183
+ // Windows: %USERPROFILE% (what the agents use), not HOME (Git Bash sets a POSIX-style "/c/Users/me" Node cannot open).
184
+ const home = (0, fs_safe_1.realpathOrSelf)((0, platform_1.resolveHomeDir)(env, process.platform, node_os_1.default.homedir));
185
+ let window = 200_000;
186
+ if (flags.has("window")) {
187
+ const w = (0, fmt_1.parseWindow)(String(flags.get("window")));
188
+ if (!w)
189
+ return fail("--window must look like 200k, 1m or 128000");
190
+ window = w;
191
+ }
192
+ const tokenizer = String(flags.get("tokenizer") ?? "auto");
193
+ if (tokenizer === "exact")
194
+ return fail("--tokenizer exact (network) is not part of 0.1; this build never makes network calls");
195
+ if (tokenizer !== "auto" && tokenizer !== "heuristic")
196
+ return fail("--tokenizer must be auto or heuristic");
197
+ const cp = String(flags.get("claude-profile") ?? "current");
198
+ if (cp !== "current" && cp !== "legacy")
199
+ return fail("--claude-profile must be current or legacy");
200
+ let agents;
201
+ if (flags.has("agent")) {
202
+ const list = String(flags.get("agent")).split(",").map((s) => s.trim().toLowerCase()).filter(Boolean);
203
+ if (!list.length)
204
+ return fail("--agent needs at least one of: " + model_1.ALL_AGENTS.join(", "));
205
+ const bad = list.filter((x) => !model_1.ALL_AGENTS.includes(x));
206
+ if (bad.length)
207
+ return fail(`unknown agent '${bad[0]}' (supported in 0.1: ${model_1.ALL_AGENTS.join(", ")})`);
208
+ agents = list;
209
+ }
210
+ const cardTheme = String(flags.get("card-theme") ?? "dark");
211
+ const cardSize = String(flags.get("card-size") ?? "og");
212
+ if (!["dark", "light"].includes(cardTheme))
213
+ return fail("--card-theme must be dark or light");
214
+ if (!["og", "square"].includes(cardSize))
215
+ return fail("--card-size must be og or square");
216
+ const cardHeadline = String(flags.get("card-headline") ?? "cut");
217
+ if (!["total", "cut"].includes(cardHeadline))
218
+ return fail("--card-headline must be total or cut");
219
+ const cardScaleRaw = String(flags.get("card-scale") ?? "2");
220
+ if (!["1", "2"].includes(cardScaleRaw))
221
+ return fail("--card-scale must be 1 or 2");
222
+ const cardScale = Number(cardScaleRaw);
223
+ const fmtFlag = flags.has("card-format") ? String(flags.get("card-format")).toLowerCase() : undefined;
224
+ if (fmtFlag !== undefined && !["svg", "png"].includes(fmtFlag))
225
+ return fail("--card-format must be svg or png");
226
+ if (flags.has("png-engine") && flags.get("png-engine") !== "none")
227
+ return fail("--png-engine only accepts 'none' (test hook)");
228
+ let cardFile = "";
229
+ let cardFormat = "svg";
230
+ if (flags.get("card") !== undefined) {
231
+ const cf = flags.get("card");
232
+ if (typeof cf === "string") {
233
+ const ext = /\.(svg|png)$/i.exec(cf)?.[1].toLowerCase();
234
+ if (!ext)
235
+ return fail("--card file name must end in .svg or .png");
236
+ if (fmtFlag && fmtFlag !== ext)
237
+ return fail(`--card-format ${fmtFlag} does not match the file name ${cf}`);
238
+ cardFile = cf;
239
+ cardFormat = ext;
240
+ }
241
+ else {
242
+ cardFormat = fmtFlag ?? "svg";
243
+ cardFile = `prehello-card.${cardFormat}`;
244
+ }
245
+ }
246
+ else if (fmtFlag)
247
+ return fail("--card-format needs --card");
248
+ const mdFlag = flags.get("markdown");
249
+ const mdCompact = flags.has("markdown-compact");
250
+ let mdFile = "";
251
+ if (typeof mdFlag === "string") {
252
+ if (!/\.(md|markdown)$/i.test(mdFlag))
253
+ return fail("--markdown file name must end in .md or .markdown (for stdout, write --markdown with no file name)");
254
+ mdFile = mdFlag;
255
+ }
256
+ const mdStdout = mdFile === "" && (mdFlag !== undefined || mdCompact);
257
+ const sarifFlag = flags.get("sarif");
258
+ let sarifFile = "";
259
+ if (typeof sarifFlag === "string") {
260
+ if (!/\.sarif(\.json)?$/i.test(sarifFlag))
261
+ return fail("--sarif file name must end in .sarif or .sarif.json (for stdout, write --sarif with no file name)");
262
+ sarifFile = sarifFlag;
263
+ }
264
+ const sarifStdout = sarifFlag === true;
265
+ if (mdStdout && flags.get("json") === true)
266
+ return fail("--json and --markdown both want standard output; give one of them a file: --markdown=report.md or --json=report.json");
267
+ if (mdStdout && flags.has("fix"))
268
+ return fail("--fix and --markdown both want standard output; give the Markdown report a file: --markdown=report.md");
269
+ if (sarifStdout && flags.get("json") === true)
270
+ return fail("--json and --sarif both want standard output; give one of them a file: --sarif=report.sarif or --json=report.json");
271
+ if (sarifStdout && mdStdout)
272
+ return fail("--markdown and --sarif both want standard output; give one of them a file: --sarif=report.sarif or --markdown=report.md");
273
+ if (sarifStdout && flags.has("fix"))
274
+ return fail("--fix and --sarif both want standard output; give the SARIF report a file: --sarif=report.sarif");
275
+ let maxTokens;
276
+ if (flags.has("max-tokens")) {
277
+ const m = /^(\d+(?:\.\d+)?)\s*([km]?)$/i.exec(String(flags.get("max-tokens")).trim());
278
+ if (!m)
279
+ return fail("--max-tokens must look like 16000 or 16k");
280
+ maxTokens = Math.round(parseFloat(m[1]) * (m[2].toLowerCase() === "k" ? 1000 : m[2].toLowerCase() === "m" ? 1_000_000 : 1));
281
+ }
282
+ const force = flags.has("force");
283
+ const posNum = (name) => {
284
+ if (!flags.has(name))
285
+ return undefined;
286
+ const raw = String(flags.get(name));
287
+ const n = Number(raw);
288
+ return /^\d+(\.\d+)?$/.test(raw) && n > 0 ? n : `--${name} must be a positive number`;
289
+ };
290
+ const budgetMb = posNum("scan-budget-mb");
291
+ const budgetMs = posNum("scan-budget-ms");
292
+ if (typeof budgetMb === "string")
293
+ return fail(budgetMb);
294
+ if (typeof budgetMs === "string")
295
+ return fail(budgetMs);
296
+ let pastedUserRules;
297
+ if (flags.has("paste-user-rules")) {
298
+ const src = String(flags.get("paste-user-rules"));
299
+ try {
300
+ if (src === "-") {
301
+ if (process.stdin.isTTY)
302
+ return fail("--paste-user-rules - reads standard input; pipe the text in (e.g. pbpaste | prehello --paste-user-rules -) or pass a file");
303
+ pastedUserRules = (0, platform_1.decodeUserText)(node_fs_1.default.readFileSync(0)).replace(/\r\n/g, "\n");
304
+ }
305
+ else {
306
+ const st = node_fs_1.default.statSync(src);
307
+ if (!st.isFile())
308
+ return fail(`--paste-user-rules: not a file: ${src}`);
309
+ if (st.size > 1024 * 1024)
310
+ return fail("--paste-user-rules: file is larger than 1 MB");
311
+ pastedUserRules = (0, platform_1.decodeUserText)(node_fs_1.default.readFileSync(src)).replace(/\r\n/g, "\n");
312
+ }
313
+ }
314
+ catch (e) {
315
+ return fail(`--paste-user-rules: could not read ${src === "-" ? "standard input" : src} (${e.code ?? "error"})`);
316
+ }
317
+ if (pastedUserRules.length > 1024 * 1024)
318
+ return fail("--paste-user-rules: text is larger than 1 MB");
319
+ if (!pastedUserRules.trim())
320
+ return fail("--paste-user-rules: the text is empty");
321
+ }
322
+ const report = (0, scan_1.scan)({
323
+ cwd,
324
+ home,
325
+ env,
326
+ platform: process.platform,
327
+ ceiling: flags.has("ceiling") ? (0, fs_safe_1.realpathOrSelf)(String(flags.get("ceiling"))) : undefined,
328
+ managedDir: flags.has("managed-dir") ? String(flags.get("managed-dir")) : undefined,
329
+ noBundledCatalog: flags.has("no-bundled-catalog"),
330
+ systemRoot: flags.has("system-root") ? node_path_1.default.resolve(String(flags.get("system-root"))) : undefined,
331
+ pastedUserRules,
332
+ window,
333
+ tokenizer,
334
+ claudeProfile: cp,
335
+ includeFixtures: flags.has("include-fixtures"),
336
+ agents,
337
+ catalogPath: flags.has("mcp-catalog") ? node_path_1.default.resolve(String(flags.get("mcp-catalog"))) : undefined,
338
+ now: new Date(),
339
+ maxScanBytes: budgetMb === undefined ? undefined : Math.round(budgetMb * 1048576),
340
+ maxScanMs: budgetMs,
341
+ noGrade: flags.has("no-grade"),
342
+ });
343
+ const protectedPaths = new Set(report.agents.flatMap((a) => a.items.map((i) => (0, fs_safe_1.realpathOrSelf)(i.path))));
344
+ const write = (file, content, what, kind) => {
345
+ const abs = node_path_1.default.resolve(file);
346
+ if (protectedPaths.has((0, fs_safe_1.realpathOrSelf)(abs)))
347
+ return fail(`refusing to overwrite a scanned context file with ${what}: ${file}`);
348
+ try {
349
+ (0, safe_write_1.safeWrite)(abs, content, { kind, force });
350
+ }
351
+ catch (e) {
352
+ if (e instanceof safe_write_1.SafeWriteError)
353
+ return fail(`${what} not written to ${file}: ${e.message}`);
354
+ return fail(`could not write ${what} to ${file}: ${(0, safe_write_1.describeWriteError)(e.code)}`);
355
+ }
356
+ };
357
+ const strict = flags.has("redact");
358
+ const jsonFlag = flags.get("json");
359
+ const color = !flags.has("no-color") && !env.NO_COLOR && env.TERM !== "dumb" && !!process.stdout.isTTY;
360
+ const asciiOut = flags.has("ascii") || env.PREHELLO_ASCII === "1" || (!!process.stdout.isTTY && !(0, platform_1.unicodeSupported)(env, process.platform));
361
+ const term = (t) => (asciiOut ? (0, platform_1.toAscii)(t) : t);
362
+ const width = Math.min(process.stdout.columns || 80, 100);
363
+ const jsonText = jsonFlag !== undefined ? JSON.stringify((0, json_1.toJson)(report, { cwd, home }, strict), null, 2) + "\n" : "";
364
+ if (typeof jsonFlag === "string")
365
+ write(jsonFlag, jsonText, "JSON", "json");
366
+ let cardMsg = "";
367
+ let cardFailed = false;
368
+ const anonymizeNames = flags.has("card-anonymize") || flags.has("anonymize") || strict;
369
+ let mdText = "";
370
+ let mdMsg = "";
371
+ let sarifOut = "";
372
+ if (mdFlag !== undefined || mdCompact) {
373
+ mdText = (0, markdown_1.renderMarkdown)(report, { anonymize: anonymizeNames, compact: mdCompact, headline: cardHeadline });
374
+ if (mdFile) {
375
+ write(mdFile, `${safe_write_1.MD_MARKER_PREFIX}${scan_1.VERSION} -->\n${mdText}`, "Markdown report", "md");
376
+ mdMsg = `Markdown report written to ${mdFile} (no paths, file contents or finding texts; ${anonymizeNames ? "plugin and MCP server names anonymised" : "plugin and MCP server names are shown, add --card-anonymize to hide them"}).\n`;
377
+ }
378
+ }
379
+ if (cardFile) {
380
+ const agent = (0, card_1.pickCardAgent)(report, flags.has("card-agent") ? String(flags.get("card-agent")) : undefined);
381
+ if (!agent)
382
+ cardMsg = "No card written: nothing measurable (or unmeasured) loads before hello for the selected agent(s).\n";
383
+ else {
384
+ const base = { theme: cardTheme, size: cardSize, headline: cardHeadline, anonymize: anonymizeNames };
385
+ if (cardFormat === "svg")
386
+ write(cardFile, (0, card_1.renderCard)(report, agent, base), "card", "svg");
387
+ else {
388
+ try {
389
+ const png = await (0, png_1.svgToPng)((0, card_1.renderCard)(report, agent, { ...base, fontFamily: png_1.PNG_FONT_FAMILY, charEm: png_1.PNG_CHAR_EM }), {
390
+ scale: cardScale,
391
+ version: scan_1.VERSION,
392
+ resolver: flags.get("png-engine") === "none" ? () => { throw new Error("disabled"); } : undefined,
393
+ });
394
+ write(cardFile, png, "card", "png");
395
+ }
396
+ catch (e) {
397
+ if (!(e instanceof png_1.PngUnavailableError))
398
+ throw e;
399
+ cardFailed = true;
400
+ cardMsg = `prehello: ${e.message}\n`;
401
+ }
402
+ }
403
+ if (!cardFailed)
404
+ cardMsg = `Card written to ${cardFile} (${agent.name}; no paths or file contents; ${anonymizeNames ? "plugin and MCP server names anonymised" : "plugin and MCP server names are shown, add --card-anonymize to hide them"}).\n`;
405
+ }
406
+ }
407
+ let sarifMsg = "";
408
+ if (sarifFlag !== undefined) {
409
+ const sarifText = (0, sarif_1.renderSarif)(report, { anonymize: anonymizeNames, strict, maxTokens, cwd, home });
410
+ if (sarifFile) {
411
+ write(sarifFile, sarifText, "SARIF report", "sarif");
412
+ sarifMsg = `SARIF report written to ${sarifFile} (no file contents or finding texts; file locations only for files inside the scanned folder${strict ? ", none with --redact" : ""}).\n`;
413
+ }
414
+ else
415
+ sarifOut = sarifText;
416
+ }
417
+ const docOnStdout = jsonFlag === true || mdStdout || sarifStdout;
418
+ if (jsonFlag === true)
419
+ process.stdout.write(jsonText);
420
+ else if (sarifStdout)
421
+ process.stdout.write(sarifOut);
422
+ else if (mdStdout)
423
+ process.stdout.write(mdText);
424
+ else if (flags.has("fix"))
425
+ process.stdout.write(term((0, fix_1.renderFix)(report)));
426
+ else
427
+ process.stdout.write(term((0, terminal_1.renderTerminal)(report, { color, width, verbose: flags.has("verbose"), quiet: flags.has("quiet"), home, cwd })));
428
+ if (cardMsg)
429
+ (docOnStdout || cardFailed ? process.stderr : process.stdout).write(term(cardMsg));
430
+ if (mdMsg)
431
+ (docOnStdout ? process.stderr : process.stdout).write(term(mdMsg));
432
+ if (sarifMsg)
433
+ (docOnStdout ? process.stderr : process.stdout).write(term(sarifMsg));
434
+ process.exitCode = cardFailed ? 1 : report.warnings.length ? 3 : 0;
435
+ if (maxTokens !== undefined) {
436
+ const over = report.agents.filter((a) => !a.presentInRepoOnly && a.totals.always.est > maxTokens);
437
+ for (const a of over) {
438
+ const lb = a.lowerBound ? "at least " : "";
439
+ process.stderr.write(`prehello: ${a.name} always-on context is ${lb}~${(0, fmt_1.fmtTok)(a.totals.always.est)} tokens (estimate), above --max-tokens ${maxTokens}\n`);
440
+ }
441
+ if (over.length)
442
+ process.exitCode = 2;
443
+ }
444
+ }
445
+ // `prehello --markdown | head` closes the pipe early: that is not an error.
446
+ process.stdout.on("error", (e) => {
447
+ if (e.code === "EPIPE")
448
+ process.exit(process.exitCode ?? 0);
449
+ throw e;
450
+ });
451
+ main().catch((e) => {
452
+ process.stderr.write(`prehello: internal error: ${e.message}\n`);
453
+ process.exitCode = 1;
454
+ });