prehello 0.0.0-stage → 1.0.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 (61) hide show
  1. package/CHANGELOG.md +78 -0
  2. package/LICENSE +21 -0
  3. package/README.md +171 -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 +275 -0
  13. package/dist/adapters/gemini-cli.js +214 -0
  14. package/dist/adapters/github-copilot.js +249 -0
  15. package/dist/adapters/index.js +16 -0
  16. package/dist/adapters/skills.js +165 -0
  17. package/dist/adapters/types.js +2 -0
  18. package/dist/agents.js +54 -0
  19. package/dist/analyze/findings.js +82 -0
  20. package/dist/analyze/grade.js +55 -0
  21. package/dist/analyze/savings.js +111 -0
  22. package/dist/analyze/summary.js +57 -0
  23. package/dist/analyze/totals.js +21 -0
  24. package/dist/calibrate.js +265 -0
  25. package/dist/cli.js +591 -0
  26. package/dist/context.js +329 -0
  27. package/dist/diff.js +182 -0
  28. package/dist/fixtures.js +52 -0
  29. package/dist/fs-safe.js +222 -0
  30. package/dist/mcp/catalog.js +44 -0
  31. package/dist/mcp/configs.js +42 -0
  32. package/dist/model.js +8 -0
  33. package/dist/platform.js +115 -0
  34. package/dist/redact.js +39 -0
  35. package/dist/render/card.js +306 -0
  36. package/dist/render/fix.js +22 -0
  37. package/dist/render/json.js +97 -0
  38. package/dist/render/markdown.js +182 -0
  39. package/dist/render/png.js +162 -0
  40. package/dist/render/sarif.js +472 -0
  41. package/dist/render/terminal.js +318 -0
  42. package/dist/render/treemap.js +58 -0
  43. package/dist/safe-write.js +122 -0
  44. package/dist/scan.js +98 -0
  45. package/dist/spec.js +27 -0
  46. package/dist/text/comments.js +60 -0
  47. package/dist/text/frontmatter.js +90 -0
  48. package/dist/text/imports.js +86 -0
  49. package/dist/tokens/counter.js +41 -0
  50. package/dist/tokens/families.js +79 -0
  51. package/dist/util/fmt.js +102 -0
  52. package/dist/util/secrets.js +35 -0
  53. package/dist/version.js +5 -0
  54. package/package.json +70 -4
  55. package/spec/calibration.json +33 -0
  56. package/spec/claude-code.json +22 -0
  57. package/spec/codex.json +14 -0
  58. package/spec/cursor.json +15 -0
  59. package/spec/gemini-cli.json +17 -0
  60. package/spec/github-copilot.json +14 -0
  61. package/spec/mcp-weights.json +62 -0
