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.
- package/CHANGELOG.md +78 -0
- package/LICENSE +21 -0
- package/README.md +171 -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 +275 -0
- package/dist/adapters/gemini-cli.js +214 -0
- package/dist/adapters/github-copilot.js +249 -0
- package/dist/adapters/index.js +16 -0
- package/dist/adapters/skills.js +165 -0
- package/dist/adapters/types.js +2 -0
- package/dist/agents.js +54 -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/calibrate.js +265 -0
- package/dist/cli.js +591 -0
- package/dist/context.js +329 -0
- package/dist/diff.js +182 -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 +8 -0
- package/dist/platform.js +115 -0
- package/dist/redact.js +39 -0
- package/dist/render/card.js +306 -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 +318 -0
- package/dist/render/treemap.js +58 -0
- package/dist/safe-write.js +122 -0
- package/dist/scan.js +98 -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 +86 -0
- package/dist/tokens/counter.js +41 -0
- package/dist/tokens/families.js +79 -0
- package/dist/util/fmt.js +102 -0
- package/dist/util/secrets.js +35 -0
- package/dist/version.js +5 -0
- package/package.json +70 -4
- package/spec/calibration.json +33 -0
- package/spec/claude-code.json +22 -0
- package/spec/codex.json +14 -0
- package/spec/cursor.json +15 -0
- package/spec/gemini-cli.json +17 -0
- package/spec/github-copilot.json +14 -0
- package/spec/mcp-weights.json +62 -0
package/dist/fs-safe.js
ADDED
|
@@ -0,0 +1,222 @@
|
|
|
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.MAX_WALK_DEPTH = exports.MAX_WALK_ENTRIES = exports.DEFAULT_MAX_SCAN_MS = exports.DEFAULT_MAX_SCAN_BYTES = exports.MAX_FILE_BYTES = void 0;
|
|
7
|
+
exports.realpathOrSelf = realpathOrSelf;
|
|
8
|
+
exports.isInside = isInside;
|
|
9
|
+
exports.exists = exists;
|
|
10
|
+
exports.isDir = isDir;
|
|
11
|
+
exports.isFile = isFile;
|
|
12
|
+
exports.listDir = listDir;
|
|
13
|
+
exports.readTextSafe = readTextSafe;
|
|
14
|
+
exports.walkForFiles = walkForFiles;
|
|
15
|
+
exports.findGitRoot = findGitRoot;
|
|
16
|
+
exports.onPath = onPath;
|
|
17
|
+
/**
|
|
18
|
+
* The ONLY module that touches the filesystem for scanning. It exposes read-only operations;
|
|
19
|
+
* nothing in the scan path may call a write API (enforced by test/safety.test.ts).
|
|
20
|
+
*/
|
|
21
|
+
const node_fs_1 = __importDefault(require("node:fs"));
|
|
22
|
+
const node_path_1 = __importDefault(require("node:path"));
|
|
23
|
+
const platform_1 = require("./platform");
|
|
24
|
+
/**
|
|
25
|
+
* Per-file read cap. 4 MiB is Claude Code's documented limit for startup CLAUDE.md files ("Claude Code skips a file over
|
|
26
|
+
* 4 MiB and loads none of it"), so for Claude Code a skipped file here is a file the agent skips too. For other agents it is
|
|
27
|
+
* a generic safety cap (Codex has its own, much smaller, project_doc_max_bytes that the Codex adapter models separately).
|
|
28
|
+
*/
|
|
29
|
+
exports.MAX_FILE_BYTES = 4 * 1024 * 1024;
|
|
30
|
+
/** Whole-scan budgets (a safety net against pathological trees, not a feature knob). Bytes are deterministic; the clock is not. */
|
|
31
|
+
exports.DEFAULT_MAX_SCAN_BYTES = 64 * 1024 * 1024;
|
|
32
|
+
exports.DEFAULT_MAX_SCAN_MS = 20_000;
|
|
33
|
+
exports.MAX_WALK_ENTRIES = 5000;
|
|
34
|
+
exports.MAX_WALK_DEPTH = 8;
|
|
35
|
+
const IGNORE_DIRS = new Set([
|
|
36
|
+
"node_modules", ".git", "dist", "build", "out", "target", ".next", ".nuxt", ".venv", "venv", "__pycache__",
|
|
37
|
+
"vendor", ".cache", "coverage", ".gradle", ".idea", ".tox", ".turbo",
|
|
38
|
+
]);
|
|
39
|
+
/**
|
|
40
|
+
* Canonical path. Uses the OS-native realpath where available so that on case-insensitive filesystems
|
|
41
|
+
* (macOS APFS default, Windows NTFS) `claude.md` and `CLAUDE.md`, or a `/var` -> `/private/var` alias,
|
|
42
|
+
* resolve to ONE path and are never counted twice.
|
|
43
|
+
*/
|
|
44
|
+
function realpathOrSelf(p) {
|
|
45
|
+
try {
|
|
46
|
+
return (node_fs_1.default.realpathSync.native ?? node_fs_1.default.realpathSync)(p);
|
|
47
|
+
}
|
|
48
|
+
catch {
|
|
49
|
+
try {
|
|
50
|
+
return node_fs_1.default.realpathSync(p);
|
|
51
|
+
}
|
|
52
|
+
catch {
|
|
53
|
+
return node_path_1.default.resolve(p);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
function isInside(child, parent) {
|
|
58
|
+
return (0, platform_1.isInsideWith)(node_path_1.default, child, parent);
|
|
59
|
+
}
|
|
60
|
+
function exists(p) {
|
|
61
|
+
try {
|
|
62
|
+
node_fs_1.default.statSync(p);
|
|
63
|
+
return true;
|
|
64
|
+
}
|
|
65
|
+
catch {
|
|
66
|
+
return false;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
function isDir(p) {
|
|
70
|
+
try {
|
|
71
|
+
return node_fs_1.default.statSync(p).isDirectory();
|
|
72
|
+
}
|
|
73
|
+
catch {
|
|
74
|
+
return false;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
function isFile(p) {
|
|
78
|
+
try {
|
|
79
|
+
return node_fs_1.default.statSync(p).isFile();
|
|
80
|
+
}
|
|
81
|
+
catch {
|
|
82
|
+
return false;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
/** Sorted directory entries, or [] if unreadable. */
|
|
86
|
+
function listDir(p) {
|
|
87
|
+
try {
|
|
88
|
+
return node_fs_1.default.readdirSync(p, { withFileTypes: true }).sort((a, b) => (a.name < b.name ? -1 : a.name > b.name ? 1 : 0));
|
|
89
|
+
}
|
|
90
|
+
catch {
|
|
91
|
+
return [];
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* Read a text file defensively. `jail`: if given, the file's real path must be inside it
|
|
96
|
+
* (blocks symlinks in untrusted project trees that point at e.g. ~/.ssh).
|
|
97
|
+
*/
|
|
98
|
+
function readTextSafe(p, opts = {}) {
|
|
99
|
+
let st;
|
|
100
|
+
try {
|
|
101
|
+
st = node_fs_1.default.statSync(p);
|
|
102
|
+
}
|
|
103
|
+
catch (e) {
|
|
104
|
+
const code = e.code;
|
|
105
|
+
return code === "ENOENT" || code === "ENOTDIR" ? { ok: false, reason: "missing" } : { ok: false, reason: "unreadable", detail: String(code) };
|
|
106
|
+
}
|
|
107
|
+
if (!st.isFile())
|
|
108
|
+
return { ok: false, reason: "not-file" };
|
|
109
|
+
const real = realpathOrSelf(p);
|
|
110
|
+
if (opts.jail && !isInside(real, realpathOrSelf(opts.jail)))
|
|
111
|
+
return { ok: false, reason: "symlink-escape" };
|
|
112
|
+
if (st.size > (opts.maxBytes ?? exports.MAX_FILE_BYTES))
|
|
113
|
+
return { ok: false, reason: "too-large", detail: `${st.size} bytes` };
|
|
114
|
+
if (opts.budgetBytes !== undefined && st.size > opts.budgetBytes)
|
|
115
|
+
return { ok: false, reason: "budget", detail: `${st.size} bytes` };
|
|
116
|
+
let buf;
|
|
117
|
+
try {
|
|
118
|
+
buf = node_fs_1.default.readFileSync(p);
|
|
119
|
+
}
|
|
120
|
+
catch (e) {
|
|
121
|
+
return { ok: false, reason: "unreadable", detail: String(e.code) };
|
|
122
|
+
}
|
|
123
|
+
// Windows PowerShell 5 writes UTF-16 with a BOM for `echo x > AGENTS.md`; agents read these files as UTF-8, so say so instead of "binary".
|
|
124
|
+
if (buf.length >= 2 && ((buf[0] === 0xff && buf[1] === 0xfe) || (buf[0] === 0xfe && buf[1] === 0xff)))
|
|
125
|
+
return { ok: false, reason: "utf16" };
|
|
126
|
+
if (buf.subarray(0, 8000).includes(0))
|
|
127
|
+
return { ok: false, reason: "binary" };
|
|
128
|
+
let text = buf.toString("utf8");
|
|
129
|
+
if (text.charCodeAt(0) === 0xfeff)
|
|
130
|
+
text = text.slice(1);
|
|
131
|
+
text = text.replace(/\r\n?/g, "\n");
|
|
132
|
+
return { ok: true, text, bytes: buf.length, realPath: real };
|
|
133
|
+
}
|
|
134
|
+
/**
|
|
135
|
+
* Bounded project walk (no symlink following, ignores build dirs, dot-dirs, ≤5k entries, depth ≤ 8).
|
|
136
|
+
* Returns directories (other than root) that contain any of the candidate relative paths.
|
|
137
|
+
*/
|
|
138
|
+
function walkForFiles(root, candidates, opts = {}) {
|
|
139
|
+
const hits = [];
|
|
140
|
+
const skipped = [];
|
|
141
|
+
const maxEntries = opts.maxEntries ?? exports.MAX_WALK_ENTRIES;
|
|
142
|
+
let seen = 0;
|
|
143
|
+
let truncated = false;
|
|
144
|
+
let timedOut = false;
|
|
145
|
+
const visit = (dir, depth, rel) => {
|
|
146
|
+
if (truncated || timedOut || (opts.stopAtFirstHit && hits.length))
|
|
147
|
+
return;
|
|
148
|
+
if (opts.timeUp?.()) {
|
|
149
|
+
timedOut = true;
|
|
150
|
+
return;
|
|
151
|
+
}
|
|
152
|
+
const entries = listDir(dir);
|
|
153
|
+
seen += entries.length;
|
|
154
|
+
if (seen > maxEntries) {
|
|
155
|
+
truncated = true;
|
|
156
|
+
return;
|
|
157
|
+
}
|
|
158
|
+
if (dir !== root || opts.includeRoot) {
|
|
159
|
+
// a trailing "/" in a candidate means "a directory with this relative path"
|
|
160
|
+
const present = candidates.filter((c) => (c.endsWith("/") ? isDir(node_path_1.default.join(dir, c)) : isFile(node_path_1.default.join(dir, c))) && !isSymlink(node_path_1.default.join(dir, c)));
|
|
161
|
+
if (present.length)
|
|
162
|
+
hits.push({ dir, names: present });
|
|
163
|
+
}
|
|
164
|
+
if (depth >= exports.MAX_WALK_DEPTH)
|
|
165
|
+
return;
|
|
166
|
+
for (const e of entries) {
|
|
167
|
+
if (!e.isDirectory() || e.isSymbolicLink())
|
|
168
|
+
continue;
|
|
169
|
+
if (IGNORE_DIRS.has(e.name) || e.name.startsWith("."))
|
|
170
|
+
continue;
|
|
171
|
+
const childRel = [...rel, e.name];
|
|
172
|
+
if (opts.skipDir?.(e.name, childRel)) {
|
|
173
|
+
skipped.push(node_path_1.default.join(dir, e.name));
|
|
174
|
+
continue;
|
|
175
|
+
}
|
|
176
|
+
visit(node_path_1.default.join(dir, e.name), depth + 1, childRel);
|
|
177
|
+
}
|
|
178
|
+
};
|
|
179
|
+
visit(root, 0, []);
|
|
180
|
+
return { hits, truncated, timedOut, entries: Math.min(seen, maxEntries), skipped };
|
|
181
|
+
}
|
|
182
|
+
function isSymlink(p) {
|
|
183
|
+
try {
|
|
184
|
+
return node_fs_1.default.lstatSync(p).isSymbolicLink();
|
|
185
|
+
}
|
|
186
|
+
catch {
|
|
187
|
+
return false;
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
/** Find the nearest ancestor (inclusive) containing `.git`. */
|
|
191
|
+
function findGitRoot(start, ceiling) {
|
|
192
|
+
let dir = start;
|
|
193
|
+
for (;;) {
|
|
194
|
+
if (exists(node_path_1.default.join(dir, ".git")))
|
|
195
|
+
return dir;
|
|
196
|
+
if (ceiling && dir === ceiling)
|
|
197
|
+
return undefined;
|
|
198
|
+
const parent = node_path_1.default.dirname(dir);
|
|
199
|
+
if (parent === dir)
|
|
200
|
+
return undefined;
|
|
201
|
+
dir = parent;
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
/**
|
|
205
|
+
* Is an executable named `name` on the given PATH string? (read-only stat calls)
|
|
206
|
+
* On Windows the binary is `claude.exe` / `claude.cmd` (npm shims), never bare `claude`, so PATHEXT is honoured.
|
|
207
|
+
*/
|
|
208
|
+
function onPath(name, pathVar, opts = {}) {
|
|
209
|
+
if (!pathVar)
|
|
210
|
+
return false;
|
|
211
|
+
const win = opts.platform === "win32";
|
|
212
|
+
const delim = opts.platform ? (win ? ";" : ":") : node_path_1.default.delimiter;
|
|
213
|
+
const exts = win ? ["", ...(opts.pathExt || ".COM;.EXE;.BAT;.CMD").split(";").filter(Boolean).map((e) => e.toLowerCase()), ".ps1"] : [""];
|
|
214
|
+
for (const d of pathVar.split(delim)) {
|
|
215
|
+
if (!d)
|
|
216
|
+
continue;
|
|
217
|
+
for (const e of exts)
|
|
218
|
+
if (isFile(node_path_1.default.join(d, name + e)))
|
|
219
|
+
return true;
|
|
220
|
+
}
|
|
221
|
+
return false;
|
|
222
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
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.loadCatalog = loadCatalog;
|
|
7
|
+
exports.lookup = lookup;
|
|
8
|
+
const node_fs_1 = __importDefault(require("node:fs"));
|
|
9
|
+
const platform_1 = require("../platform");
|
|
10
|
+
const spec_1 = require("../spec");
|
|
11
|
+
function valid(e) {
|
|
12
|
+
if (typeof e !== "object" || e === null)
|
|
13
|
+
return false;
|
|
14
|
+
const x = e;
|
|
15
|
+
return ((typeof x.name === "string" || typeof x.package === "string") &&
|
|
16
|
+
Number.isFinite(x.nameTokens) && x.nameTokens >= 0 &&
|
|
17
|
+
Number.isFinite(x.schemaTokens) && x.schemaTokens >= 0 &&
|
|
18
|
+
Number.isFinite(x.tools) &&
|
|
19
|
+
typeof x.measuredAt === "string");
|
|
20
|
+
}
|
|
21
|
+
function loadCatalog(extraPath, noBundled = false) {
|
|
22
|
+
const entries = [];
|
|
23
|
+
const warnings = [];
|
|
24
|
+
// user-supplied file first: lookup() returns the first match, so a user's own measurement beats the bundled one
|
|
25
|
+
for (const p of [extraPath, noBundled ? undefined : (0, spec_1.loadBundledCatalogPath)()].filter((x) => !!x)) {
|
|
26
|
+
try {
|
|
27
|
+
const j = JSON.parse((0, platform_1.decodeUserText)(node_fs_1.default.readFileSync(p)));
|
|
28
|
+
for (const e of j.servers ?? []) {
|
|
29
|
+
if (valid(e))
|
|
30
|
+
entries.push(e);
|
|
31
|
+
else
|
|
32
|
+
warnings.push("an MCP catalog entry was malformed and ignored");
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
catch {
|
|
36
|
+
if (p === extraPath)
|
|
37
|
+
warnings.push("MCP catalog file could not be read or parsed; ignored");
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
return { entries, warnings };
|
|
41
|
+
}
|
|
42
|
+
function lookup(entries, def) {
|
|
43
|
+
return entries.find((e) => (e.name && e.name === def.name) || (e.package && def.pkg && e.package === def.pkg));
|
|
44
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.MAX_MCP_SERVERS = void 0;
|
|
4
|
+
exports.parseMcpMap = parseMcpMap;
|
|
5
|
+
exports.parseCodexToml = parseCodexToml;
|
|
6
|
+
const smol_toml_1 = require("smol-toml");
|
|
7
|
+
const fmt_1 = require("../util/fmt");
|
|
8
|
+
function isObj(x) {
|
|
9
|
+
return typeof x === "object" && x !== null && !Array.isArray(x);
|
|
10
|
+
}
|
|
11
|
+
function guessPkg(entry) {
|
|
12
|
+
const args = Array.isArray(entry.args) ? entry.args.filter((a) => typeof a === "string") : [];
|
|
13
|
+
const cmd = typeof entry.command === "string" ? entry.command : "";
|
|
14
|
+
if (!/(^|\/)(npx|bunx|pnpx|uvx|pipx)$/.test(cmd) && !/^(npx|bunx|pnpx|uvx|pipx)$/.test(cmd))
|
|
15
|
+
return undefined;
|
|
16
|
+
const a = args.find((x) => !x.startsWith("-"));
|
|
17
|
+
return a ? a.replace(/@[\w.^~<>=*-]+$/, (m) => (a.startsWith("@") && a.indexOf("@", 1) === -1 ? m : "")) : undefined;
|
|
18
|
+
}
|
|
19
|
+
/** A real config has a handful of servers. A hostile one with 200,000 entries must not become a 180 MB report. */
|
|
20
|
+
exports.MAX_MCP_SERVERS = 500;
|
|
21
|
+
/** Extract server definitions from a `{ mcpServers: {...} }`-style map. Values (env, headers, args) are never retained. */
|
|
22
|
+
function parseMcpMap(map, warn) {
|
|
23
|
+
if (!isObj(map))
|
|
24
|
+
return [];
|
|
25
|
+
const out = [];
|
|
26
|
+
const names = Object.keys(map);
|
|
27
|
+
if (names.length > exports.MAX_MCP_SERVERS)
|
|
28
|
+
warn?.(`an MCP config lists ${names.length} servers; only the first ${exports.MAX_MCP_SERVERS} were read, so MCP counts are a lower bound`);
|
|
29
|
+
for (const name of names.slice(0, exports.MAX_MCP_SERVERS)) {
|
|
30
|
+
const v = map[name];
|
|
31
|
+
if (!isObj(v))
|
|
32
|
+
continue;
|
|
33
|
+
const t = typeof v.type === "string" ? v.type : typeof v.transport === "string" ? v.transport : undefined;
|
|
34
|
+
const transport = t === "http" || t === "streamable-http" ? "http" : t === "sse" ? "sse" : t === "stdio" || t === "local" || typeof v.command === "string" ? "stdio" : typeof v.url === "string" || typeof v.httpUrl === "string" ? "http" : "unknown";
|
|
35
|
+
out.push({ name: (0, fmt_1.sanitize)(name, 60), transport, pkg: guessPkg(v), disabled: v.disabled === true || v.enabled === false });
|
|
36
|
+
}
|
|
37
|
+
return out;
|
|
38
|
+
}
|
|
39
|
+
function parseCodexToml(text, warn) {
|
|
40
|
+
const cfg = (0, smol_toml_1.parse)(text);
|
|
41
|
+
return { servers: parseMcpMap(cfg.mcp_servers, warn), config: cfg };
|
|
42
|
+
}
|
package/dist/model.js
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.LOAD_MODES = exports.ALL_AGENTS = exports.AGENT_NAMES = exports.SCHEMA_VERSION = void 0;
|
|
4
|
+
exports.SCHEMA_VERSION = 1;
|
|
5
|
+
var agents_1 = require("./agents");
|
|
6
|
+
Object.defineProperty(exports, "AGENT_NAMES", { enumerable: true, get: function () { return agents_1.AGENT_NAMES; } });
|
|
7
|
+
Object.defineProperty(exports, "ALL_AGENTS", { enumerable: true, get: function () { return agents_1.ALL_AGENTS; } });
|
|
8
|
+
exports.LOAD_MODES = ["always", "conditional", "deferred", "on-demand", "unknown"];
|
package/dist/platform.js
ADDED
|
@@ -0,0 +1,115 @@
|
|
|
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.isWinAbsolute = isWinAbsolute;
|
|
7
|
+
exports.resolveHomeDir = resolveHomeDir;
|
|
8
|
+
exports.isWindowsReservedName = isWindowsReservedName;
|
|
9
|
+
exports.hasWindowsStream = hasWindowsStream;
|
|
10
|
+
exports.unicodeSupported = unicodeSupported;
|
|
11
|
+
exports.toAscii = toAscii;
|
|
12
|
+
exports.toPosix = toPosix;
|
|
13
|
+
exports.makeGlobMatcher = makeGlobMatcher;
|
|
14
|
+
exports.isInsideWith = isInsideWith;
|
|
15
|
+
exports.decodeUserText = decodeUserText;
|
|
16
|
+
/**
|
|
17
|
+
* Platform helpers: everything that differs between Linux, macOS and Windows lives here, as pure functions that take the
|
|
18
|
+
* platform (and a `path` implementation) as arguments, so the Windows behaviour can be unit-tested on any machine.
|
|
19
|
+
*/
|
|
20
|
+
const node_path_1 = __importDefault(require("node:path"));
|
|
21
|
+
const picomatch_1 = __importDefault(require("picomatch"));
|
|
22
|
+
/** Is `p` an absolute path in the Windows sense (C:\x, C:/x, \\server\share)? Works on any host. */
|
|
23
|
+
function isWinAbsolute(p) {
|
|
24
|
+
return node_path_1.default.win32.isAbsolute(p) && /^(?:[a-zA-Z]:[\\/]|\\\\|\/\/)/.test(p);
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* The home directory the AGENTS use. Claude Code, Cursor (Electron) and Codex all take it from the OS, not from $HOME:
|
|
28
|
+
* on Windows that is %USERPROFILE%. Git Bash sets HOME to a POSIX-style "/c/Users/me" that Node cannot open, and a network HOME
|
|
29
|
+
* ("H:\") can differ from the profile folder the agents read, so on Windows HOME is only a last resort.
|
|
30
|
+
*/
|
|
31
|
+
function resolveHomeDir(env, platform, osHomedir) {
|
|
32
|
+
if (platform === "win32") {
|
|
33
|
+
if (env.USERPROFILE && isWinAbsolute(env.USERPROFILE))
|
|
34
|
+
return env.USERPROFILE;
|
|
35
|
+
if (env.HOMEDRIVE && env.HOMEPATH && isWinAbsolute(env.HOMEDRIVE + env.HOMEPATH))
|
|
36
|
+
return env.HOMEDRIVE + env.HOMEPATH;
|
|
37
|
+
if (env.HOME && isWinAbsolute(env.HOME))
|
|
38
|
+
return env.HOME;
|
|
39
|
+
return osHomedir();
|
|
40
|
+
}
|
|
41
|
+
if (env.HOME && node_path_1.default.posix.isAbsolute(env.HOME))
|
|
42
|
+
return env.HOME;
|
|
43
|
+
return osHomedir();
|
|
44
|
+
}
|
|
45
|
+
/** Windows device names: opening "nul.svg" or "COM1.md" writes to a device (silently discarding the report), whatever the extension. */
|
|
46
|
+
function isWindowsReservedName(file) {
|
|
47
|
+
const base = file.split(/[\\/]/).pop() ?? "";
|
|
48
|
+
const stem = base.split(".")[0].replace(/[ .]+$/, "");
|
|
49
|
+
return /^(con|prn|aux|nul|conin\$|conout\$|com[0-9\u00b9\u00b2\u00b3]|lpt[0-9\u00b9\u00b2\u00b3])$/i.test(stem);
|
|
50
|
+
}
|
|
51
|
+
/** On Windows a ":" after the drive letter is an NTFS alternate data stream ("report.md:hidden"): never write one. */
|
|
52
|
+
function hasWindowsStream(file) {
|
|
53
|
+
return file.replace(/^(?:\\\\\?\\)?[a-zA-Z]:/, "").includes(":");
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Can this terminal draw the box/block/arrow glyphs prehello uses? Mirrors the widely used "is-unicode-supported" rule:
|
|
57
|
+
* the Linux virtual console (TERM=linux) and the legacy Windows console host (cmd.exe / classic PowerShell window) cannot,
|
|
58
|
+
* Windows Terminal, VS Code, JetBrains, ConEmu, Alacritty and modern xterm-likes can.
|
|
59
|
+
*/
|
|
60
|
+
function unicodeSupported(env, platform) {
|
|
61
|
+
if (env.PREHELLO_ASCII === "1")
|
|
62
|
+
return false;
|
|
63
|
+
if (platform !== "win32")
|
|
64
|
+
return env.TERM !== "linux";
|
|
65
|
+
return (!!env.WT_SESSION ||
|
|
66
|
+
!!env.TERMINUS_SUBLIME ||
|
|
67
|
+
env.ConEmuTask === "{cmd::Cmder}" ||
|
|
68
|
+
env.TERM_PROGRAM === "vscode" ||
|
|
69
|
+
env.TERM_PROGRAM === "Terminus-Sublime" ||
|
|
70
|
+
env.TERM === "xterm-256color" ||
|
|
71
|
+
env.TERM === "alacritty" ||
|
|
72
|
+
env.TERMINAL_EMULATOR === "JetBrains-JediTerm");
|
|
73
|
+
}
|
|
74
|
+
const ASCII_MAP = {
|
|
75
|
+
"·": "-", "–": "-", "—": "-", "−": "-", "…": "...", "≥": ">=", "≤": "<=", "→": "->", "⚠": "!", "ℹ": "i", "•": "*",
|
|
76
|
+
"█": "#", "▌": "=", "‘": "'", "’": "'", "“": '"', "”": '"', "≈": "~", "×": "x",
|
|
77
|
+
};
|
|
78
|
+
/** Replace every non-ASCII glyph prehello prints with an ASCII stand-in (accents and other letters become "?"). */
|
|
79
|
+
function toAscii(s) {
|
|
80
|
+
// eslint-disable-next-line no-control-regex
|
|
81
|
+
return s.replace(/[^\x00-\x7f]/gu, (c) => ASCII_MAP[c] ?? "?");
|
|
82
|
+
}
|
|
83
|
+
function toPosix(p) {
|
|
84
|
+
return p.replace(/\\/g, "/");
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* Glob matcher for settings such as claudeMdExcludes. Always matches forward-slash paths: a Windows path is converted first,
|
|
88
|
+
* and backslashes in a pattern are path separators there (not escapes). Windows paths are case-insensitive.
|
|
89
|
+
*/
|
|
90
|
+
function makeGlobMatcher(patterns, platform) {
|
|
91
|
+
const win = platform === "win32";
|
|
92
|
+
const pats = win ? patterns.map(toPosix) : patterns;
|
|
93
|
+
const m = (0, picomatch_1.default)(pats, { dot: true, windows: false, nocase: win });
|
|
94
|
+
return (file) => m(win ? toPosix(file) : file);
|
|
95
|
+
}
|
|
96
|
+
/** True if `child` is `parent` or inside it. A directory called "..foo" is inside; "../x" is not. */
|
|
97
|
+
function isInsideWith(pm, child, parent) {
|
|
98
|
+
const rel = pm.relative(parent, child);
|
|
99
|
+
return rel === "" || (rel !== ".." && !rel.startsWith(".." + pm.sep) && !pm.isAbsolute(rel));
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* Text a USER hands to prehello (--mcp-catalog file, --paste-user-rules file or stdin). Windows editors and PowerShell 5 write
|
|
103
|
+
* a UTF-8 BOM or UTF-16: both are decoded here instead of failing as "malformed JSON" or counting a BOM as a token.
|
|
104
|
+
* CRLF is normalised to LF for pasted rules so the same text counts the same on every platform.
|
|
105
|
+
*/
|
|
106
|
+
function decodeUserText(buf) {
|
|
107
|
+
let s;
|
|
108
|
+
if (buf.length >= 2 && buf[0] === 0xff && buf[1] === 0xfe)
|
|
109
|
+
s = buf.subarray(2).toString("utf16le");
|
|
110
|
+
else if (buf.length >= 2 && buf[0] === 0xfe && buf[1] === 0xff)
|
|
111
|
+
s = Buffer.from(buf.subarray(2)).swap16().toString("utf16le");
|
|
112
|
+
else
|
|
113
|
+
s = buf.toString("utf8");
|
|
114
|
+
return s.replace(/^\ufeff/, "");
|
|
115
|
+
}
|
package/dist/redact.js
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
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.scrubSecrets = exports.findSecretLines = void 0;
|
|
7
|
+
exports.displayPath = displayPath;
|
|
8
|
+
const node_path_1 = __importDefault(require("node:path"));
|
|
9
|
+
const fmt_1 = require("./util/fmt");
|
|
10
|
+
const secrets_1 = require("./util/secrets");
|
|
11
|
+
Object.defineProperty(exports, "findSecretLines", { enumerable: true, get: function () { return secrets_1.findSecretLines; } });
|
|
12
|
+
Object.defineProperty(exports, "scrubSecrets", { enumerable: true, get: function () { return secrets_1.scrubSecrets; } });
|
|
13
|
+
/** True when `rel` (from path.relative) leaves its base: "..", "../x", but not a child called "..foo". */
|
|
14
|
+
function escapes(pm, rel) {
|
|
15
|
+
return rel === ".." || rel.startsWith(".." + pm.sep) || pm.isAbsolute(rel);
|
|
16
|
+
}
|
|
17
|
+
/** Display form of a path: ./relative inside the project, ~/ inside HOME, else absolute. Always forward slashes. Sanitised + scrubbed. */
|
|
18
|
+
function displayPath(p, c) {
|
|
19
|
+
const pm = c.pathMod ?? node_path_1.default;
|
|
20
|
+
const slash = (x) => x.split(pm.sep).join("/");
|
|
21
|
+
let out;
|
|
22
|
+
const rel = pm.relative(c.cwd, p);
|
|
23
|
+
if (rel === "")
|
|
24
|
+
out = ".";
|
|
25
|
+
else if (!escapes(pm, rel))
|
|
26
|
+
out = "./" + slash(rel);
|
|
27
|
+
else {
|
|
28
|
+
const hr = pm.relative(c.home, p);
|
|
29
|
+
if (hr === "")
|
|
30
|
+
out = "~";
|
|
31
|
+
else if (!escapes(pm, hr))
|
|
32
|
+
out = "~/" + slash(hr);
|
|
33
|
+
else if (rel.split(pm.sep).filter((x) => x === "..").length <= 4 && !pm.isAbsolute(rel))
|
|
34
|
+
out = slash(rel); // ../../CLAUDE.md (ancestor directory)
|
|
35
|
+
else
|
|
36
|
+
out = slash(p); // absolute elsewhere; on Windows "C:/Users/..." rather than backslashes (JSON/markdown-safe)
|
|
37
|
+
}
|
|
38
|
+
return (0, secrets_1.scrubSecrets)((0, fmt_1.sanitize)(out, 200));
|
|
39
|
+
}
|