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.
- package/CHANGELOG.md +40 -0
- package/LICENSE +21 -0
- package/README.md +515 -2
- package/THIRD_PARTY_NOTICES.md +33 -0
- package/assets/fonts/Inter-ExtraBold.subset.ttf +0 -0
- package/assets/fonts/Inter-Regular.subset.ttf +0 -0
- package/assets/fonts/Inter-SemiBold.subset.ttf +0 -0
- package/assets/fonts/OFL.txt +94 -0
- package/dist/adapters/claude-code.js +480 -0
- package/dist/adapters/codex.js +184 -0
- package/dist/adapters/common.js +157 -0
- package/dist/adapters/cursor.js +257 -0
- package/dist/adapters/skills.js +155 -0
- package/dist/adapters/types.js +2 -0
- package/dist/analyze/findings.js +82 -0
- package/dist/analyze/grade.js +55 -0
- package/dist/analyze/savings.js +111 -0
- package/dist/analyze/summary.js +57 -0
- package/dist/analyze/totals.js +21 -0
- package/dist/cli.js +454 -0
- package/dist/context.js +331 -0
- package/dist/fixtures.js +52 -0
- package/dist/fs-safe.js +222 -0
- package/dist/mcp/catalog.js +44 -0
- package/dist/mcp/configs.js +42 -0
- package/dist/model.js +11 -0
- package/dist/platform.js +115 -0
- package/dist/redact.js +39 -0
- package/dist/render/card.js +305 -0
- package/dist/render/fix.js +22 -0
- package/dist/render/json.js +97 -0
- package/dist/render/markdown.js +182 -0
- package/dist/render/png.js +162 -0
- package/dist/render/sarif.js +472 -0
- package/dist/render/terminal.js +329 -0
- package/dist/render/treemap.js +58 -0
- package/dist/safe-write.js +120 -0
- package/dist/scan.js +104 -0
- package/dist/spec.js +27 -0
- package/dist/text/comments.js +60 -0
- package/dist/text/frontmatter.js +90 -0
- package/dist/text/imports.js +81 -0
- package/dist/tokens/counter.js +41 -0
- package/dist/tokens/families.js +47 -0
- package/dist/util/fmt.js +102 -0
- package/dist/util/secrets.js +35 -0
- package/package.json +66 -4
- package/spec/claude-code.json +22 -0
- package/spec/codex.json +14 -0
- package/spec/cursor.json +15 -0
- 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,81 @@
|
|
|
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
|
+
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);
|
|
55
|
+
const real = (0, fs_safe_1.realpathOrSelf)(target);
|
|
56
|
+
if (seen.has(real))
|
|
57
|
+
continue;
|
|
58
|
+
if (depth >= MAX_DEPTH) {
|
|
59
|
+
skipped.push({ spec, reason: "depth>4" });
|
|
60
|
+
continue;
|
|
61
|
+
}
|
|
62
|
+
if (!allowed(real)) {
|
|
63
|
+
skipped.push({ spec, reason: "outside allowed roots" });
|
|
64
|
+
continue;
|
|
65
|
+
}
|
|
66
|
+
const r = (opts.read ?? ((t) => (0, fs_safe_1.readTextSafe)(t)))(target);
|
|
67
|
+
if (!r.ok) {
|
|
68
|
+
if (r.reason !== "missing")
|
|
69
|
+
skipped.push({ spec, reason: r.reason });
|
|
70
|
+
continue;
|
|
71
|
+
}
|
|
72
|
+
seen.add(real);
|
|
73
|
+
imported++;
|
|
74
|
+
const t = opts.stripComments ? (0, comments_1.stripHtmlComments)(r.text) : r.text;
|
|
75
|
+
parts.push(t);
|
|
76
|
+
walk(t, target, depth + 1);
|
|
77
|
+
}
|
|
78
|
+
};
|
|
79
|
+
walk(fileText, filePath, 0);
|
|
80
|
+
return { text: [fileText, ...parts].join("\n"), imported, skipped };
|
|
81
|
+
}
|
|
@@ -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,47 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.profileFor = profileFor;
|
|
4
|
+
function sym(raw, factor, bandPct) {
|
|
5
|
+
const est = Math.round(raw * factor);
|
|
6
|
+
return { est, low: Math.round(est * (1 - bandPct)), high: Math.round(est * (1 + bandPct)) };
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* Priors from PRODUCT_PLAN §7.2. All Claude/heuristic factors and bands are UNVALIDATED priors until
|
|
10
|
+
* calibrated against ground truth (see docs/accuracy.md). The OpenAI band is for the BPE itself.
|
|
11
|
+
*/
|
|
12
|
+
function profileFor(agent, method, claude) {
|
|
13
|
+
if (method === "heuristic") {
|
|
14
|
+
return {
|
|
15
|
+
id: "heuristic",
|
|
16
|
+
label: "heuristic chars/3.5 (no tokenizer)",
|
|
17
|
+
band: "±35%",
|
|
18
|
+
apply: (raw) => sym(raw, 1, 0.35),
|
|
19
|
+
unvalidated: true,
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
if (agent === "codex") {
|
|
23
|
+
return { id: "openai-o200k", label: "o200k BPE (OpenAI-family)", band: "±3%", apply: (raw) => sym(raw, 1.0, 0.03), unvalidated: false };
|
|
24
|
+
}
|
|
25
|
+
const f = claude === "current" ? 1.5 : 1.2;
|
|
26
|
+
const claudeProfile = {
|
|
27
|
+
id: `claude-${claude}`,
|
|
28
|
+
label: `o200k × ${f} (Claude-${claude} prior)`,
|
|
29
|
+
band: "±20%",
|
|
30
|
+
apply: (raw) => sym(raw, f, 0.2),
|
|
31
|
+
unvalidated: true,
|
|
32
|
+
};
|
|
33
|
+
if (agent === "claude-code")
|
|
34
|
+
return claudeProfile;
|
|
35
|
+
// Cursor: model not discoverable from disk -> span GPT-like (×1.0, −3%) to Claude-like (×f, +20%); est = midpoint.
|
|
36
|
+
return {
|
|
37
|
+
id: "cursor-mixed",
|
|
38
|
+
label: `o200k; model unknown, so midpoint of GPT-like (×1.0) and Claude-like (×${f}) priors`,
|
|
39
|
+
band: "wide: GPT-like −3% to Claude-like +20%",
|
|
40
|
+
apply: (raw) => {
|
|
41
|
+
const lo = Math.round(raw * 0.97);
|
|
42
|
+
const hi = Math.round(raw * f * 1.2);
|
|
43
|
+
return { est: Math.round((raw * (1 + f)) / 2), low: lo, high: hi };
|
|
44
|
+
},
|
|
45
|
+
unvalidated: true,
|
|
46
|
+
};
|
|
47
|
+
}
|
package/dist/util/fmt.js
ADDED
|
@@ -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) => ({ "&": "&", "<": "<", ">": ">", '"': """, "'": "'" })[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
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,68 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "prehello",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "See what your coding agent (Claude Code, Cursor, Codex) 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": ">=18"
|
|
29
|
+
},
|
|
30
|
+
"scripts": {
|
|
31
|
+
"clean": "node -e \"require('fs').rmSync('dist',{recursive:true,force:true})\"",
|
|
32
|
+
"build": "tsc -p tsconfig.build.json",
|
|
33
|
+
"typecheck": "tsc -p tsconfig.json --noEmit",
|
|
34
|
+
"test": "npm run build && vitest run",
|
|
35
|
+
"prepack": "npm run clean && npm run build",
|
|
36
|
+
"prehello": "node dist/cli.js"
|
|
37
|
+
},
|
|
38
|
+
"keywords": [
|
|
39
|
+
"claude-code",
|
|
40
|
+
"cursor",
|
|
41
|
+
"codex",
|
|
42
|
+
"agents-md",
|
|
43
|
+
"ai-agents",
|
|
44
|
+
"context-window",
|
|
45
|
+
"tokens",
|
|
46
|
+
"token-counter",
|
|
47
|
+
"mcp",
|
|
48
|
+
"sarif",
|
|
49
|
+
"cli",
|
|
50
|
+
"developer-tools"
|
|
51
|
+
],
|
|
52
|
+
"dependencies": {
|
|
53
|
+
"gpt-tokenizer": "^4.1.0",
|
|
54
|
+
"picomatch": "^4.0.7",
|
|
55
|
+
"smol-toml": "^1.9.1"
|
|
56
|
+
},
|
|
57
|
+
"devDependencies": {
|
|
58
|
+
"@types/node": "^20.19.43",
|
|
59
|
+
"@types/picomatch": "^4.0.3",
|
|
60
|
+
"ajv": "^8.20.0",
|
|
61
|
+
"ajv-formats": "^3.0.1",
|
|
62
|
+
"typescript": "^5.9.3",
|
|
63
|
+
"vitest": "^3.2.7"
|
|
64
|
+
},
|
|
65
|
+
"optionalDependencies": {
|
|
66
|
+
"@resvg/resvg-wasm": "^2.6.2"
|
|
67
|
+
}
|
|
68
|
+
}
|
|
@@ -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
|
+
}
|
package/spec/codex.json
ADDED
|
@@ -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
|
+
}
|
package/spec/cursor.json
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
{
|
|
2
|
+
"agent": "cursor",
|
|
3
|
+
"verifiedAt": "2026-10-25",
|
|
4
|
+
"verifiedNote": "Rule modes, skill locations (native + compat dirs), skill frontmatter (paths/globs, disable-model-invocation), plugin manifest/default component dirs, and lazy MCP loading (tool names static, descriptions on demand) are from Cursor's docs and blog (fetched 2026-10-25). UNVERIFIED (marked assumed): how Cursor injects agent-requested rule descriptions and the skills listing, ~/.cursor/skills-cursor (observed on a real Mac only), the plugin cache/marketplace on-disk layout and install state, and whether nested .cursor/rules apply (docs and forum disagree).",
|
|
5
|
+
"docs": {
|
|
6
|
+
"rules": "https://cursor.com/docs/rules",
|
|
7
|
+
"mcp": "https://cursor.com/docs/mcp",
|
|
8
|
+
"skills": "https://cursor.com/docs/skills",
|
|
9
|
+
"plugins": "https://cursor.com/docs/plugins",
|
|
10
|
+
"mcpLazy": "https://cursor.com/blog/dynamic-context-discovery"
|
|
11
|
+
},
|
|
12
|
+
"caps": {
|
|
13
|
+
"ruleGuidelineLines": 500
|
|
14
|
+
}
|
|
15
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 1,
|
|
3
|
+
"note": "MEASURED with scripts/measure-mcp.cjs: each server was installed from npm (--ignore-scripts), started offline in a network-less sandbox with a scrubbed env, and asked for tools/list over stdio. Tokens are o200k_base over the raw tool-list JSON (name; description+inputSchema). That is NOT the exact text any agent injects, and tool lists change between server versions, so treat these as ballpark for the stated version. measuredAt is a UTC date. Only servers that could be measured without credentials are listed; everything else stays unmeasured.",
|
|
4
|
+
"servers": [
|
|
5
|
+
{
|
|
6
|
+
"package": "@modelcontextprotocol/server-memory",
|
|
7
|
+
"tools": 9,
|
|
8
|
+
"nameTokens": 24,
|
|
9
|
+
"schemaTokens": 905,
|
|
10
|
+
"measuredAt": "2026-10-24",
|
|
11
|
+
"version": "1.0.0"
|
|
12
|
+
},
|
|
13
|
+
{
|
|
14
|
+
"package": "@modelcontextprotocol/server-filesystem",
|
|
15
|
+
"tools": 14,
|
|
16
|
+
"nameTokens": 36,
|
|
17
|
+
"schemaTokens": 1614,
|
|
18
|
+
"measuredAt": "2026-10-24",
|
|
19
|
+
"version": "1.0.0"
|
|
20
|
+
},
|
|
21
|
+
{
|
|
22
|
+
"package": "@modelcontextprotocol/server-sequential-thinking",
|
|
23
|
+
"tools": 1,
|
|
24
|
+
"nameTokens": 3,
|
|
25
|
+
"schemaTokens": 857,
|
|
26
|
+
"measuredAt": "2026-10-24",
|
|
27
|
+
"version": "1.0.0"
|
|
28
|
+
},
|
|
29
|
+
{
|
|
30
|
+
"package": "@modelcontextprotocol/server-everything",
|
|
31
|
+
"tools": 13,
|
|
32
|
+
"nameTokens": 48,
|
|
33
|
+
"schemaTokens": 997,
|
|
34
|
+
"measuredAt": "2026-10-24",
|
|
35
|
+
"version": "1.0.0"
|
|
36
|
+
},
|
|
37
|
+
{
|
|
38
|
+
"package": "@modelcontextprotocol/server-github",
|
|
39
|
+
"tools": 26,
|
|
40
|
+
"nameTokens": 76,
|
|
41
|
+
"schemaTokens": 3392,
|
|
42
|
+
"measuredAt": "2026-10-24",
|
|
43
|
+
"version": "2025.4.8"
|
|
44
|
+
},
|
|
45
|
+
{
|
|
46
|
+
"package": "@playwright/mcp",
|
|
47
|
+
"tools": 25,
|
|
48
|
+
"nameTokens": 70,
|
|
49
|
+
"schemaTokens": 3617,
|
|
50
|
+
"measuredAt": "2026-10-24",
|
|
51
|
+
"version": "0.0.83"
|
|
52
|
+
},
|
|
53
|
+
{
|
|
54
|
+
"package": "@upstash/context7-mcp",
|
|
55
|
+
"tools": 2,
|
|
56
|
+
"nameTokens": 6,
|
|
57
|
+
"schemaTokens": 1050,
|
|
58
|
+
"measuredAt": "2026-10-24",
|
|
59
|
+
"version": "4.4.4"
|
|
60
|
+
}
|
|
61
|
+
]
|
|
62
|
+
}
|