@praneeth_54/agentdoctor 0.1.0-beta

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 (112) hide show
  1. package/CHANGELOG.md +58 -0
  2. package/LICENSE +21 -0
  3. package/README.md +348 -0
  4. package/dist/agents/claude/adapter.d.ts +1 -0
  5. package/dist/agents/claude/adapter.js +1 -0
  6. package/dist/agents/claude/detector.d.ts +16 -0
  7. package/dist/agents/claude/detector.js +225 -0
  8. package/dist/agents/codex/adapter.d.ts +1 -0
  9. package/dist/agents/codex/adapter.js +1 -0
  10. package/dist/agents/codex/detector.d.ts +17 -0
  11. package/dist/agents/codex/detector.js +151 -0
  12. package/dist/agents/cursor/adapter.d.ts +1 -0
  13. package/dist/agents/cursor/adapter.js +1 -0
  14. package/dist/agents/cursor/detector.d.ts +17 -0
  15. package/dist/agents/cursor/detector.js +198 -0
  16. package/dist/agents/detect-agents.d.ts +17 -0
  17. package/dist/agents/detect-agents.js +23 -0
  18. package/dist/agents/inspect.d.ts +36 -0
  19. package/dist/agents/inspect.js +165 -0
  20. package/dist/agents/registry.d.ts +7 -0
  21. package/dist/agents/registry.js +11 -0
  22. package/dist/agents/types.d.ts +52 -0
  23. package/dist/agents/types.js +15 -0
  24. package/dist/cli/commands/doctor.d.ts +5 -0
  25. package/dist/cli/commands/doctor.js +22 -0
  26. package/dist/cli/commands/explain.d.ts +5 -0
  27. package/dist/cli/commands/explain.js +51 -0
  28. package/dist/cli/commands/fix.d.ts +8 -0
  29. package/dist/cli/commands/fix.js +19 -0
  30. package/dist/cli/commands/scan.d.ts +10 -0
  31. package/dist/cli/commands/scan.js +59 -0
  32. package/dist/cli/index.d.ts +2 -0
  33. package/dist/cli/index.js +3 -0
  34. package/dist/cli/program.d.ts +3 -0
  35. package/dist/cli/program.js +124 -0
  36. package/dist/constants.d.ts +5 -0
  37. package/dist/constants.js +38 -0
  38. package/dist/core/mcp/parse.d.ts +7 -0
  39. package/dist/core/mcp/parse.js +303 -0
  40. package/dist/core/mcp/types.d.ts +33 -0
  41. package/dist/core/mcp/types.js +1 -0
  42. package/dist/core/rules/build-context.d.ts +10 -0
  43. package/dist/core/rules/build-context.js +31 -0
  44. package/dist/core/rules/context/generated-directory.d.ts +2 -0
  45. package/dist/core/rules/context/generated-directory.js +89 -0
  46. package/dist/core/rules/context/large-instruction-file.d.ts +2 -0
  47. package/dist/core/rules/context/large-instruction-file.js +56 -0
  48. package/dist/core/rules/context/large-log-file.d.ts +2 -0
  49. package/dist/core/rules/context/large-log-file.js +45 -0
  50. package/dist/core/rules/dedupe.d.ts +13 -0
  51. package/dist/core/rules/dedupe.js +53 -0
  52. package/dist/core/rules/ignore.d.ts +23 -0
  53. package/dist/core/rules/ignore.js +97 -0
  54. package/dist/core/rules/instructions/duplicate-content.d.ts +2 -0
  55. package/dist/core/rules/instructions/duplicate-content.js +99 -0
  56. package/dist/core/rules/instructions/empty-instructions.d.ts +2 -0
  57. package/dist/core/rules/instructions/empty-instructions.js +53 -0
  58. package/dist/core/rules/instructions/missing-path-reference.d.ts +2 -0
  59. package/dist/core/rules/instructions/missing-path-reference.js +131 -0
  60. package/dist/core/rules/mcp/mcp-rules.d.ts +3 -0
  61. package/dist/core/rules/mcp/mcp-rules.js +83 -0
  62. package/dist/core/rules/registry.d.ts +6 -0
  63. package/dist/core/rules/registry.js +31 -0
  64. package/dist/core/rules/run-rules.d.ts +16 -0
  65. package/dist/core/rules/run-rules.js +36 -0
  66. package/dist/core/rules/security/claude-bypass-permissions.d.ts +7 -0
  67. package/dist/core/rules/security/claude-bypass-permissions.js +52 -0
  68. package/dist/core/rules/security/env-file-exposure.d.ts +2 -0
  69. package/dist/core/rules/security/env-file-exposure.js +87 -0
  70. package/dist/core/rules/security/mcp-broad-filesystem.d.ts +2 -0
  71. package/dist/core/rules/security/mcp-broad-filesystem.js +42 -0
  72. package/dist/core/rules/security/private-key-file.d.ts +2 -0
  73. package/dist/core/rules/security/private-key-file.js +60 -0
  74. package/dist/core/rules/text-cache.d.ts +19 -0
  75. package/dist/core/rules/text-cache.js +112 -0
  76. package/dist/core/rules/thresholds.d.ts +13 -0
  77. package/dist/core/rules/thresholds.js +13 -0
  78. package/dist/core/rules/types.d.ts +57 -0
  79. package/dist/core/rules/types.js +23 -0
  80. package/dist/core/scanner/scan.d.ts +7 -0
  81. package/dist/core/scanner/scan.js +70 -0
  82. package/dist/core/scoring/placeholder.d.ts +7 -0
  83. package/dist/core/scoring/placeholder.js +29 -0
  84. package/dist/detectors/framework.d.ts +17 -0
  85. package/dist/detectors/framework.js +148 -0
  86. package/dist/detectors/language.d.ts +14 -0
  87. package/dist/detectors/language.js +122 -0
  88. package/dist/detectors/monorepo.d.ts +13 -0
  89. package/dist/detectors/monorepo.js +44 -0
  90. package/dist/detectors/package-manager.d.ts +14 -0
  91. package/dist/detectors/package-manager.js +78 -0
  92. package/dist/detectors/project.d.ts +10 -0
  93. package/dist/detectors/project.js +60 -0
  94. package/dist/discovery/files.d.ts +15 -0
  95. package/dist/discovery/files.js +118 -0
  96. package/dist/index.d.ts +8 -0
  97. package/dist/index.js +7 -0
  98. package/dist/reporters/json/report.d.ts +5 -0
  99. package/dist/reporters/json/report.js +84 -0
  100. package/dist/reporters/terminal/report.d.ts +8 -0
  101. package/dist/reporters/terminal/report.js +147 -0
  102. package/dist/security/redaction.d.ts +9 -0
  103. package/dist/security/redaction.js +26 -0
  104. package/dist/types/index.d.ts +137 -0
  105. package/dist/types/index.js +6 -0
  106. package/dist/utils/colors.d.ts +14 -0
  107. package/dist/utils/colors.js +26 -0
  108. package/dist/utils/fs.d.ts +17 -0
  109. package/dist/utils/fs.js +63 -0
  110. package/dist/utils/path.d.ts +17 -0
  111. package/dist/utils/path.js +37 -0
  112. package/package.json +80 -0