@@ -0,0 +1,60 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.splitFences = splitFences;
4
+ exports.stripHtmlComments = stripHtmlComments;
5
+ /** Split markdown into alternating [prose, fence, prose, fence...] segments (``` or ~~~ fences). */
6
+ function splitFences(text) {
7
+ const lines = text.split("\n");
8
+ const out = [];
9
+ let cur = [];
10
+ let inFence = false;
11
+ let marker = "";
12
+ const flush = (code) => {
13
+ if (cur.length)
14
+ out.push({ code, text: cur.join("\n") });
15
+ cur = [];
16
+ };
17
+ for (const line of lines) {
18
+ const fm = /^\s{0,3}(`{3,}|~{3,})/.exec(line);
19
+ if (!inFence && fm) {
20
+ flush(false);
21
+ inFence = true;
22
+ marker = fm[1][0];
23
+ cur.push(line);
24
+ }
25
+ else if (inFence && fm && fm[1][0] === marker && /^\s{0,3}(`{3,}|~{3,})\s*$/.test(line)) {
26
+ cur.push(line);
27
+ flush(true);
28
+ inFence = false;
29
+ }
30
+ else {
31
+ cur.push(line);
32
+ }
33
+ }
34
+ flush(inFence);
35
+ return out;
36
+ }
37
+ /** Claude Code strips block-level HTML comments before injection; comments inside code fences are kept. */
38
+ function stripHtmlComments(text) {
39
+ return splitFences(text)
40
+ .map((seg) => (seg.code ? seg.text : stripComments(seg.text)))
41
+ .join("\n");
42
+ }
43
+ /**
44
+ * Linear-time equivalent of text.replace(/<!--[\s\S]*?-->/g, ""). The regex form is quadratic on input such as "<!--" repeated
45
+ * (every start rescans to the end for a "-->" that never comes), which a hostile 4 MiB CLAUDE.md turns into a hang.
46
+ */
47
+ function stripComments(text) {
48
+ let out = "";
49
+ let i = 0;
50
+ for (;;) {
51
+ const s = text.indexOf("<!--", i);
52
+ if (s < 0)
53
+ return out + text.slice(i);
54
+ const e = text.indexOf("-->", s + 4);
55
+ if (e < 0)
56
+ return out + text.slice(i); // an unclosed comment is left as it is, like the regex
57
+ out += text.slice(i, s);
58
+ i = e + 3;
59
+ }
60
+ }
@@ -0,0 +1,90 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.parseFrontmatter = parseFrontmatter;
4
+ exports.asList = asList;
5
+ exports.asString = asString;
6
+ function unquote(s) {
7
+ const t = s.trim();
8
+ if (t.length >= 2 && ((t[0] === '"' && t.endsWith('"')) || (t[0] === "'" && t.endsWith("'"))))
9
+ return t.slice(1, -1);
10
+ return t;
11
+ }
12
+ /** Tiny YAML-subset frontmatter parser: scalars, booleans, inline lists, "- item" lists, | and > block scalars. */
13
+ function parseFrontmatter(text) {
14
+ const t = text.replace(/\r\n?/g, "\n");
15
+ if (!t.startsWith("---\n") && t !== "---")
16
+ return { data: Object.create(null), body: t, hasFrontmatter: false };
17
+ const rest = t.slice(4);
18
+ const m = /(^|\n)---[ \t]*(\n|$)/.exec(rest);
19
+ if (!m)
20
+ return { data: Object.create(null), body: t, hasFrontmatter: false };
21
+ const header = rest.slice(0, m.index);
22
+ const body = rest.slice(m.index + m[0].length);
23
+ // no prototype: a key such as `__proto__: [a, b]` in an untrusted file must stay plain data
24
+ const data = Object.create(null);
25
+ const lines = header.split("\n");
26
+ for (let i = 0; i < lines.length; i++) {
27
+ const line = lines[i];
28
+ const km = /^([A-Za-z_][\w-]*)\s*:\s*(.*)$/.exec(line);
29
+ if (!km)
30
+ continue;
31
+ const key = km[1];
32
+ const val = km[2].trim();
33
+ if (val === "|" || val === ">" || val === "|-" || val === ">-") {
34
+ const parts = [];
35
+ while (i + 1 < lines.length && (/^\s+/.test(lines[i + 1]) || lines[i + 1] === ""))
36
+ parts.push(lines[++i].trim());
37
+ data[key] = parts.join(val.startsWith(">") ? " " : "\n").trim();
38
+ }
39
+ else if (val === "") {
40
+ const items = [];
41
+ while (i + 1 < lines.length && /^\s*-\s+/.test(lines[i + 1]))
42
+ items.push(unquote(lines[++i].replace(/^\s*-\s+/, "")));
43
+ data[key] = items.length ? items : "";
44
+ }
45
+ else if (val.startsWith("[") && val.endsWith("]")) {
46
+ data[key] = val
47
+ .slice(1, -1)
48
+ .split(",")
49
+ .map((x) => unquote(x))
50
+ .filter(Boolean);
51
+ }
52
+ else if (val === "true" || val === "false") {
53
+ data[key] = val === "true";
54
+ }
55
+ else {
56
+ // plain or quoted scalar that continues on indented lines (YAML folds the line breaks into spaces)
57
+ let full = val;
58
+ const q = full[0] === '"' || full[0] === "'" ? full[0] : "";
59
+ const closed = () => q === "" || full.indexOf(q, 1) !== -1;
60
+ while (i + 1 < lines.length && (!closed() || (/^[ \t]+\S/.test(lines[i + 1]) && !/^\s*-\s/.test(lines[i + 1])))) {
61
+ if (q && closed())
62
+ break;
63
+ i++;
64
+ full += " " + lines[i].trim();
65
+ if (i > 4000)
66
+ break;
67
+ }
68
+ data[key] = unquote(full);
69
+ }
70
+ }
71
+ return { data, body, hasFrontmatter: true };
72
+ }
73
+ /** Normalise a frontmatter value that may be a list or a comma-separated string. */
74
+ function asList(v) {
75
+ if (v === undefined || typeof v === "boolean")
76
+ return [];
77
+ if (Array.isArray(v))
78
+ return v.filter(Boolean);
79
+ return v
80
+ .split(",")
81
+ .map((s) => s.trim())
82
+ .filter(Boolean);
83
+ }
84
+ function asString(v) {
85
+ if (v === undefined)
86
+ return "";
87
+ if (typeof v === "boolean")
88
+ return String(v);
89
+ return Array.isArray(v) ? v.join(", ") : v;
90
+ }
@@ -0,0 +1,86 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.isInside = void 0;
7
+ exports.expandImports = expandImports;
8
+ const node_path_1 = __importDefault(require("node:path"));
9
+ const fs_safe_1 = require("../fs-safe");
10
+ Object.defineProperty(exports, "isInside", { enumerable: true, get: function () { return fs_safe_1.isInside; } });
11
+ const comments_1 = require("./comments");
12
+ const MAX_DEPTH = 4;
13
+ /** Upper bound on @path candidates examined per file tree; a hostile file with 150,000 of them would otherwise cost seconds of stat calls. */
14
+ const MAX_CANDIDATES = 2000;
15
+ function candidates(prose) {
16
+ // Drop inline code spans first, then find @path tokens.
17
+ const noCode = prose.replace(/`[^`\n]*`/g, " ");
18
+ const out = [];
19
+ const re = /(^|[\s(])@([^\s)]+)/g;
20
+ let m;
21
+ while ((m = re.exec(noCode))) {
22
+ let spec = m[2].replace(/[.,;:!?]+$/, "");
23
+ if (!spec)
24
+ continue;
25
+ const looksLikePath = spec.includes("/") || /\.[A-Za-z0-9]{1,8}$/.test(spec);
26
+ if (!looksLikePath || /^[\w.-]+@[\w.-]+$/.test(spec))
27
+ continue; // @mentions / emails
28
+ out.push(spec);
29
+ }
30
+ return out;
31
+ }
32
+ /**
33
+ * Expand Claude-style `@path` imports (depth ≤ 4, cycle-safe, code spans/fences skipped).
34
+ * Imports do NOT reduce cost, so imported text is added to the total.
35
+ * `allowed(realPath)` restricts which files may be read (project tree / home).
36
+ */
37
+ function expandImports(fileText, filePath, home, allowed, opts) {
38
+ const seen = new Set([(0, fs_safe_1.realpathOrSelf)(filePath)]);
39
+ const skipped = [];
40
+ let imported = 0;
41
+ let tried = 0;
42
+ const parts = [];
43
+ const walk = (text, from, depth) => {
44
+ const prose = (0, comments_1.splitFences)(text)
45
+ .filter((s) => !s.code)
46
+ .map((s) => s.text)
47
+ .join("\n");
48
+ for (const spec of candidates(prose)) {
49
+ if (++tried > MAX_CANDIDATES) {
50
+ if (tried === MAX_CANDIDATES + 1)
51
+ skipped.push({ spec: "(more)", reason: `more than ${MAX_CANDIDATES} @imports; the rest were not followed` });
52
+ continue;
53
+ }
54
+ if (opts.relativeOnly && (spec.startsWith("~") || node_path_1.default.isAbsolute(spec) || /^[A-Za-z]:[\\/]/.test(spec))) {
55
+ skipped.push({ spec, reason: "absolute or ~/ path (not loaded by this agent)" });
56
+ continue;
57
+ }
58
+ const target = spec.startsWith("~/") ? node_path_1.default.join(home, spec.slice(2)) : node_path_1.default.resolve(node_path_1.default.dirname(from), spec);
59
+ const real = (0, fs_safe_1.realpathOrSelf)(target);
60
+ if (seen.has(real))
61
+ continue;
62
+ const maxDepth = opts.maxDepth ?? MAX_DEPTH;
63
+ if (depth >= maxDepth) {
64
+ skipped.push({ spec, reason: `depth>${maxDepth}` });
65
+ continue;
66
+ }
67
+ if (!allowed(real)) {
68
+ skipped.push({ spec, reason: "outside allowed roots" });
69
+ continue;
70
+ }
71
+ const r = (opts.read ?? ((t) => (0, fs_safe_1.readTextSafe)(t)))(target);
72
+ if (!r.ok) {
73
+ if (r.reason !== "missing")
74
+ skipped.push({ spec, reason: r.reason });
75
+ continue;
76
+ }
77
+ seen.add(real);
78
+ imported++;
79
+ const t = opts.stripComments ? (0, comments_1.stripHtmlComments)(r.text) : r.text;
80
+ parts.push(t);
81
+ walk(t, target, depth + 1);
82
+ }
83
+ };
84
+ walk(fileText, filePath, 0);
85
+ return { text: [fileText, ...parts].join("\n"), imported, skipped };
86
+ }
@@ -0,0 +1,41 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.makeCounter = makeCounter;
4
+ function heuristicCount(text) {
5
+ if (!text)
6
+ return 0;
7
+ const jsonish = (text.match(/[{}\[\]":,]/g) || []).length / text.length > 0.08;
8
+ return Math.ceil(text.length / (jsonish ? 3.0 : 3.5));
9
+ }
10
+ function makeCounter(requested) {
11
+ if (requested === "auto") {
12
+ try {
13
+ // eslint-disable-next-line @typescript-eslint/no-var-requires
14
+ const enc = require("gpt-tokenizer/encoding/o200k_base");
15
+ // Verify it works (and that special-token text such as "<|endoftext|>" cannot throw).
16
+ enc.encode("hello", { disallowedSpecial: new Set() });
17
+ return {
18
+ method: "bpe-o200k",
19
+ note: "o200k BPE (gpt-tokenizer, bundled) with per-family scaling",
20
+ count(text) {
21
+ if (!text)
22
+ return 0;
23
+ try {
24
+ return enc.encode(text, { disallowedSpecial: new Set() }).length;
25
+ }
26
+ catch {
27
+ return heuristicCount(text);
28
+ }
29
+ },
30
+ };
31
+ }
32
+ catch {
33
+ /* fall through to heuristic */
34
+ }
35
+ }
36
+ return {
37
+ method: "heuristic",
38
+ note: requested === "auto" ? "BPE tokenizer unavailable; fell back to chars/3.5 heuristic" : "chars/3.5 (prose) or chars/3.0 (JSON/code) heuristic",
39
+ count: heuristicCount,
40
+ };
41
+ }
@@ -0,0 +1,79 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.claudeCalibration = claudeCalibration;
7
+ exports.profileFor = profileFor;
8
+ const node_fs_1 = __importDefault(require("node:fs"));
9
+ const node_path_1 = __importDefault(require("node:path"));
10
+ const agents_1 = require("../agents");
11
+ let calCache;
12
+ /** Claude ratios from spec/calibration.json (published count_tokens measurements; see docs/ACCURACY.md). */
13
+ function claudeCalibration() {
14
+ if (!calCache) {
15
+ const j = JSON.parse(node_fs_1.default.readFileSync(node_path_1.default.join(__dirname, "..", "..", "spec", "calibration.json"), "utf8"));
16
+ calCache = j.claude;
17
+ }
18
+ return calCache;
19
+ }
20
+ function sym(raw, factor, bandPct) {
21
+ const est = Math.round(raw * factor);
22
+ return { est, low: Math.round(est * (1 - bandPct)), high: Math.round(est * (1 + bandPct)) };
23
+ }
24
+ const pct = (x) => `±${Math.round(x * 100)}%`;
25
+ /**
26
+ * Claude factors come from spec/calibration.json (published ratios, still unvalidated: n is small). Codex is o200k itself.
27
+ * "mixed" agents (Cursor, Copilot) let the user pick the model, so they span GPT-like to Claude-like. Gemini has no measured ratio.
28
+ */
29
+ function profileFor(agent, method, claude) {
30
+ if (method === "heuristic") {
31
+ return {
32
+ id: "heuristic",
33
+ label: "heuristic chars/3.5 (no tokenizer)",
34
+ band: "±35%",
35
+ apply: (raw) => sym(raw, 1, 0.35),
36
+ unvalidated: true,
37
+ };
38
+ }
39
+ const family = (0, agents_1.agentMeta)(agent).tokenFamily;
40
+ if (family === "openai") {
41
+ return { id: "openai-o200k", label: "o200k BPE (OpenAI-family)", band: "±3%", apply: (raw) => sym(raw, 1.0, 0.03), unvalidated: false };
42
+ }
43
+ const cal = claudeCalibration();
44
+ const scale = claude === "current" ? 1 : cal.legacyRatio.value;
45
+ const f = (k) => Math.round(cal.byKind[k].factor * scale * 1000) / 1000;
46
+ const b = (k) => cal.byKind[k].band;
47
+ const claudeProfile = {
48
+ id: `claude-${claude}`,
49
+ label: `o200k × ${f("markdown")} (text) / × ${f("json")} (JSON), Claude-${claude} ratios from published count_tokens measurements`,
50
+ band: pct(b("markdown")),
51
+ apply: (raw, kind = "markdown") => sym(raw, f(kind), b(kind)),
52
+ unvalidated: cal.unvalidated,
53
+ };
54
+ if (family === "claude")
55
+ return claudeProfile;
56
+ if (family === "gemini") {
57
+ // No published Gemini-vs-o200k ratio was found (docs/ACCURACY.md), so prehello does not invent one: the estimate is the
58
+ // raw o200k count and the band is the same GPT-like-to-Claude-like spread used for "model unknown". NOT a Gemini measurement.
59
+ return {
60
+ id: "gemini-unvalidated",
61
+ label: `o200k × 1.0; no measured Gemini ratio, band borrowed from the GPT-like (×1.0) to Claude-like (×${f("markdown")}) spread`,
62
+ band: `wide, unvalidated: −3% to +${Math.round((f("markdown") * (1 + b("markdown")) - 1) * 100)}%`,
63
+ apply: (raw, kind = "markdown") => ({ est: Math.round(raw), low: Math.round(raw * 0.97), high: Math.round(raw * f(kind) * (1 + b(kind))) }),
64
+ unvalidated: true,
65
+ };
66
+ }
67
+ // mixed (Cursor, GitHub Copilot: the model is user-selectable and not discoverable from disk): span GPT-like (×1.0, −3%) to Claude-like (×f, +band); est = midpoint.
68
+ return {
69
+ id: agent === "cursor" ? "cursor-mixed" : "mixed",
70
+ label: `o200k; model unknown, so midpoint of GPT-like (×1.0) and Claude-like (×${f("markdown")}) ratios`,
71
+ band: `wide: GPT-like −3% to Claude-like +${Math.round(b("markdown") * 100)}%`,
72
+ apply: (raw, kind = "markdown") => {
73
+ const lo = Math.round(raw * 0.97);
74
+ const hi = Math.round(raw * f(kind) * (1 + b(kind)));
75
+ return { est: Math.round((raw * (1 + f(kind))) / 2), low: lo, high: hi };
76
+ },
77
+ unvalidated: true,
78
+ };
79
+ }
@@ -0,0 +1,102 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.sanitize = sanitize;
4
+ exports.fmtInt = fmtInt;
5
+ exports.fmtTok = fmtTok;
6
+ exports.fmtPct = fmtPct;
7
+ exports.parseWindow = parseWindow;
8
+ exports.fmtWindow = fmtWindow;
9
+ exports.xmlEscape = xmlEscape;
10
+ exports.wrap = wrap;
11
+ exports.shortenLeft = shortenLeft;
12
+ exports.shortenLabel = shortenLabel;
13
+ exports.cmp = cmp;
14
+ const secrets_1 = require("./secrets");
15
+ /** Remove control characters / ANSI escapes so file names from untrusted repos cannot hijack the terminal or a downstream agent. */
16
+ function sanitize(s, max = 120) {
17
+ // eslint-disable-next-line no-control-regex
18
+ let out = s.replace(/\u001b\[[0-9;?]*[ -\/]*[@-~]/g, "").replace(/[\u0000-\u001f\u007f-\u009f\u202a-\u202e\u2066-\u2069]/g, "?");
19
+ // Scrub BEFORE truncating: a cut-off key (sk-ant-…) would no longer match and would leak its prefix.
20
+ out = (0, secrets_1.scrubSecrets)(out);
21
+ if (out.length > max)
22
+ out = out.slice(0, max - 1) + "…";
23
+ return out;
24
+ }
25
+ function fmtInt(n) {
26
+ return Math.round(n).toLocaleString("en-US");
27
+ }
28
+ /** 16440 -> "16.4k", 950 -> "950". */
29
+ function fmtTok(n) {
30
+ const r = Math.round(n);
31
+ if (r < 1000)
32
+ return String(r);
33
+ if (r < 100000)
34
+ return (r / 1000).toFixed(1).replace(/\.0$/, "") + "k";
35
+ if (r < 999500)
36
+ return Math.round(r / 1000) + "k";
37
+ return (r / 1_000_000).toFixed(r < 10_000_000 ? 2 : 1).replace(/\.?0+$/, "") + "M";
38
+ }
39
+ function fmtPct(x) {
40
+ if (x < 0.1)
41
+ return x === 0 ? "0%" : "< 0.1%";
42
+ return (x < 10 ? x.toFixed(1) : String(Math.round(x))) + "%";
43
+ }
44
+ function parseWindow(s) {
45
+ const m = /^(\d+(?:\.\d+)?)\s*([km]?)$/i.exec(s.trim());
46
+ if (!m)
47
+ return null;
48
+ const n = parseFloat(m[1]);
49
+ const mult = m[2].toLowerCase() === "k" ? 1000 : m[2].toLowerCase() === "m" ? 1_000_000 : 1;
50
+ const v = Math.round(n * mult);
51
+ return v >= 1000 ? v : null;
52
+ }
53
+ function fmtWindow(n) {
54
+ if (n >= 1_000_000 && n % 1_000_000 === 0)
55
+ return n / 1_000_000 + "M";
56
+ if (n >= 1000 && n % 1000 === 0)
57
+ return n / 1000 + "k";
58
+ return String(n);
59
+ }
60
+ function xmlEscape(s) {
61
+ return s.replace(/[&<>"']/g, (c) => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;", "'": "&apos;" })[c]);
62
+ }
63
+ /** Greedy word wrap; first line gets `first`, continuation lines get `indent`. */
64
+ function wrap(text, width, first = "", indent = "") {
65
+ const room = Math.max(20, width - Math.max(first.length, indent.length) - 1);
66
+ const words = text
67
+ .split(/\s+/)
68
+ .filter(Boolean)
69
+ .flatMap((w) => (w.length > room ? w.match(new RegExp(`[\\s\\S]{1,${room}}`, "gu")) ?? [w] : [w]));
70
+ const lines = [];
71
+ let cur = first;
72
+ let empty = true;
73
+ for (const w of words) {
74
+ if (!empty && cur.length + 1 + w.length > width) {
75
+ lines.push(cur);
76
+ cur = indent + w;
77
+ }
78
+ else {
79
+ cur += (empty ? "" : " ") + w;
80
+ }
81
+ empty = false;
82
+ }
83
+ lines.push(cur);
84
+ return lines;
85
+ }
86
+ /** Truncate from the LEFT, keeping the end (the file name) of a path-like label. */
87
+ function shortenLeft(s, max) {
88
+ if (s.length <= max)
89
+ return s;
90
+ return "…" + s.slice(s.length - (max - 1));
91
+ }
92
+ /** Shorten a row label: paths keep their tail (the file name); prose keeps its start. */
93
+ function shortenLabel(s, max) {
94
+ const t = s.replace(/^\.\//, "");
95
+ if (t.length <= max)
96
+ return t;
97
+ return /[\\/]/.test(t) && !/\s/.test(t) ? "…" + t.slice(t.length - (max - 1)) : t.slice(0, max - 1) + "…";
98
+ }
99
+ /** Locale-independent, ICU-independent string order (UTF-16 code units). Use instead of localeCompare for deterministic output. */
100
+ function cmp(a, b) {
101
+ return a < b ? -1 : a > b ? 1 : 0;
102
+ }
@@ -0,0 +1,35 @@
1
+ "use strict";
2
+ /** Secret-shaped string detection. Dependency-free so util/fmt can use it (sanitize() scrubs everything it displays). */
3
+ Object.defineProperty(exports, "__esModule", { value: true });
4
+ exports.SECRET_RES = void 0;
5
+ exports.findSecretLines = findSecretLines;
6
+ exports.scrubSecrets = scrubSecrets;
7
+ exports.SECRET_RES = [
8
+ /AKIA[0-9A-Z]{16}/,
9
+ /gh[pousr]_[A-Za-z0-9]{30,}/,
10
+ /github_pat_[A-Za-z0-9_]{30,}/,
11
+ /sk-[A-Za-z0-9_-]{20,}/,
12
+ /xox[baprs]-[A-Za-z0-9-]{10,}/,
13
+ /AIza[0-9A-Za-z_-]{35}/,
14
+ /-----BEGIN [A-Z ]*PRIVATE KEY-----/,
15
+ /(api[_-]?key|secret|token|passwd|password)["']?\s*[:=]\s*["']?[A-Za-z0-9_\-\/+=.]{16,}/i,
16
+ ];
17
+ /** 1-based line numbers containing something that looks like a secret. Values are never returned. */
18
+ function findSecretLines(text) {
19
+ const out = [];
20
+ const lines = text.split("\n");
21
+ for (let i = 0; i < lines.length && out.length < 50; i++) {
22
+ if (lines[i].length > 4000)
23
+ continue;
24
+ if (exports.SECRET_RES.some((re) => re.test(lines[i])))
25
+ out.push(i + 1);
26
+ }
27
+ return out;
28
+ }
29
+ /** Replace anything secret-looking inside a string we are about to display. */
30
+ function scrubSecrets(s) {
31
+ let out = s;
32
+ for (const re of exports.SECRET_RES)
33
+ out = out.replace(new RegExp(re.source, re.flags.includes("g") ? re.flags : re.flags + "g"), "[redacted]");
34
+ return out;
35
+ }
@@ -0,0 +1,5 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.VERSION = void 0;
4
+ // Generated by scripts/gen-version.mjs from package.json. Do not edit.
5
+ exports.VERSION = "1.0.0";
package/package.json CHANGED
@@ -1,6 +1,72 @@
1
1
  {
2
2
  "name": "prehello",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "1.0.0",
4
+ "description": "See what your coding agent (Claude Code, Cursor, Codex, GitHub Copilot, Gemini CLI) loads before you say hello, and what you could cut. Local-only, read-only, no API key.",
5
+ "license": "MIT",
6
+ "author": "Piyush Kumar",
7
+ "homepage": "https://github.com/PIYUSH-KUMAR1809/prehello#readme",
8
+ "repository": {
9
+ "type": "git",
10
+ "url": "git+https://github.com/PIYUSH-KUMAR1809/prehello.git"
11
+ },
12
+ "bugs": {
13
+ "url": "https://github.com/PIYUSH-KUMAR1809/prehello/issues"
14
+ },
15
+ "bin": {
16
+ "prehello": "dist/cli.js"
17
+ },
18
+ "files": [
19
+ "dist",
20
+ "assets",
21
+ "spec",
22
+ "README.md",
23
+ "LICENSE",
24
+ "CHANGELOG.md",
25
+ "THIRD_PARTY_NOTICES.md"
26
+ ],
27
+ "engines": {
28
+ "node": ">=22"
29
+ },
30
+ "scripts": {
31
+ "clean": "node -e \"require('fs').rmSync('dist',{recursive:true,force:true})\"",
32
+ "prebuild": "node scripts/gen-version.mjs",
33
+ "build": "tsc -p tsconfig.build.json",
34
+ "pretypecheck": "node scripts/gen-version.mjs",
35
+ "typecheck": "tsc -p tsconfig.json --noEmit",
36
+ "test": "npm run build && vitest run",
37
+ "prepack": "npm run clean && npm run build",
38
+ "prehello": "node dist/cli.js"
39
+ },
40
+ "keywords": [
41
+ "claude-code",
42
+ "cursor",
43
+ "codex",
44
+ "github-copilot",
45
+ "gemini-cli",
46
+ "agents-md",
47
+ "ai-agents",
48
+ "context-window",
49
+ "tokens",
50
+ "token-counter",
51
+ "mcp",
52
+ "sarif",
53
+ "cli",
54
+ "developer-tools"
55
+ ],
56
+ "dependencies": {
57
+ "gpt-tokenizer": "^4.1.0",
58
+ "picomatch": "^4.0.7",
59
+ "smol-toml": "^1.9.1"
60
+ },
61
+ "devDependencies": {
62
+ "@types/node": "^22.19.0",
63
+ "@types/picomatch": "^4.0.3",
64
+ "ajv": "^8.20.0",
65
+ "ajv-formats": "^3.0.1",
66
+ "typescript": "^5.9.3",
67
+ "vitest": "^3.2.7"
68
+ },
69
+ "optionalDependencies": {
70
+ "@resvg/resvg-wasm": "^2.6.2"
71
+ }
72
+ }
@@ -0,0 +1,33 @@
1
+ {
2
+ "version": 1,
3
+ "note": "Claude-to-o200k token ratios from PUBLISHED Anthropic count_tokens measurements (not measured by prehello). factor = median of the published points for that content kind; band = relative half-width used for low/high. Unvalidated until n >= 100 own measurements (scripts/calibrate-anthropic.mjs). See docs/ACCURACY.md.",
4
+ "claude": {
5
+ "measuredAt": "2026-10-23",
6
+ "tokenizer": "Claude 4.7 and later (Opus/Sonnet/Haiku 5.5 give identical counts per both sources)",
7
+ "n": 10,
8
+ "unvalidated": true,
9
+ "byKind": {
10
+ "markdown": {
11
+ "factor": 1.43,
12
+ "band": 0.15,
13
+ "points": [
14
+ { "ratio": 1.486, "what": "English prose reference text, 430 o200k tokens", "source": "https://measuredlab.com/tools/token-counter/" },
15
+ { "ratio": 1.38, "what": "5 Markdown/reST files, 44,621 o200k tokens", "source": "https://aitoollabs.dev/posts/count-tokens-in-codebase/" }
16
+ ]
17
+ },
18
+ "json": {
19
+ "factor": 1.35,
20
+ "band": 0.15,
21
+ "points": [
22
+ { "ratio": 1.282, "what": "JSON reference text, 535 o200k tokens", "source": "https://measuredlab.com/tools/token-counter/" },
23
+ { "ratio": 1.42, "what": "5 YAML/JSON/TOML files, 3,391 o200k tokens", "source": "https://aitoollabs.dev/posts/count-tokens-in-codebase/" }
24
+ ]
25
+ }
26
+ },
27
+ "legacyRatio": {
28
+ "value": 0.82,
29
+ "what": "Sonnet 4.6 (pre-4.7 tokenizer) used 18% fewer tokens than 4.7+ on the same 33 files",
30
+ "source": "https://aitoollabs.dev/posts/count-tokens-in-codebase/"
31
+ }
32
+ }
33
+ }
@@ -0,0 +1,22 @@
1
+ {
2
+ "agent": "claude-code",
3
+ "verifiedAt": "2026-10-25",
4
+ "verifiedNote": "Skills (locations, combined 1,536-char description cap, disable-model-invocation, skillOverrides, nested lazy loading, commands merged into skills, claude.ai synced skills) and plugin install records (installed_plugins.json with installPath, enabledPlugins, plugins cache layout) are from Claude Code docs (fetched 2026-10-25). The 1% skill listing budget is documented but its unit (tokens vs characters) is unclear. managed-mcp.json is documented by name; its location inside the managed directory is assumed. Not re-verified against a real install.",
5
+ "docs": {
6
+ "memory": "https://code.claude.com/docs/en/memory",
7
+ "mcp": "https://code.claude.com/docs/en/mcp",
8
+ "skills": "https://code.claude.com/docs/en/skills",
9
+ "plugins": "https://code.claude.com/docs/en/plugins/loading"
10
+ },
11
+ "caps": {
12
+ "instructionFileGuidelineLines": 200,
13
+ "memoryMaxLines": 200,
14
+ "memoryMaxBytes": 25600,
15
+ "skillDescriptionMaxChars": 1536,
16
+ "skillListingBudgetFractionOfWindow": 0.01,
17
+ "importMaxDepth": 4
18
+ },
19
+ "minVersions": {
20
+ "agentsMdFallback": "2.1.277"
21
+ }
22
+ }
@@ -0,0 +1,14 @@
1
+ {
2
+ "agent": "codex",
3
+ "verifiedAt": "2026-10-25",
4
+ "verifiedNote": "AGENTS.md discovery, the 32 KiB default, skills locations (.agents/skills cwd\u2192repo root, ~/.agents/skills, /etc/codex/skills), the name+description+path listing and [[skills.config]] disabling are from OpenAI's docs (fetched 2026-10-25). ASSUMED: project .codex/config.toml MCP, MCP schema loading (modelled upfront), ~/.codex/skills as a legacy skills path. Token accuracy was never validated.",
5
+ "docs": {
6
+ "agentsMd": "https://learn.chatgpt.com/docs/agent-configuration/agents-md",
7
+ "mcp": "https://learn.chatgpt.com/docs/extend/mcp?surface=cli",
8
+ "skills": "https://learn.chatgpt.com/docs/build-skills",
9
+ "config": "https://learn.chatgpt.com/docs/config-file/config-reference"
10
+ },
11
+ "caps": {
12
+ "projectDocMaxBytesDefault": 32768
13
+ }
14
+ }