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,184 @@
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.detectCodex = detectCodex;
7
+ exports.discoverCodex = discoverCodex;
8
+ const node_path_1 = __importDefault(require("node:path"));
9
+ const spec_1 = require("../spec");
10
+ const fs_safe_1 = require("../fs-safe");
11
+ const common_1 = require("./common");
12
+ const configs_1 = require("../mcp/configs");
13
+ const skills_1 = require("./skills");
14
+ const fs_safe_2 = require("../fs-safe");
15
+ const AGENT = "codex";
16
+ const ev = (doc) => (0, spec_1.evidenceFor)(AGENT, doc);
17
+ function codexHome(ctx) {
18
+ return ctx.opts.env.CODEX_HOME || node_path_1.default.join(ctx.home, ".codex");
19
+ }
20
+ function detectCodex(ctx) {
21
+ const by = [];
22
+ if ((0, fs_safe_1.isDir)(codexHome(ctx)))
23
+ by.push("config dir");
24
+ if ((0, fs_safe_1.onPath)("codex", ctx.opts.env.PATH ?? ctx.opts.env.Path, { platform: ctx.opts.platform, pathExt: ctx.opts.env.PATHEXT }))
25
+ by.push("binary on PATH");
26
+ const repo = [];
27
+ if ((0, fs_safe_1.isFile)(node_path_1.default.join(ctx.cwd, "AGENTS.md")))
28
+ repo.push(node_path_1.default.join(ctx.cwd, "AGENTS.md"));
29
+ if ((0, fs_safe_1.isFile)(node_path_1.default.join(ctx.cwd, "AGENTS.override.md")))
30
+ repo.push(node_path_1.default.join(ctx.cwd, "AGENTS.override.md"));
31
+ if ((0, fs_safe_1.isDir)(node_path_1.default.join(ctx.cwd, ".codex")))
32
+ repo.push(node_path_1.default.join(ctx.cwd, ".codex"));
33
+ return { detectedBy: by, repoFiles: repo };
34
+ }
35
+ function discoverCodex(ctx) {
36
+ const caps = (0, spec_1.loadSpec)(AGENT).caps;
37
+ const items = [];
38
+ const home = codexHome(ctx);
39
+ const cfgPath = node_path_1.default.join(home, "config.toml");
40
+ let cfg = {};
41
+ let servers = [];
42
+ const cfgText = ctx.read(cfgPath, { maxBytes: 5 * 1024 * 1024 });
43
+ if (cfgText.ok) {
44
+ try {
45
+ const p = (0, configs_1.parseCodexToml)(cfgText.text, (m) => ctx.warn(m));
46
+ cfg = p.config;
47
+ servers = p.servers;
48
+ }
49
+ catch {
50
+ ctx.warn(`${ctx.display(cfgPath)}: could not parse as TOML, skipped`);
51
+ }
52
+ }
53
+ const maxBytes = typeof cfg.project_doc_max_bytes === "number" && cfg.project_doc_max_bytes >= 0 ? cfg.project_doc_max_bytes : caps.projectDocMaxBytesDefault;
54
+ const fallbackNames = (Array.isArray(cfg.project_doc_fallback_filenames) ? cfg.project_doc_fallback_filenames : []).filter((x) => typeof x === "string" && x.length > 0 && !/[\\/]/.test(x) && x !== "." && x !== "..");
55
+ // Global: AGENTS.override.md else AGENTS.md (first non-empty)
56
+ for (const name of ["AGENTS.override.md", "AGENTS.md"]) {
57
+ const f = node_path_1.default.join(home, name);
58
+ const it = (0, common_1.loadInstruction)(ctx, f, { agent: AGENT, scope: "user", loadMode: "always", label: `~/.codex/${name}`, cardLabel: "global AGENTS.md", stripComments: false, imports: false, confidence: "documented", evidence: ev("agentsMd") });
59
+ if (it) {
60
+ items.push(it);
61
+ const shadowed = node_path_1.default.join(home, "AGENTS.md");
62
+ if (name === "AGENTS.override.md" && (0, fs_safe_1.isFile)(shadowed))
63
+ ctx.exclude(AGENT, shadowed, "shadowed by AGENTS.override.md in the same directory (Codex reads only one)");
64
+ break;
65
+ }
66
+ }
67
+ // Project chain: git root (or cwd) -> cwd; one file per dir: override -> AGENTS.md -> fallbacks
68
+ const root = ctx.gitRoot ?? ctx.cwd;
69
+ const chain = ctx.ancestorDirs().filter((d) => (0, fs_safe_1.isInside)(d, root));
70
+ let remaining = maxBytes;
71
+ for (const dir of chain) {
72
+ const names = ["AGENTS.override.md", "AGENTS.md", ...fallbackNames];
73
+ for (const name of names) {
74
+ const f = node_path_1.default.join(dir, name);
75
+ if (!(0, fs_safe_1.isFile)(f))
76
+ continue;
77
+ const jail = ctx.jailFor(dir);
78
+ const probe = ctx.read(f, { jail });
79
+ if (!probe.ok || !probe.text.trim())
80
+ continue;
81
+ if (remaining <= 0) {
82
+ ctx.exclude(AGENT, f, `beyond project_doc_max_bytes (${maxBytes} B): Codex stops reading AGENTS.md files here`);
83
+ break;
84
+ }
85
+ const bytes = Buffer.byteLength(probe.text);
86
+ const it = (0, common_1.loadInstruction)(ctx, f, {
87
+ agent: AGENT,
88
+ scope: dir === ctx.cwd ? "project" : "ancestor",
89
+ loadMode: "always",
90
+ label: ctx.display(f),
91
+ cardLabel: name === "AGENTS.md" ? "AGENTS.md" : name,
92
+ jail,
93
+ stripComments: false,
94
+ imports: false,
95
+ confidence: "documented",
96
+ evidence: ev("agentsMd"),
97
+ transform: (raw) => {
98
+ if (bytes <= remaining)
99
+ return { text: raw };
100
+ const t = Buffer.from(raw).subarray(0, remaining).toString("utf8");
101
+ ctx.addFinding({ id: "F12", severity: "warn", agent: AGENT, message: `${ctx.display(f)} is truncated by Codex at project_doc_max_bytes (${maxBytes} B): only the first ${remaining} of ${bytes} bytes load`, paths: [f], confidence: "high", fix: "Shorten the AGENTS.md chain or raise project_doc_max_bytes in ~/.codex/config.toml." });
102
+ return { text: t, notes: [`truncated at project_doc_max_bytes: ${remaining}/${bytes} bytes loaded`] };
103
+ },
104
+ });
105
+ if (it) {
106
+ items.push(it);
107
+ remaining -= Math.min(bytes, remaining) + 2; // 2 bytes ~ separator between concatenated files
108
+ for (const other of names.slice(names.indexOf(name) + 1)) {
109
+ const of = node_path_1.default.join(dir, other);
110
+ if ((0, fs_safe_1.isFile)(of))
111
+ ctx.exclude(AGENT, of, `shadowed by ${name} in the same directory (Codex reads at most one file per directory)`);
112
+ }
113
+ }
114
+ break; // max one file per directory
115
+ }
116
+ }
117
+ // nested AGENTS.md below cwd: Codex walks root -> cwd only
118
+ for (const hit of ctx.walk().hits) {
119
+ if (hit.names.includes("AGENTS.md"))
120
+ ctx.exclude(AGENT, node_path_1.default.join(hit.dir, "AGENTS.md"), "below the launch directory (Codex reads AGENTS.md root→cwd only)");
121
+ }
122
+ // MCP: global config.toml; project .codex/config.toml (trusted projects; assumed)
123
+ for (const def of servers) {
124
+ if (def.disabled)
125
+ continue;
126
+ items.push(...(0, common_1.mcpItems)(ctx, { agent: AGENT, def, source: cfgPath, scope: "user", mode: "full", evidence: ev("mcp"), notes: ["Codex MCP schema loading modelled as upfront (unverified)"], index: items.length }));
127
+ }
128
+ const projCfg = node_path_1.default.join(ctx.cwd, ".codex", "config.toml");
129
+ const pr = ctx.read(projCfg, { jail: ctx.jailFor(ctx.cwd) });
130
+ if (pr.ok) {
131
+ try {
132
+ for (const def of (0, configs_1.parseCodexToml)(pr.text, (m) => ctx.warn(m)).servers) {
133
+ if (def.disabled)
134
+ continue;
135
+ items.push(...(0, common_1.mcpItems)(ctx, { agent: AGENT, def, source: projCfg, scope: "project", mode: "full", evidence: ev("mcp"), notes: ["project .codex/config.toml: only loaded for trusted projects (assumed)"], index: items.length }));
136
+ }
137
+ }
138
+ catch {
139
+ ctx.warn(`${ctx.display(projCfg)}: could not parse as TOML, skipped`);
140
+ }
141
+ }
142
+ // Skills. Documented: Codex scans .agents/skills from the working directory up to the repo root, plus ~/.agents/skills and /etc/codex/skills.
143
+ // The listing sent to the model is name + description + file path (documented); bodies load when a skill is used.
144
+ const disabled = new Set();
145
+ const sc = cfg.skills?.config;
146
+ if (Array.isArray(sc)) {
147
+ for (const e of sc) {
148
+ const x = e;
149
+ if (x && typeof x.path === "string" && x.enabled === false) {
150
+ const p = x.path.replace(/^~(?=[\\/]|$)/, ctx.home);
151
+ disabled.add((0, fs_safe_2.realpathOrSelf)(node_path_1.default.basename(p) === "SKILL.md" ? node_path_1.default.dirname(p) : p));
152
+ }
153
+ }
154
+ }
155
+ const seenSkill = new Set();
156
+ const codexLine = (r) => `- ${r.name}: ${r.desc} (file: ${r.file})`;
157
+ const emit = (dir, scope, tag, jail, confidence, note) => {
158
+ const recs = (0, skills_1.discoverSkills)(ctx, dir, jail ? { jail } : {}).filter((r) => {
159
+ if (seenSkill.has(r.realPath))
160
+ return false;
161
+ seenSkill.add(r.realPath);
162
+ if (disabled.has((0, fs_safe_2.realpathOrSelf)(node_path_1.default.dirname(r.file)))) {
163
+ ctx.exclude(AGENT, r.file, "disabled in config.toml ([[skills.config]] enabled = false)");
164
+ return false;
165
+ }
166
+ return true;
167
+ });
168
+ if (!recs.length)
169
+ return;
170
+ const its = (0, skills_1.skillItems)(ctx, { agent: AGENT, dir, scope, tag, recs, lineFor: codexLine, listingMode: "always", confidence, evidence: ev("skills"), notes: ["name + description + file path are listed up front (documented); the body loads when the skill is used", ...(note ? [note] : [])] });
171
+ items.push(...its);
172
+ const l = its.find((i) => i.kind === "skills-listing");
173
+ const budget = 0.02 * ctx.opts.window;
174
+ if (l?.tokens && l.tokens.est > budget) {
175
+ ctx.addFinding({ id: "F13", severity: "info", agent: AGENT, message: `${tag} skills listing (~${l.tokens.est} tokens) exceeds ≈2% of a ${ctx.opts.window / 1000}k window (~${Math.round(budget)}); Codex shortens descriptions to fit its skills budget (documented), so actual cost is lower but descriptions are lost`, paths: [dir], confidence: "low", fix: "Remove skills you do not use, or disable them with [[skills.config]] enabled = false." });
176
+ }
177
+ };
178
+ for (const dir of [...chain].reverse())
179
+ emit(node_path_1.default.join(dir, ".agents", "skills"), dir === ctx.cwd ? "project" : "ancestor", dir === ctx.cwd ? "project" : "ancestor", ctx.jailFor(dir), "documented");
180
+ emit(node_path_1.default.join(ctx.home, ".agents", "skills"), "user", "user", undefined, "documented");
181
+ emit(ctx.sys("/etc/codex/skills"), "managed", "admin (/etc/codex/skills)", undefined, "documented");
182
+ emit(node_path_1.default.join(home, "skills"), "user", "legacy ~/.codex", undefined, "assumed", "~/.codex/skills is read by Cursor and was the older Codex location; current Codex docs list ~/.agents/skills, so whether Codex still loads it is unverified");
183
+ return { items, unmeasuredBanner: ["built-in system prompt and tools", "Codex bundled system skills (~/.codex/skills/.system, hidden dirs are skipped)", "server-side / org instructions"] };
184
+ }
@@ -0,0 +1,157 @@
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.loadInstruction = loadInstruction;
7
+ exports.listFilesRec = listFilesRec;
8
+ exports.catalogFor = catalogFor;
9
+ exports.mcpItems = mcpItems;
10
+ const node_fs_1 = __importDefault(require("node:fs"));
11
+ const node_path_1 = __importDefault(require("node:path"));
12
+ const context_1 = require("../context");
13
+ const comments_1 = require("../text/comments");
14
+ const imports_1 = require("../text/imports");
15
+ const fs_safe_1 = require("../fs-safe");
16
+ const catalog_1 = require("../mcp/catalog");
17
+ /** Read one instruction-like file and turn it into an Item. Returns null if missing/unreadable/empty. */
18
+ function loadInstruction(ctx, filePath, o) {
19
+ const r = ctx.read(filePath, o.jail ? { jail: o.jail } : {});
20
+ if (!r.ok) {
21
+ // The file exists but could not be measured: surface it as UNMEASURED so totals become lower bounds (never silently dropped).
22
+ if (r.reason === "missing" || r.reason === "not-file")
23
+ return null;
24
+ const why = r.reason === "budget" ? "not read: scan budget reached" : (0, context_1.readFailWhy)(r.reason, "not read");
25
+ return ctx.makeItem({
26
+ agent: o.agent, path: filePath, kind: o.kind ?? "instructions", scope: o.scope, loadMode: o.loadMode, raw: null,
27
+ unmeasuredReason: why, confidence: o.confidence, evidence: o.evidence, notes: [...(o.notes ?? [])],
28
+ cardLabel: o.cardLabel, label: o.label, group: o.group, groupLabel: o.groupLabel,
29
+ });
30
+ }
31
+ let text = r.text;
32
+ const notes = [...(o.notes ?? [])];
33
+ if (o.transform) {
34
+ const t = o.transform(text);
35
+ text = t.text;
36
+ notes.push(...(t.notes ?? []));
37
+ }
38
+ const afterTransform = text;
39
+ if (o.stripComments)
40
+ text = (0, comments_1.stripHtmlComments)(text);
41
+ let detail;
42
+ if (o.imports) {
43
+ const roots = (o.importRoots ?? [ctx.home, ctx.gitRoot ?? ctx.cwd, node_path_1.default.dirname(filePath)]).map(fs_safe_1.realpathOrSelf);
44
+ const res = (0, imports_1.expandImports)(text, filePath, ctx.home, (real) => roots.some((root) => (0, fs_safe_1.isInside)(real, root)), { stripComments: o.stripComments, read: (t) => ctx.read(t, { quiet: true }), maxDepth: o.importMaxDepth, relativeOnly: o.importRelativeOnly });
45
+ text = res.text;
46
+ if (res.imported)
47
+ detail = `(+${res.imported} @import${res.imported > 1 ? "s" : ""})`;
48
+ if (res.skipped.length)
49
+ notes.push(`${res.skipped.length} @import(s) not followed (${[...new Set(res.skipped.map((s) => s.reason))].join(", ")})`);
50
+ }
51
+ if (!text.trim())
52
+ return null;
53
+ const key = `${r.realPath}|${o.stripComments ? 1 : 0}${o.imports ? 1 : 0}|${o.importRoots ? o.importRoots.join(",") : ""}${o.importMaxDepth ?? ""}${o.importRelativeOnly ? "r" : ""}|${o.transform ? text.length : ""}`;
54
+ const raw = ctx.count(key, text);
55
+ if (!o.noAnalyze)
56
+ ctx.analyzeDoc(o.agent, filePath, r.text, afterTransform, raw, o.guidelineLines);
57
+ return ctx.makeItem({
58
+ agent: o.agent,
59
+ path: filePath,
60
+ kind: o.kind ?? "instructions",
61
+ scope: o.scope,
62
+ loadMode: o.loadMode,
63
+ raw,
64
+ chars: text.length,
65
+ lines: (0, context_1.lineCount)(r.text),
66
+ confidence: o.confidence,
67
+ evidence: o.evidence,
68
+ notes,
69
+ cardLabel: o.cardLabel,
70
+ label: o.label,
71
+ detail,
72
+ group: o.group,
73
+ groupLabel: o.groupLabel,
74
+ });
75
+ }
76
+ /** Recursively list files with an extension under dir (bounded, no symlink following). */
77
+ function listFilesRec(dir, ext, limit = 500, maxDepth = 5) {
78
+ const out = [];
79
+ const visit = (d, depth) => {
80
+ let entries;
81
+ try {
82
+ entries = node_fs_1.default.readdirSync(d, { withFileTypes: true }).sort((a, b) => (a.name < b.name ? -1 : 1));
83
+ }
84
+ catch {
85
+ return;
86
+ }
87
+ for (const e of entries) {
88
+ if (out.length >= limit)
89
+ return;
90
+ const p = node_path_1.default.join(d, e.name);
91
+ if (e.isSymbolicLink()) {
92
+ // allow symlinked files (resolved by jail in the reader) but not symlinked dirs
93
+ if (ext.some((x) => e.name.endsWith(x)))
94
+ out.push(p);
95
+ }
96
+ else if (e.isDirectory() && depth < maxDepth)
97
+ visit(p, depth + 1);
98
+ else if (e.isFile() && ext.some((x) => e.name.endsWith(x)))
99
+ out.push(p);
100
+ }
101
+ };
102
+ visit(dir, 0);
103
+ return out;
104
+ }
105
+ let catalogCache;
106
+ function catalogFor(ctx) {
107
+ const key = `${ctx.opts.noBundledCatalog ? "nb" : "b"}|${ctx.opts.catalogPath ?? ""}`;
108
+ if (!catalogCache || catalogCache.key !== key) {
109
+ const c = (0, catalog_1.loadCatalog)(ctx.opts.catalogPath, ctx.opts.noBundledCatalog);
110
+ catalogCache = { key, ...c };
111
+ }
112
+ catalogCache.warnings.forEach((w) => ctx.warn(w));
113
+ return catalogCache.entries;
114
+ }
115
+ /** Turn one configured MCP server into 1–2 items. Unmeasured servers are NEVER zero. */
116
+ function mcpItems(ctx, b) {
117
+ const items = mcpItemsInner(ctx, b);
118
+ for (const it of items)
119
+ it.server = { name: b.def.name, transport: b.def.transport };
120
+ return items;
121
+ }
122
+ function mcpItemsInner(ctx, b) {
123
+ const entry = (0, catalog_1.lookup)(catalogFor(ctx), b.def);
124
+ const base = {
125
+ agent: b.agent,
126
+ path: b.source,
127
+ kind: "mcp-server",
128
+ scope: b.scope,
129
+ evidence: b.evidence,
130
+ cardLabel: "MCP server",
131
+ group: "mcp",
132
+ groupLabel: "MCP servers",
133
+ };
134
+ if (!entry) {
135
+ return [
136
+ ctx.makeItem({
137
+ ...base,
138
+ loadMode: "always",
139
+ raw: null,
140
+ unmeasuredReason: "no local tool-list cache or catalog entry; run is static (no server was started)",
141
+ confidence: "assumed",
142
+ notes: [...b.notes, `transport: ${b.def.transport}`],
143
+ label: `MCP server "${b.def.name}"`,
144
+ }),
145
+ ];
146
+ }
147
+ const tag = `catalog: measured ${entry.measuredAt}${entry.version ? ` v${entry.version}` : ""}, ${entry.tools} tools; your version may differ`;
148
+ if (b.mode === "split") {
149
+ return [
150
+ ctx.makeItem({ ...base, loadMode: "always", raw: entry.nameTokens, confidence: "assumed", notes: [...b.notes, tag, "tool names only (tool search)"], label: `MCP "${b.def.name}" (tool names)` }),
151
+ ctx.makeItem({ ...base, loadMode: "deferred", raw: entry.schemaTokens, confidence: "assumed", notes: [...b.notes, tag, "schemas fetched on demand (tool search)"], label: `MCP "${b.def.name}" (schemas)` }),
152
+ ];
153
+ }
154
+ return [
155
+ ctx.makeItem({ ...base, loadMode: "always", raw: entry.nameTokens + entry.schemaTokens, confidence: "assumed", notes: [...b.notes, tag], label: `MCP "${b.def.name}" (${entry.tools} tools)` }),
156
+ ];
157
+ }
@@ -0,0 +1,275 @@
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.CURSOR_LISTING_NOTE = exports.CURSOR_DESC_CHARS = void 0;
7
+ exports.detectCursor = detectCursor;
8
+ exports.discoverCursor = discoverCursor;
9
+ exports.cursorSkillLine = cursorSkillLine;
10
+ const node_path_1 = __importDefault(require("node:path"));
11
+ const spec_1 = require("../spec");
12
+ const fs_safe_1 = require("../fs-safe");
13
+ const frontmatter_1 = require("../text/frontmatter");
14
+ const common_1 = require("./common");
15
+ const configs_1 = require("../mcp/configs");
16
+ const skills_1 = require("./skills");
17
+ const fmt_1 = require("../util/fmt");
18
+ const AGENT = "cursor";
19
+ const ev = (doc) => (0, spec_1.evidenceFor)(AGENT, doc);
20
+ function detectCursor(ctx) {
21
+ const by = [];
22
+ if ((0, fs_safe_1.isDir)(node_path_1.default.join(ctx.home, ".cursor")))
23
+ by.push("~/.cursor");
24
+ if ((0, fs_safe_1.onPath)("cursor", ctx.opts.env.PATH ?? ctx.opts.env.Path, { platform: ctx.opts.platform, pathExt: ctx.opts.env.PATHEXT }))
25
+ by.push("binary on PATH");
26
+ if (ctx.opts.platform === "darwin" && ((0, fs_safe_1.isDir)(ctx.sys("/Applications/Cursor.app")) || (0, fs_safe_1.isDir)(node_path_1.default.join(ctx.home, "Applications", "Cursor.app"))))
27
+ by.push("Cursor.app");
28
+ if (ctx.opts.platform === "win32") {
29
+ // the per-user installer puts Cursor in %LOCALAPPDATA%\Programs\cursor; the system installer in %ProgramFiles%\cursor
30
+ const local = ctx.opts.env.LOCALAPPDATA || node_path_1.default.join(ctx.home, "AppData", "Local");
31
+ const pf = ctx.opts.env.ProgramFiles || "C:\\Program Files";
32
+ if ((0, fs_safe_1.isFile)(node_path_1.default.join(local, "Programs", "cursor", "Cursor.exe")) || (0, fs_safe_1.isFile)(ctx.sys(node_path_1.default.win32.join(pf, "cursor", "Cursor.exe"))))
33
+ by.push("Cursor.exe");
34
+ }
35
+ if (ctx.opts.pastedUserRules?.trim())
36
+ by.push("--paste-user-rules");
37
+ const repo = [];
38
+ for (const f of [".cursor", ".cursorrules"])
39
+ if ((0, fs_safe_1.isDir)(node_path_1.default.join(ctx.cwd, f)) || (0, fs_safe_1.isFile)(node_path_1.default.join(ctx.cwd, f)))
40
+ repo.push(node_path_1.default.join(ctx.cwd, f));
41
+ if ((0, fs_safe_1.isFile)(node_path_1.default.join(ctx.cwd, "AGENTS.md")))
42
+ repo.push(node_path_1.default.join(ctx.cwd, "AGENTS.md"));
43
+ return { detectedBy: by, repoFiles: repo };
44
+ }
45
+ function truthy(v) {
46
+ return v === true || (typeof v === "string" && v.trim().toLowerCase() === "true");
47
+ }
48
+ /** One .mdc rule file -> items, following the four documented Cursor rule modes. */
49
+ function ruleFile(ctx, f, o, guideline) {
50
+ const items = [];
51
+ const r = ctx.read(f, o.jail ? { jail: o.jail } : {});
52
+ if (!r.ok)
53
+ return items;
54
+ const fm = (0, frontmatter_1.parseFrontmatter)(r.text);
55
+ const always = truthy(fm.data.alwaysApply);
56
+ const globs = (0, frontmatter_1.asList)(fm.data.globs);
57
+ const desc = (0, frontmatter_1.asString)(fm.data.description).trim();
58
+ const common = { agent: AGENT, path: f, kind: "rule", scope: o.scope, evidence: ev("rules"), cardLabel: o.conditional ? "nested rule file" : "rule file" };
59
+ const bodyRaw = ctx.count(`cursor-body|${r.realPath}`, fm.body);
60
+ if (!fm.body.trim() && !desc)
61
+ return items;
62
+ const up = o.conditional ? "conditional" : "always";
63
+ const conf = (c) => o.confidence ?? c;
64
+ const extra = o.note ? [o.note] : [];
65
+ let mode;
66
+ if (always) {
67
+ mode = "always";
68
+ items.push(ctx.makeItem({ ...common, loadMode: up, raw: bodyRaw, chars: fm.body.length, lines: fm.body.split("\n").length, confidence: conf("documented"), label: ctx.display(f), groupLabel: `${o.gl} (alwaysApply)`, notes: ["alwaysApply: true", ...extra], group: `${o.gl}#always` }));
69
+ }
70
+ else if (globs.length) {
71
+ mode = "conditional";
72
+ items.push(ctx.makeItem({ ...common, loadMode: "conditional", raw: bodyRaw, chars: fm.body.length, confidence: conf("documented"), label: ctx.display(f), groupLabel: `${o.gl} (glob-scoped)`, notes: [`globs: ${globs.length} pattern${globs.length > 1 ? "s" : ""}`, ...extra], group: `${o.gl}#glob` }));
73
+ }
74
+ else if (desc) {
75
+ mode = "description";
76
+ const draw = ctx.count(`cursor-desc|${r.realPath}`, desc);
77
+ items.push(ctx.makeItem({ ...common, loadMode: up, kind: "rule-description", raw: draw, chars: desc.length, confidence: "assumed", label: ctx.display(f) + " (description)", groupLabel: `${o.gl} descriptions (agent-requested)`, notes: ["description is visible so the agent can decide to fetch the rule; exact injection unverified", ...extra], group: `${o.gl}#desc` }), ctx.makeItem({ ...common, loadMode: "deferred", kind: "rule-body", raw: bodyRaw, chars: fm.body.length, confidence: "assumed", label: ctx.display(f) + " (body)", groupLabel: `${o.gl} bodies (agent-requested)`, notes: ["fetched only if the agent decides it is relevant", ...extra], group: `${o.gl}#body` }));
78
+ }
79
+ else {
80
+ mode = "manual";
81
+ items.push(ctx.makeItem({ ...common, loadMode: "on-demand", raw: bodyRaw, chars: fm.body.length, confidence: conf("documented"), label: ctx.display(f), groupLabel: `${o.gl} (manual @-mention)`, notes: ["no alwaysApply/globs/description: only via @-mention", ...extra], group: `${o.gl}#manual` }));
82
+ }
83
+ ctx.analyzeDoc(AGENT, f, r.text, fm.body, bodyRaw, mode === "always" && !o.conditional ? guideline : undefined);
84
+ return items;
85
+ }
86
+ function rulesIn(ctx, dir, o, guideline, allowMd) {
87
+ const items = [];
88
+ for (const f of (0, common_1.listFilesRec)(dir, [".mdc", ".md"])) {
89
+ if (f.endsWith(".md") && !allowMd) {
90
+ ctx.exclude(AGENT, f, "plain .md in .cursor/rules is ignored by Cursor (only .mdc is loaded)");
91
+ ctx.addFinding({ id: "F11", severity: "warn", agent: AGENT, message: `${ctx.display(f)} is ignored by Cursor: only .mdc files in .cursor/rules are loaded`, paths: [f], confidence: "high", fix: "Rename it to .mdc (and add frontmatter) if you intend Cursor to use it." });
92
+ continue;
93
+ }
94
+ items.push(...ruleFile(ctx, f, o, guideline));
95
+ }
96
+ return items;
97
+ }
98
+ const SKILLS_DOC = "skills";
99
+ /** Cursor skill locations. Native dirs + compat dirs are documented; ~/.cursor/skills-cursor was OBSERVED on a real Mac, not documented. */
100
+ function skillRoots(ctx) {
101
+ const h = ctx.home;
102
+ const c = ctx.cwd;
103
+ const compat = "also read by Cursor for compatibility (documented)";
104
+ return [
105
+ { dir: node_path_1.default.join(c, ".cursor", "skills"), scope: "project", tag: "project", project: true, confidence: "documented" },
106
+ { dir: node_path_1.default.join(c, ".agents", "skills"), scope: "project", tag: "project (.agents)", project: true, confidence: "documented" },
107
+ { dir: node_path_1.default.join(c, ".claude", "skills"), scope: "project", tag: "project (.claude)", project: true, confidence: "documented", note: compat },
108
+ { dir: node_path_1.default.join(c, ".codex", "skills"), scope: "project", tag: "project (.codex)", project: true, confidence: "documented", note: compat },
109
+ { dir: node_path_1.default.join(h, ".cursor", "skills"), scope: "user", tag: "user", project: false, confidence: "documented" },
110
+ { dir: node_path_1.default.join(h, ".agents", "skills"), scope: "user", tag: "user (.agents)", project: false, confidence: "documented" },
111
+ { dir: node_path_1.default.join(h, ".claude", "skills"), scope: "user", tag: "user (.claude)", project: false, confidence: "documented", note: compat },
112
+ { dir: node_path_1.default.join(h, ".codex", "skills"), scope: "user", tag: "user (.codex)", project: false, confidence: "documented", note: compat },
113
+ { dir: node_path_1.default.join(h, ".cursor", "skills-cursor"), scope: "builtin", tag: "Cursor-managed", project: false, confidence: "assumed", note: "~/.cursor/skills-cursor is not in Cursor's docs; it was observed on a real install and is assumed to hold Cursor's own built-in skills, listed like any other skill (listing format unverified)" },
114
+ ];
115
+ }
116
+ /** Plugin roots under ~/.cursor/plugins/{local,cache}. The on-disk layout is NOT documented: any directory holding a manifest counts. */
117
+ function findCursorPlugins(ctx) {
118
+ const base = node_path_1.default.join(ctx.home, ".cursor", "plugins");
119
+ const found = [];
120
+ const hasManifest = (d) => (0, fs_safe_1.isFile)(node_path_1.default.join(d, ".cursor-plugin", "plugin.json")) || (0, fs_safe_1.isFile)(node_path_1.default.join(d, "plugin.json"));
121
+ const visit = (d, depth, origin) => {
122
+ if (found.length >= 200)
123
+ return;
124
+ if (hasManifest(d)) {
125
+ const mf = (0, fs_safe_1.isFile)(node_path_1.default.join(d, ".cursor-plugin", "plugin.json")) ? node_path_1.default.join(d, ".cursor-plugin", "plugin.json") : node_path_1.default.join(d, "plugin.json");
126
+ const j = ctx.readJson(mf);
127
+ const name = j && typeof j.name === "string" && j.name.trim() ? j.name.trim().slice(0, 80) : node_path_1.default.basename(d);
128
+ found.push({ name, root: d, origin });
129
+ return;
130
+ }
131
+ if (depth <= 0)
132
+ return;
133
+ for (const e of (0, fs_safe_1.listDir)(d)) {
134
+ if (e.name.startsWith("."))
135
+ continue;
136
+ const p = node_path_1.default.join(d, e.name);
137
+ if (e.isDirectory() || (e.isSymbolicLink() && (0, fs_safe_1.isDir)(p)))
138
+ visit(p, depth - 1, origin);
139
+ }
140
+ };
141
+ visit(node_path_1.default.join(base, "local"), 2, "local");
142
+ visit(node_path_1.default.join(base, "cache"), 5, "cache");
143
+ // same plugin name in several cached versions: keep the last path in sorted order (usually the newest version)
144
+ const byName = new Map();
145
+ for (const p of found.sort((a, b) => (0, fmt_1.cmp)(a.root, b.root)))
146
+ byName.set(p.name, p);
147
+ return [...byName.values()];
148
+ }
149
+ function discoverCursor(ctx) {
150
+ const caps = (0, spec_1.loadSpec)(AGENT).caps;
151
+ const items = [];
152
+ const jail = ctx.jailFor(ctx.cwd);
153
+ const banner = ["User Rules and Team Rules (stored in Cursor, not on disk; paste them with --paste-user-rules to count them)", "built-in system prompt and tools"];
154
+ // Rules: .mdc only; plain .md is ignored by Cursor
155
+ items.push(...rulesIn(ctx, node_path_1.default.join(ctx.cwd, ".cursor", "rules"), { scope: "project", jail, gl: ".cursor/rules" }, caps.ruleGuidelineLines, false));
156
+ // Legacy .cursorrules (assumed always)
157
+ const legacy = node_path_1.default.join(ctx.cwd, ".cursorrules");
158
+ const lit = (0, common_1.loadInstruction)(ctx, legacy, { agent: AGENT, scope: "project", loadMode: "always", label: ".cursorrules (legacy)", cardLabel: ".cursorrules", jail, stripComments: false, imports: false, confidence: "assumed", evidence: ev("rules"), notes: ["legacy format; still read as an always-on rule (unverified)"] });
159
+ if (lit)
160
+ items.push(lit);
161
+ // AGENTS.md: root always, nested conditional
162
+ const rootAgents = node_path_1.default.join(ctx.cwd, "AGENTS.md");
163
+ const rit = (0, common_1.loadInstruction)(ctx, rootAgents, { agent: AGENT, scope: "project", loadMode: "always", label: "AGENTS.md", cardLabel: "AGENTS.md", jail, stripComments: false, imports: false, confidence: "documented", evidence: ev("rules") });
164
+ if (rit)
165
+ items.push(rit);
166
+ const nestedDirs = [];
167
+ for (const hit of ctx.walk().hits) {
168
+ nestedDirs.push(hit);
169
+ if (!hit.names.includes("AGENTS.md"))
170
+ continue;
171
+ const nf = node_path_1.default.join(hit.dir, "AGENTS.md");
172
+ const it = (0, common_1.loadInstruction)(ctx, nf, { agent: AGENT, scope: "nested", loadMode: "conditional", label: ctx.display(nf), cardLabel: "nested AGENTS.md", jail: ctx.jailFor(hit.dir), stripComments: false, imports: false, confidence: "documented", evidence: ev("rules"), notes: ["nested AGENTS.md applies when working in that directory"] });
173
+ if (it)
174
+ items.push(it);
175
+ }
176
+ // Nested .cursor/rules in subdirectories. Sources conflict on whether Cursor loads these (docs describe project-root rules only;
177
+ // forum reports say subdirectory rules apply), so they are labelled "assumed" and counted as conditional.
178
+ for (const hit of nestedDirs) {
179
+ if (!hit.names.includes(".cursor/rules/"))
180
+ continue;
181
+ items.push(...rulesIn(ctx, node_path_1.default.join(hit.dir, ".cursor", "rules"), { scope: "nested", jail: ctx.jailFor(hit.dir), gl: "nested .cursor/rules", conditional: true, confidence: "assumed", note: "nested .cursor/rules: whether Cursor loads subdirectory rules is not stated in its docs (assumed conditional)" }, caps.ruleGuidelineLines, false));
182
+ }
183
+ // Skills. Listing (name+description) is assumed to load up front, bodies on demand; listing format/cost is UNVERIFIED for Cursor.
184
+ const seenSkill = new Set();
185
+ const cursorSkillNotes = (extra) => [exports.CURSOR_LISTING_NOTE, ...(extra ? [extra] : [])];
186
+ const emitSkills = (recs, dir, scope, tag, confidence, note, forceConditional = false) => {
187
+ const fresh = recs.filter((r) => !seenSkill.has(r.realPath));
188
+ for (const r of fresh)
189
+ seenSkill.add(r.realPath);
190
+ const unscoped = fresh.filter((r) => r.scopedTo.length === 0);
191
+ const scoped = fresh.filter((r) => r.scopedTo.length > 0);
192
+ const spec = {
193
+ agent: AGENT, dir, scope, confidence, evidence: ev(SKILLS_DOC),
194
+ lineFor: (r) => (r.disableModelInvocation ? null : cursorSkillLine(dir, r, "short")),
195
+ lowLineFor: (r) => cursorSkillLine(dir, r, "none"),
196
+ highLineFor: (r) => cursorSkillLine(dir, r, "full"),
197
+ };
198
+ if (unscoped.length)
199
+ items.push(...(0, skills_1.skillItems)(ctx, { ...spec, tag, recs: unscoped, listingMode: forceConditional ? "conditional" : "always", notes: cursorSkillNotes(note) }));
200
+ if (scoped.length)
201
+ items.push(...(0, skills_1.skillItems)(ctx, { ...spec, tag: `${tag}, path-scoped`, recs: scoped, listingMode: "conditional", notes: cursorSkillNotes("skills with paths/globs frontmatter are surfaced only when matching files are in play (documented)") }));
202
+ };
203
+ for (const root of skillRoots(ctx)) {
204
+ const recs = (0, skills_1.discoverSkills)(ctx, root.dir, { recursive: true, ...(root.project ? { jail } : {}) });
205
+ emitSkills(recs, root.dir, root.scope, root.tag, root.confidence, root.note);
206
+ }
207
+ for (const hit of nestedDirs) {
208
+ for (const rel of [".cursor/skills/", ".agents/skills/"]) {
209
+ if (!hit.names.includes(rel))
210
+ continue;
211
+ const dir = node_path_1.default.join(hit.dir, ...rel.split("/").filter(Boolean));
212
+ emitSkills((0, skills_1.discoverSkills)(ctx, dir, { recursive: true, jail: ctx.jailFor(hit.dir) }), dir, "nested", "nested", "documented", "nested skill directories are scoped to their subtree (documented)", true);
213
+ }
214
+ }
215
+ // Plugins (skills / rules / mcp.json). Install state is not on a documented path: anything found in plugins/local or plugins/cache counts.
216
+ const plugins = findCursorPlugins(ctx);
217
+ const pluginNote = "plugin found under ~/.cursor/plugins; Cursor's on-disk plugin layout and install state are undocumented, so it may not be enabled (assumed)";
218
+ let pluginOtherComponents = 0;
219
+ const pluginSkillRecs = [];
220
+ for (const pl of plugins) {
221
+ const recs = (0, skills_1.discoverSkills)(ctx, node_path_1.default.join(pl.root, "skills"), { recursive: true });
222
+ for (const r of recs)
223
+ r.source = pl.name;
224
+ pluginSkillRecs.push(...recs);
225
+ items.push(...rulesIn(ctx, node_path_1.default.join(pl.root, "rules"), { scope: "plugin", gl: `plugin ${pl.name} rules`, confidence: "assumed", note: pluginNote }, caps.ruleGuidelineLines, true));
226
+ const mcpFile = node_path_1.default.join(pl.root, "mcp.json");
227
+ const j = ctx.readJson(mcpFile);
228
+ if (j) {
229
+ for (const def of (0, configs_1.parseMcpMap)(j.mcpServers ?? j, (m) => ctx.warn(m))) {
230
+ if (def.disabled)
231
+ continue;
232
+ items.push(...(0, common_1.mcpItems)(ctx, { agent: AGENT, def, source: mcpFile, scope: "plugin", mode: "split", evidence: ev("mcpLazy"), notes: [pluginNote, "Cursor keeps only tool names in static context (documented)"], index: items.length }));
233
+ }
234
+ }
235
+ if (["agents", "commands", "hooks"].some((d) => (0, fs_safe_1.isDir)(node_path_1.default.join(pl.root, d))))
236
+ pluginOtherComponents++;
237
+ }
238
+ if (pluginSkillRecs.length)
239
+ emitSkills(pluginSkillRecs, node_path_1.default.join(ctx.home, ".cursor", "plugins"), "plugin", `from ${plugins.length} plugin${plugins.length > 1 ? "s" : ""}`, "assumed", pluginNote);
240
+ if (plugins.length)
241
+ banner.push(`plugin install state (${plugins.length} plugin${plugins.length > 1 ? "s" : ""} found under ~/.cursor/plugins${(0, fs_safe_1.isDir)(node_path_1.default.join(ctx.home, ".cursor", "plugins", "marketplaces")) ? "; marketplace clones ignored" : ""})`);
242
+ if (pluginOtherComponents)
243
+ banner.push(`plugin agents/commands/hooks (${pluginOtherComponents} plugin${pluginOtherComponents > 1 ? "s" : ""})`);
244
+ // MCP: ~/.cursor/mcp.json and .cursor/mcp.json. Cursor keeps only tool NAMES in static context and fetches descriptions on demand (documented, Jan 2026).
245
+ for (const [file, scope] of [[node_path_1.default.join(ctx.home, ".cursor", "mcp.json"), "user"], [node_path_1.default.join(ctx.cwd, ".cursor", "mcp.json"), "project"]]) {
246
+ const j = ctx.readJson(file);
247
+ if (!j)
248
+ continue;
249
+ for (const def of (0, configs_1.parseMcpMap)(j.mcpServers, (m) => ctx.warn(m))) {
250
+ if (def.disabled)
251
+ continue;
252
+ items.push(...(0, common_1.mcpItems)(ctx, { agent: AGENT, def, source: file, scope, mode: "split", evidence: ev("mcpLazy"), notes: ["Cursor keeps only tool names in static context; descriptions are fetched on demand (documented in Cursor's 'Dynamic context discovery' post). Name count is unmeasured without a catalog entry."], index: items.length }));
253
+ }
254
+ }
255
+ // Pasted User Rules (they live inside the app, not on disk)
256
+ if (ctx.opts.pastedUserRules !== undefined && ctx.opts.pastedUserRules.trim()) {
257
+ const text = ctx.opts.pastedUserRules;
258
+ items.push(ctx.makeItem({ agent: AGENT, path: node_path_1.default.join(ctx.home, ".cursor", "(pasted user rules)"), kind: "user-rules", scope: "user", loadMode: "always", raw: ctx.count(`pasted|${text.length}`, text), chars: text.length, lines: text.split("\n").length, confidence: "assumed", evidence: ev("rules"), cardLabel: "User Rules (pasted)", label: "User Rules (pasted via --paste-user-rules)", notes: ["text you pasted; assumed to be sent on every request (Cursor docs describe User Rules as always applied; exact injection unverified)"] }));
259
+ const i = banner.findIndex((b) => b.startsWith("User Rules"));
260
+ if (i >= 0)
261
+ banner[i] = "Team Rules (stored in Cursor, not on disk)";
262
+ }
263
+ return { items, unmeasuredBanner: banner };
264
+ }
265
+ /** Observed Cursor listing (forum.cursor.com/t/163761, staff-confirmed 2026-06-22): description cut to about 80 characters. */
266
+ exports.CURSOR_DESC_CHARS = 80;
267
+ const CURSOR_PATH_STANDIN = "/Users/you/.cursor/skills";
268
+ exports.CURSOR_LISTING_NOTE = "Cursor lists each skill as <agent_skill fullPath=...> with its description cut to ~80 characters, and drops descriptions when the catalog is large (observed, confirmed by Cursor staff 2026-06; budget undocumented). Estimate = 80-character form; low = path only; high = full description.";
269
+ /** One `<agent_skill>` entry. The absolute path is approximated by a fixed stand-in + the path below the skills root (deterministic, never output). */
270
+ function cursorSkillLine(root, r, desc) {
271
+ const rel = node_path_1.default.relative(root, r.file).split(node_path_1.default.sep).join("/");
272
+ const d = r.desc.trim();
273
+ const text = desc === "none" ? "" : desc === "full" || d.length <= exports.CURSOR_DESC_CHARS ? d : `${d.slice(0, exports.CURSOR_DESC_CHARS)}...`;
274
+ return `<agent_skill fullPath="${CURSOR_PATH_STANDIN}/${rel}">${text}</agent_skill>`;
275
+ }