@@ -0,0 +1,17 @@
1
+ import type { AgentAdapter, AgentDetectionContext, AgentDetectionResult } from "../types.js";
2
+ /**
3
+ * Codex project configuration detection.
4
+ *
5
+ * Official sources (Codex AGENTS.md / project config docs):
6
+ * - Project files: `AGENTS.md`, `AGENTS.override.md` (override wins at same directory)
7
+ * - Nested files from repo root down toward the working directory
8
+ * - Optional project-local Codex home via `CODEX_HOME=$(pwd)/.codex`
9
+ * (we detect repo `.codex/AGENTS.md` and `.codex/config.toml` only)
10
+ *
11
+ * Fallback filenames (`project_doc_fallback_filenames`) are user-global config —
12
+ * we do not invent fallbacks; only official default names are detected.
13
+ *
14
+ * We do NOT read `~/.codex` for normal repository scans.
15
+ */
16
+ export declare function detectCodex(context: AgentDetectionContext): Promise<AgentDetectionResult>;
17
+ export declare const codexAdapter: AgentAdapter;
@@ -0,0 +1,151 @@
1
+ import { AGENT_DISPLAY_NAMES } from "../../constants.js";
2
+ import { deriveStatus, inspectRepoFile, pathExistsInsideRoot, toAgentConfigFile, } from "../inspect.js";
3
+ import { basenameOf, isRootLevel } from "../types.js";
4
+ /**
5
+ * Codex project configuration detection.
6
+ *
7
+ * Official sources (Codex AGENTS.md / project config docs):
8
+ * - Project files: `AGENTS.md`, `AGENTS.override.md` (override wins at same directory)
9
+ * - Nested files from repo root down toward the working directory
10
+ * - Optional project-local Codex home via `CODEX_HOME=$(pwd)/.codex`
11
+ * (we detect repo `.codex/AGENTS.md` and `.codex/config.toml` only)
12
+ *
13
+ * Fallback filenames (`project_doc_fallback_filenames`) are user-global config —
14
+ * we do not invent fallbacks; only official default names are detected.
15
+ *
16
+ * We do NOT read `~/.codex` for normal repository scans.
17
+ */
18
+ export async function detectCodex(context) {
19
+ const { root, discovery, maxFileSizeBytes } = context;
20
+ const configFiles = [];
21
+ const diagnostics = [];
22
+ const seen = new Set();
23
+ const metadata = {
24
+ agentsMdCount: 0,
25
+ overrideCount: 0,
26
+ hasProjectCodexDir: false,
27
+ };
28
+ const hasProjectCodexDir = await pathExistsInsideRoot(root, ".codex");
29
+ metadata.hasProjectCodexDir = hasProjectCodexDir;
30
+ async function addAgentsFile(relativePath, kind) {
31
+ if (seen.has(relativePath)) {
32
+ return;
33
+ }
34
+ seen.add(relativePath);
35
+ const inspected = await inspectRepoFile(root, relativePath, maxFileSizeBytes);
36
+ configFiles.push(toAgentConfigFile(inspected, kind, {
37
+ legacy: false,
38
+ scope: isRootLevel(relativePath) ? "root" : "nested",
39
+ }));
40
+ if (kind === "agents-md") {
41
+ metadata.agentsMdCount = Number(metadata.agentsMdCount) + 1;
42
+ }
43
+ else {
44
+ metadata.overrideCount = Number(metadata.overrideCount) + 1;
45
+ }
46
+ if (!inspected.exists) {
47
+ return;
48
+ }
49
+ if (!inspected.readable && inspected.error) {
50
+ diagnostics.push({
51
+ code: "codex/unreadable-file",
52
+ severity: "warning",
53
+ message: `Could not read Codex instruction file: ${inspected.error}`,
54
+ file: relativePath,
55
+ });
56
+ }
57
+ else if (inspected.empty) {
58
+ diagnostics.push({
59
+ code: "codex/empty-agents",
60
+ severity: "info",
61
+ // Official docs: Codex skips empty files
62
+ message: "Codex instruction file is empty (Codex skips empty files)",
63
+ file: relativePath,
64
+ });
65
+ }
66
+ }
67
+ for (const file of discovery.files) {
68
+ const relative = file.relativePath;
69
+ const base = basenameOf(relative);
70
+ if (base === "AGENTS.override.md") {
71
+ await addAgentsFile(relative, "agents-override-md");
72
+ continue;
73
+ }
74
+ if (base === "AGENTS.md") {
75
+ await addAgentsFile(relative, "agents-md");
76
+ }
77
+ }
78
+ // Explicit targeted checks for empty root files that discovery may still include
79
+ for (const relative of ["AGENTS.md", "AGENTS.override.md"]) {
80
+ if (!seen.has(relative) && (await pathExistsInsideRoot(root, relative))) {
81
+ await addAgentsFile(relative, relative === "AGENTS.override.md" ? "agents-override-md" : "agents-md");
82
+ }
83
+ }
84
+ // Optional project-local Codex home (CODEX_HOME=$(pwd)/.codex)
85
+ if (hasProjectCodexDir) {
86
+ for (const relative of [".codex/AGENTS.md", ".codex/AGENTS.override.md"]) {
87
+ if (await pathExistsInsideRoot(root, relative)) {
88
+ await addAgentsFile(relative, relative.endsWith("override.md") ? "agents-override-md" : "agents-md");
89
+ }
90
+ }
91
+ if (await pathExistsInsideRoot(root, ".codex/config.toml")) {
92
+ const inspected = await inspectRepoFile(root, ".codex/config.toml", maxFileSizeBytes);
93
+ configFiles.push(toAgentConfigFile(inspected, "codex-config", {
94
+ legacy: false,
95
+ scope: "root",
96
+ }));
97
+ // Existence only — TOML values are not parsed here.
98
+ if (inspected.empty) {
99
+ diagnostics.push({
100
+ code: "codex/empty-config",
101
+ severity: "info",
102
+ message: ".codex/config.toml is empty",
103
+ file: ".codex/config.toml",
104
+ });
105
+ }
106
+ }
107
+ }
108
+ const usableInstruction = configFiles.some((f) => f.readable && !f.empty && (f.kind === "agents-md" || f.kind === "agents-override-md"));
109
+ const detected = Number(metadata.agentsMdCount) > 0 || Number(metadata.overrideCount) > 0 || hasProjectCodexDir;
110
+ const configured = usableInstruction;
111
+ const hasErrors = diagnostics.some((d) => d.severity === "error");
112
+ const status = deriveStatus({ detected, configured, hasErrors });
113
+ const parts = [];
114
+ if (Number(metadata.agentsMdCount) > 0) {
115
+ parts.push(Number(metadata.agentsMdCount) === 1
116
+ ? "AGENTS.md"
117
+ : `${metadata.agentsMdCount} AGENTS.md files`);
118
+ }
119
+ if (Number(metadata.overrideCount) > 0) {
120
+ parts.push(Number(metadata.overrideCount) === 1
121
+ ? "AGENTS.override.md"
122
+ : `${metadata.overrideCount} overrides`);
123
+ }
124
+ let summary;
125
+ if (!detected) {
126
+ summary = "not configured";
127
+ }
128
+ else if (configured) {
129
+ summary = parts.length > 0 ? parts.join(", ") : "configured";
130
+ }
131
+ else {
132
+ summary = "detected but not configured";
133
+ }
134
+ return {
135
+ id: "codex",
136
+ displayName: AGENT_DISPLAY_NAMES.codex,
137
+ detected,
138
+ configured,
139
+ status,
140
+ summary,
141
+ configFiles,
142
+ configPaths: configFiles.map((f) => f.relativePath),
143
+ diagnostics,
144
+ metadata,
145
+ };
146
+ }
147
+ export const codexAdapter = {
148
+ id: "codex",
149
+ displayName: AGENT_DISPLAY_NAMES.codex,
150
+ detect: detectCodex,
151
+ };
@@ -0,0 +1 @@
1
+ export { cursorAdapter, detectCursor } from "./detector.js";
@@ -0,0 +1 @@
1
+ export { cursorAdapter, detectCursor } from "./detector.js";
@@ -0,0 +1,17 @@
1
+ import type { AgentAdapter, AgentDetectionContext, AgentDetectionResult } from "../types.js";
2
+ /**
3
+ * Cursor project configuration detection.
4
+ *
5
+ * Official sources (Cursor docs — Rules):
6
+ * - Project rules: `.cursor/rules/*.mdc` (Markdown + YAML frontmatter)
7
+ * - Plain `.md` under `.cursor/rules` is ignored by the rules system
8
+ * - `AGENTS.md` is an official simple alternative (root + nested)
9
+ *
10
+ * Legacy:
11
+ * - Root `.cursorrules` is still encountered in the wild and referenced by
12
+ * other tools' migration paths; treated as legacy, not primary.
13
+ *
14
+ * We do not scan user-global Cursor settings — repository only.
15
+ */
16
+ export declare function detectCursor(context: AgentDetectionContext): Promise<AgentDetectionResult>;
17
+ export declare const cursorAdapter: AgentAdapter;
@@ -0,0 +1,198 @@
1
+ import { AGENT_DISPLAY_NAMES } from "../../constants.js";
2
+ import { deriveStatus, inspectRepoFile, pathExistsInsideRoot, toAgentConfigFile, } from "../inspect.js";
3
+ import { basenameOf, dirnameOf, isRootLevel } from "../types.js";
4
+ /**
5
+ * Cursor project configuration detection.
6
+ *
7
+ * Official sources (Cursor docs — Rules):
8
+ * - Project rules: `.cursor/rules/*.mdc` (Markdown + YAML frontmatter)
9
+ * - Plain `.md` under `.cursor/rules` is ignored by the rules system
10
+ * - `AGENTS.md` is an official simple alternative (root + nested)
11
+ *
12
+ * Legacy:
13
+ * - Root `.cursorrules` is still encountered in the wild and referenced by
14
+ * other tools' migration paths; treated as legacy, not primary.
15
+ *
16
+ * We do not scan user-global Cursor settings — repository only.
17
+ */
18
+ export async function detectCursor(context) {
19
+ const { root, discovery, maxFileSizeBytes } = context;
20
+ const configFiles = [];
21
+ const diagnostics = [];
22
+ const metadata = {
23
+ mdcRuleCount: 0,
24
+ ignoredMdRuleCount: 0,
25
+ agentsMdCount: 0,
26
+ hasLegacyCursorrules: false,
27
+ hasCursorDirectory: false,
28
+ };
29
+ const hasCursorDir = await pathExistsInsideRoot(root, ".cursor");
30
+ metadata.hasCursorDirectory = hasCursorDir;
31
+ for (const file of discovery.files) {
32
+ const relative = file.relativePath;
33
+ const base = basenameOf(relative);
34
+ // Current project rules: any .mdc under a .cursor/rules/ segment
35
+ if (relative.includes(".cursor/rules/") && base.endsWith(".mdc")) {
36
+ const inspected = await inspectRepoFile(root, relative, maxFileSizeBytes);
37
+ const scope = relative.startsWith(".cursor/rules/") ? "root" : "nested";
38
+ const entry = toAgentConfigFile(inspected, "cursor-rule-mdc", {
39
+ legacy: false,
40
+ scope,
41
+ });
42
+ configFiles.push(entry);
43
+ metadata.mdcRuleCount = Number(metadata.mdcRuleCount) + 1;
44
+ if (!inspected.readable && inspected.error) {
45
+ diagnostics.push({
46
+ code: "cursor/unreadable-rule",
47
+ severity: "warning",
48
+ message: `Could not read Cursor rule: ${inspected.error}`,
49
+ file: relative,
50
+ });
51
+ }
52
+ else if (inspected.empty) {
53
+ diagnostics.push({
54
+ code: "cursor/empty-rule",
55
+ severity: "info",
56
+ message: "Cursor rule file is empty",
57
+ file: relative,
58
+ });
59
+ }
60
+ continue;
61
+ }
62
+ // Plain .md under .cursor/rules is ignored by Cursor's rules system
63
+ if (relative.includes(".cursor/rules/") && base.endsWith(".md")) {
64
+ const inspected = await inspectRepoFile(root, relative, maxFileSizeBytes);
65
+ configFiles.push(toAgentConfigFile(inspected, "cursor-rule-ignored-md", {
66
+ legacy: false,
67
+ scope: relative.startsWith(".cursor/rules/") ? "root" : "nested",
68
+ }));
69
+ metadata.ignoredMdRuleCount = Number(metadata.ignoredMdRuleCount) + 1;
70
+ diagnostics.push({
71
+ code: "cursor/ignored-md-rule",
72
+ severity: "warning",
73
+ message: "Plain .md files in .cursor/rules are ignored by Cursor; use .mdc with frontmatter or AGENTS.md",
74
+ file: relative,
75
+ });
76
+ continue;
77
+ }
78
+ // AGENTS.md — official Cursor alternative (root or nested)
79
+ if (base === "AGENTS.md") {
80
+ const inspected = await inspectRepoFile(root, relative, maxFileSizeBytes);
81
+ configFiles.push(toAgentConfigFile(inspected, "agents-md", {
82
+ legacy: false,
83
+ scope: isRootLevel(relative) ? "root" : "nested",
84
+ }));
85
+ metadata.agentsMdCount = Number(metadata.agentsMdCount) + 1;
86
+ if (inspected.empty) {
87
+ diagnostics.push({
88
+ code: "cursor/empty-agents-md",
89
+ severity: "info",
90
+ message: "AGENTS.md is empty",
91
+ file: relative,
92
+ });
93
+ }
94
+ }
95
+ }
96
+ // Legacy root .cursorrules (may not appear if empty — check explicitly)
97
+ if (await pathExistsInsideRoot(root, ".cursorrules")) {
98
+ const inspected = await inspectRepoFile(root, ".cursorrules", maxFileSizeBytes);
99
+ configFiles.push(toAgentConfigFile(inspected, "cursor-legacy-cursorrules", {
100
+ legacy: true,
101
+ scope: "legacy",
102
+ }));
103
+ metadata.hasLegacyCursorrules = true;
104
+ diagnostics.push({
105
+ code: "cursor/legacy-cursorrules",
106
+ severity: "warning",
107
+ message: "Legacy .cursorrules detected; prefer .cursor/rules/*.mdc or AGENTS.md per current Cursor docs",
108
+ file: ".cursorrules",
109
+ });
110
+ if (inspected.empty) {
111
+ diagnostics.push({
112
+ code: "cursor/empty-cursorrules",
113
+ severity: "info",
114
+ message: "Legacy .cursorrules is empty",
115
+ file: ".cursorrules",
116
+ });
117
+ }
118
+ }
119
+ else if (discovery.files.some((f) => f.relativePath === ".cursorrules")) {
120
+ // already covered via existence
121
+ }
122
+ // Detect empty .cursor tree with no usable rules
123
+ if (hasCursorDir && Number(metadata.mdcRuleCount) === 0) {
124
+ const hasRulesDir = await pathExistsInsideRoot(root, ".cursor/rules");
125
+ if (hasRulesDir) {
126
+ diagnostics.push({
127
+ code: "cursor/empty-rules-dir",
128
+ severity: "info",
129
+ message: ".cursor/rules exists but no .mdc project rules were found",
130
+ file: ".cursor/rules",
131
+ });
132
+ }
133
+ else {
134
+ diagnostics.push({
135
+ code: "cursor/no-rules",
136
+ severity: "info",
137
+ message: ".cursor directory exists without a rules/ project configuration",
138
+ file: ".cursor",
139
+ });
140
+ }
141
+ }
142
+ const usableInstruction = configFiles.some((f) => f.readable &&
143
+ !f.empty &&
144
+ (f.kind === "cursor-rule-mdc" ||
145
+ f.kind === "cursor-legacy-cursorrules" ||
146
+ f.kind === "agents-md"));
147
+ const detected = hasCursorDir ||
148
+ Boolean(metadata.hasLegacyCursorrules) ||
149
+ Number(metadata.mdcRuleCount) > 0 ||
150
+ Number(metadata.agentsMdCount) > 0 ||
151
+ Number(metadata.ignoredMdRuleCount) > 0;
152
+ const configured = usableInstruction;
153
+ const hasErrors = diagnostics.some((d) => d.severity === "error");
154
+ const status = deriveStatus({ detected, configured, hasErrors });
155
+ const parts = [];
156
+ if (Number(metadata.mdcRuleCount) > 0) {
157
+ parts.push(`${metadata.mdcRuleCount} project rule${Number(metadata.mdcRuleCount) === 1 ? "" : "s"}`);
158
+ }
159
+ if (Number(metadata.agentsMdCount) > 0) {
160
+ parts.push(`${metadata.agentsMdCount} AGENTS.md`);
161
+ }
162
+ if (metadata.hasLegacyCursorrules) {
163
+ parts.push("legacy .cursorrules");
164
+ }
165
+ let summary;
166
+ if (!detected) {
167
+ summary = "not configured";
168
+ }
169
+ else if (configured) {
170
+ summary = parts.length > 0 ? parts.join(", ") : "configured";
171
+ }
172
+ else {
173
+ summary = "detected but not configured";
174
+ }
175
+ // Silence unused dirname helper warning by referencing for nested path clarity in metadata
176
+ metadata.nestedRuleDirs = [
177
+ ...new Set(configFiles
178
+ .filter((f) => f.kind === "cursor-rule-mdc" && f.scope === "nested")
179
+ .map((f) => dirnameOf(f.relativePath))),
180
+ ];
181
+ return {
182
+ id: "cursor",
183
+ displayName: AGENT_DISPLAY_NAMES.cursor,
184
+ detected,
185
+ configured,
186
+ status,
187
+ summary,
188
+ configFiles,
189
+ configPaths: configFiles.map((f) => f.relativePath),
190
+ diagnostics,
191
+ metadata,
192
+ };
193
+ }
194
+ export const cursorAdapter = {
195
+ id: "cursor",
196
+ displayName: AGENT_DISPLAY_NAMES.cursor,
197
+ detect: detectCursor,
198
+ };
@@ -0,0 +1,17 @@
1
+ import type { DiscoveryResult } from "../types/index.js";
2
+ import { agentRegistry } from "./registry.js";
3
+ import type { AgentDetectionResult } from "./types.js";
4
+ export interface DetectAgentsOptions {
5
+ root: string;
6
+ discovery: DiscoveryResult;
7
+ maxFileSizeBytes?: number;
8
+ adapters?: typeof agentRegistry;
9
+ }
10
+ export interface DetectAgentsResult {
11
+ agents: AgentDetectionResult[];
12
+ elapsedMs: number;
13
+ }
14
+ /**
15
+ * Run all registered agent adapters against the repository.
16
+ */
17
+ export declare function detectAgents(options: DetectAgentsOptions): Promise<DetectAgentsResult>;
@@ -0,0 +1,23 @@
1
+ import { DEFAULT_MAX_FILE_SIZE_BYTES } from "../constants.js";
2
+ import { agentRegistry } from "./registry.js";
3
+ /**
4
+ * Run all registered agent adapters against the repository.
5
+ */
6
+ export async function detectAgents(options) {
7
+ const started = performance.now();
8
+ const adapters = options.adapters ?? agentRegistry;
9
+ const maxFileSizeBytes = options.maxFileSizeBytes ?? DEFAULT_MAX_FILE_SIZE_BYTES;
10
+ const agents = [];
11
+ for (const adapter of adapters) {
12
+ const result = await adapter.detect({
13
+ root: options.root,
14
+ discovery: options.discovery,
15
+ maxFileSizeBytes,
16
+ });
17
+ agents.push(result);
18
+ }
19
+ return {
20
+ agents,
21
+ elapsedMs: Math.round(performance.now() - started),
22
+ };
23
+ }
@@ -0,0 +1,36 @@
1
+ import type { AgentConfigFile, AgentConfigFileKind, AgentConfigScope, AgentConfigStatus } from "./types.js";
2
+ export interface InspectedFile {
3
+ relativePath: string;
4
+ absolutePath: string;
5
+ exists: boolean;
6
+ readable: boolean;
7
+ sizeBytes: number;
8
+ empty: boolean;
9
+ isSymlink: boolean;
10
+ /** Raw UTF-8 text when readable and within size limit; otherwise null. */
11
+ text: string | null;
12
+ error?: string;
13
+ }
14
+ /**
15
+ * Safely inspect a file under the repository root.
16
+ * Never follows paths outside the root.
17
+ */
18
+ export declare function inspectRepoFile(root: string, relativePath: string, maxFileSizeBytes?: number): Promise<InspectedFile>;
19
+ export declare function pathExistsInsideRoot(root: string, relativePath: string): Promise<boolean>;
20
+ export declare function toAgentConfigFile(inspected: InspectedFile, kind: AgentConfigFileKind, options?: {
21
+ legacy?: boolean;
22
+ scope?: AgentConfigScope;
23
+ parseError?: string;
24
+ }): AgentConfigFile;
25
+ export declare function tryParseJson(text: string): {
26
+ ok: true;
27
+ data: unknown;
28
+ } | {
29
+ ok: false;
30
+ error: string;
31
+ };
32
+ export declare function deriveStatus(options: {
33
+ detected: boolean;
34
+ configured: boolean;
35
+ hasErrors: boolean;
36
+ }): AgentConfigStatus;
@@ -0,0 +1,165 @@
1
+ import fs from "node:fs/promises";
2
+ import path from "node:path";
3
+ import { DEFAULT_MAX_FILE_SIZE_BYTES } from "../constants.js";
4
+ import { isPathInsideRoot, toPosixRelative } from "../utils/path.js";
5
+ /**
6
+ * Safely inspect a file under the repository root.
7
+ * Never follows paths outside the root.
8
+ */
9
+ export async function inspectRepoFile(root, relativePath, maxFileSizeBytes = DEFAULT_MAX_FILE_SIZE_BYTES) {
10
+ const absolutePath = path.resolve(root, relativePath);
11
+ const normalizedRelative = toPosixRelative(root, absolutePath);
12
+ if (!isPathInsideRoot(root, absolutePath)) {
13
+ return {
14
+ relativePath: normalizedRelative,
15
+ absolutePath,
16
+ exists: false,
17
+ readable: false,
18
+ sizeBytes: 0,
19
+ empty: true,
20
+ isSymlink: false,
21
+ text: null,
22
+ error: "Path escapes repository root",
23
+ };
24
+ }
25
+ try {
26
+ const lstat = await fs.lstat(absolutePath);
27
+ const isSymlink = lstat.isSymbolicLink();
28
+ if (isSymlink) {
29
+ const real = await fs.realpath(absolutePath);
30
+ if (!isPathInsideRoot(root, real)) {
31
+ return {
32
+ relativePath: normalizedRelative,
33
+ absolutePath,
34
+ exists: true,
35
+ readable: false,
36
+ sizeBytes: 0,
37
+ empty: true,
38
+ isSymlink: true,
39
+ text: null,
40
+ error: "Symlink target is outside repository root",
41
+ };
42
+ }
43
+ }
44
+ const stat = await fs.stat(absolutePath);
45
+ if (!stat.isFile()) {
46
+ return {
47
+ relativePath: normalizedRelative,
48
+ absolutePath,
49
+ exists: true,
50
+ readable: false,
51
+ sizeBytes: 0,
52
+ empty: true,
53
+ isSymlink,
54
+ text: null,
55
+ error: "Not a regular file",
56
+ };
57
+ }
58
+ const sizeBytes = Number(stat.size);
59
+ if (sizeBytes === 0) {
60
+ return {
61
+ relativePath: normalizedRelative,
62
+ absolutePath,
63
+ exists: true,
64
+ readable: true,
65
+ sizeBytes: 0,
66
+ empty: true,
67
+ isSymlink,
68
+ text: "",
69
+ };
70
+ }
71
+ if (sizeBytes > maxFileSizeBytes) {
72
+ return {
73
+ relativePath: normalizedRelative,
74
+ absolutePath,
75
+ exists: true,
76
+ readable: false,
77
+ sizeBytes,
78
+ empty: false,
79
+ isSymlink,
80
+ text: null,
81
+ error: "File exceeds max size limit",
82
+ };
83
+ }
84
+ const text = await fs.readFile(absolutePath, "utf8");
85
+ const trimmedEmpty = text.trim().length === 0;
86
+ return {
87
+ relativePath: normalizedRelative,
88
+ absolutePath,
89
+ exists: true,
90
+ readable: true,
91
+ sizeBytes,
92
+ empty: trimmedEmpty,
93
+ isSymlink,
94
+ text,
95
+ };
96
+ }
97
+ catch (error) {
98
+ const message = error instanceof Error ? error.message : "Unable to read file";
99
+ const code = error && typeof error === "object" && "code" in error
100
+ ? String(error.code)
101
+ : "";
102
+ return {
103
+ relativePath: normalizedRelative,
104
+ absolutePath,
105
+ exists: code !== "ENOENT",
106
+ readable: false,
107
+ sizeBytes: 0,
108
+ empty: true,
109
+ isSymlink: false,
110
+ text: null,
111
+ error: message,
112
+ };
113
+ }
114
+ }
115
+ export async function pathExistsInsideRoot(root, relativePath) {
116
+ const absolutePath = path.resolve(root, relativePath);
117
+ if (!isPathInsideRoot(root, absolutePath)) {
118
+ return false;
119
+ }
120
+ try {
121
+ await fs.access(absolutePath);
122
+ return true;
123
+ }
124
+ catch {
125
+ return false;
126
+ }
127
+ }
128
+ export function toAgentConfigFile(inspected, kind, options = {}) {
129
+ const file = {
130
+ relativePath: inspected.relativePath,
131
+ kind,
132
+ sizeBytes: inspected.sizeBytes,
133
+ empty: inspected.empty,
134
+ readable: inspected.readable,
135
+ legacy: options.legacy ?? false,
136
+ scope: options.scope ?? (inspected.relativePath.includes("/") ? "nested" : "root"),
137
+ };
138
+ if (options.parseError !== undefined) {
139
+ file.parseError = options.parseError;
140
+ }
141
+ return file;
142
+ }
143
+ export function tryParseJson(text) {
144
+ try {
145
+ return { ok: true, data: JSON.parse(text) };
146
+ }
147
+ catch (error) {
148
+ return {
149
+ ok: false,
150
+ error: error instanceof Error ? error.message : "Invalid JSON",
151
+ };
152
+ }
153
+ }
154
+ export function deriveStatus(options) {
155
+ if (!options.detected) {
156
+ return "absent";
157
+ }
158
+ if (options.hasErrors && !options.configured) {
159
+ return "misconfigured";
160
+ }
161
+ if (options.configured) {
162
+ return options.hasErrors ? "misconfigured" : "configured";
163
+ }
164
+ return "detected";
165
+ }
@@ -0,0 +1,7 @@
1
+ import type { AgentAdapter } from "./types.js";
2
+ /**
3
+ * Central registry of AI coding agent adapters.
4
+ * Add new agents by implementing AgentAdapter and appending here.
5
+ */
6
+ export declare const agentRegistry: readonly AgentAdapter[];
7
+ export declare function getAgentAdapter(id: string): AgentAdapter | undefined;
@@ -0,0 +1,11 @@
1
+ import { claudeAdapter } from "./claude/adapter.js";
2
+ import { codexAdapter } from "./codex/adapter.js";
3
+ import { cursorAdapter } from "./cursor/adapter.js";
4
+ /**
5
+ * Central registry of AI coding agent adapters.
6
+ * Add new agents by implementing AgentAdapter and appending here.
7
+ */
8
+ export const agentRegistry = [cursorAdapter, claudeAdapter, codexAdapter];
9
+ export function getAgentAdapter(id) {
10
+ return agentRegistry.find((adapter) => adapter.id === id);
11
+ }