@praneeth_54/agentdoctor 1.1.1 → 2.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 +95 -7
- package/README.md +163 -538
- package/dist/agents/aider/adapter.d.ts +1 -0
- package/dist/agents/aider/adapter.js +1 -0
- package/dist/agents/aider/detector.d.ts +14 -0
- package/dist/agents/aider/detector.js +120 -0
- package/dist/agents/copilot/adapter.d.ts +1 -0
- package/dist/agents/copilot/adapter.js +1 -0
- package/dist/agents/copilot/detector.d.ts +13 -0
- package/dist/agents/copilot/detector.js +113 -0
- package/dist/agents/gemini/adapter.d.ts +1 -0
- package/dist/agents/gemini/adapter.js +1 -0
- package/dist/agents/gemini/detector.d.ts +13 -0
- package/dist/agents/gemini/detector.js +151 -0
- package/dist/agents/registry.js +13 -1
- package/dist/agents/types.d.ts +1 -1
- package/dist/agents/windsurf/adapter.d.ts +1 -0
- package/dist/agents/windsurf/adapter.js +1 -0
- package/dist/agents/windsurf/detector.d.ts +14 -0
- package/dist/agents/windsurf/detector.js +119 -0
- package/dist/architecture/c4.d.ts +22 -0
- package/dist/architecture/c4.js +62 -0
- package/dist/cli/commands/brain.d.ts +12 -0
- package/dist/cli/commands/brain.js +162 -0
- package/dist/cli/commands/complete.d.ts +23 -0
- package/dist/cli/commands/complete.js +232 -0
- package/dist/cli/commands/explain.js +1 -1
- package/dist/cli/commands/fix.js +22 -2
- package/dist/cli/commands/mcp.d.ts +8 -0
- package/dist/cli/commands/mcp.js +14 -0
- package/dist/cli/commands/platform.d.ts +15 -0
- package/dist/cli/commands/platform.js +191 -0
- package/dist/cli/commands/v2.d.ts +55 -0
- package/dist/cli/commands/v2.js +278 -0
- package/dist/cli/program.js +809 -2
- package/dist/constants.d.ts +3 -2
- package/dist/constants.js +5 -1
- package/dist/contracts/adapters.d.ts +7 -0
- package/dist/contracts/adapters.js +60 -0
- package/dist/contracts/index.d.ts +162 -0
- package/dist/contracts/index.js +18 -0
- package/dist/core/baseline/store.d.ts +48 -0
- package/dist/core/baseline/store.js +163 -0
- package/dist/core/brain-cli/service.d.ts +26 -0
- package/dist/core/brain-cli/service.js +175 -0
- package/dist/core/brain-product/init.d.ts +45 -0
- package/dist/core/brain-product/init.js +232 -0
- package/dist/core/changes/analyze.d.ts +41 -0
- package/dist/core/changes/analyze.js +223 -0
- package/dist/core/context-health/analyze.d.ts +15 -0
- package/dist/core/context-health/analyze.js +234 -0
- package/dist/core/fix/apply.d.ts +21 -4
- package/dist/core/fix/apply.js +118 -18
- package/dist/core/fix/backup.d.ts +30 -0
- package/dist/core/fix/backup.js +193 -0
- package/dist/core/fix/plan.d.ts +6 -3
- package/dist/core/fix/plan.js +113 -9
- package/dist/core/fix/render.d.ts +2 -0
- package/dist/core/fix/render.js +36 -21
- package/dist/core/fix/run.js +5 -1
- package/dist/core/fix/safe-target.d.ts +14 -0
- package/dist/core/fix/safe-target.js +81 -0
- package/dist/core/fix/types.d.ts +12 -1
- package/dist/core/fix/types.js +2 -0
- package/dist/core/fix/writers/claude-settings.js +3 -1
- package/dist/core/fix/writers/codex-config.d.ts +3 -0
- package/dist/core/fix/writers/codex-config.js +24 -3
- package/dist/core/fix/writers/cursorignore.js +3 -1
- package/dist/core/fix/writers/simple-ignore.d.ts +16 -0
- package/dist/core/fix/writers/simple-ignore.js +68 -0
- package/dist/core/monorepo/detect.d.ts +13 -0
- package/dist/core/monorepo/detect.js +121 -0
- package/dist/core/rules/build-context.js +22 -3
- package/dist/core/rules/context/generated-directory.js +11 -1
- package/dist/core/rules/context/large-instruction-file.js +6 -0
- package/dist/core/rules/context/large-log-file.js +16 -1
- package/dist/core/rules/ignore.d.ts +11 -1
- package/dist/core/rules/ignore.js +22 -1
- package/dist/core/rules/instructions/empty-instructions.js +7 -0
- package/dist/core/rules/security/env-file-exposure.js +23 -2
- package/dist/core/rules/security/private-key-file.js +1 -1
- package/dist/core/scanner/scan.js +1 -1
- package/dist/core/schemas/validate.d.ts +6 -0
- package/dist/core/schemas/validate.js +62 -0
- package/dist/core/scoring/compute-scores.d.ts +1 -1
- package/dist/core/scoring/compute-scores.js +5 -1
- package/dist/core/scoring/placeholder.d.ts +1 -1
- package/dist/core/scoring/placeholder.js +5 -1
- package/dist/core/secrets/scan.d.ts +23 -0
- package/dist/core/secrets/scan.js +172 -0
- package/dist/dashboard/server.d.ts +24 -0
- package/dist/dashboard/server.js +386 -0
- package/dist/enforcement/runner.d.ts +23 -0
- package/dist/enforcement/runner.js +56 -0
- package/dist/index.d.ts +13 -0
- package/dist/index.js +12 -0
- package/dist/integrations/github/pr-analyze.d.ts +28 -0
- package/dist/integrations/github/pr-analyze.js +98 -0
- package/dist/integrations/local-ai/provider.d.ts +45 -0
- package/dist/integrations/local-ai/provider.js +104 -0
- package/dist/intelligence/git/analyze.d.ts +34 -0
- package/dist/intelligence/git/analyze.js +136 -0
- package/dist/intelligence/graph/build.d.ts +13 -0
- package/dist/intelligence/graph/build.js +215 -0
- package/dist/knowledge/store.d.ts +25 -0
- package/dist/knowledge/store.js +91 -0
- package/dist/mcp/agentdoctor/server.d.ts +16 -0
- package/dist/mcp/agentdoctor/server.js +67 -0
- package/dist/mcp/brain/session.js +7 -7
- package/dist/mcp/intelligence/handlers.d.ts +11 -0
- package/dist/mcp/intelligence/handlers.js +175 -0
- package/dist/mcp/intelligence/path-safety.d.ts +11 -0
- package/dist/mcp/intelligence/path-safety.js +82 -0
- package/dist/mcp/intelligence/registry.d.ts +8 -0
- package/dist/mcp/intelligence/registry.js +160 -0
- package/dist/ops/health.d.ts +18 -0
- package/dist/ops/health.js +51 -0
- package/dist/platform/ai-quality/analyze.d.ts +9 -0
- package/dist/platform/ai-quality/analyze.js +65 -0
- package/dist/platform/architecture/drift.d.ts +17 -0
- package/dist/platform/architecture/drift.js +74 -0
- package/dist/platform/auth/local.d.ts +15 -0
- package/dist/platform/auth/local.js +58 -0
- package/dist/platform/context-security/analyze.d.ts +6 -0
- package/dist/platform/context-security/analyze.js +97 -0
- package/dist/platform/firewall/evaluate.d.ts +54 -0
- package/dist/platform/firewall/evaluate.js +308 -0
- package/dist/platform/graph/build.d.ts +6 -0
- package/dist/platform/graph/build.js +193 -0
- package/dist/platform/health/analyze.d.ts +7 -0
- package/dist/platform/health/analyze.js +186 -0
- package/dist/platform/index.d.ts +27 -0
- package/dist/platform/index.js +87 -0
- package/dist/platform/knowledge/analyze.d.ts +20 -0
- package/dist/platform/knowledge/analyze.js +96 -0
- package/dist/platform/provenance/build.d.ts +44 -0
- package/dist/platform/provenance/build.js +65 -0
- package/dist/platform/readiness/scorecard.d.ts +5 -0
- package/dist/platform/readiness/scorecard.js +98 -0
- package/dist/platform/refactor/impact.d.ts +20 -0
- package/dist/platform/refactor/impact.js +60 -0
- package/dist/platform/reports/export.d.ts +12 -0
- package/dist/platform/reports/export.js +92 -0
- package/dist/platform/security/redact.d.ts +15 -0
- package/dist/platform/security/redact.js +87 -0
- package/dist/platform/sessions/store.d.ts +32 -0
- package/dist/platform/sessions/store.js +97 -0
- package/dist/platform/store.d.ts +6 -0
- package/dist/platform/store.js +57 -0
- package/dist/platform/test-impact/analyze.d.ts +32 -0
- package/dist/platform/test-impact/analyze.js +164 -0
- package/dist/platform/time-machine/compare.d.ts +17 -0
- package/dist/platform/time-machine/compare.js +62 -0
- package/dist/platform/tokens/plan.d.ts +28 -0
- package/dist/platform/tokens/plan.js +89 -0
- package/dist/platform/types.d.ts +111 -0
- package/dist/platform/types.js +5 -0
- package/dist/plugins/runtime.d.ts +36 -0
- package/dist/plugins/runtime.js +101 -0
- package/dist/plugins/sdk.d.ts +29 -0
- package/dist/plugins/sdk.js +114 -0
- package/dist/policy/packs.d.ts +9 -0
- package/dist/policy/packs.js +59 -0
- package/dist/reporters/terminal/report.js +38 -5
- package/dist/storage/provider.d.ts +35 -0
- package/dist/storage/provider.js +103 -0
- package/dist/team/auth.d.ts +35 -0
- package/dist/team/auth.js +70 -0
- package/dist/types/index.d.ts +5 -1
- package/package.json +8 -4
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import fs from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { resolveRepoRoot, toPosixRelative } from "../../utils/path.js";
|
|
4
|
+
async function readJson(file) {
|
|
5
|
+
try {
|
|
6
|
+
const raw = await fs.readFile(file, "utf8");
|
|
7
|
+
const parsed = JSON.parse(raw);
|
|
8
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed))
|
|
9
|
+
return null;
|
|
10
|
+
return parsed;
|
|
11
|
+
}
|
|
12
|
+
catch {
|
|
13
|
+
return null;
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
async function resolveGlobDirs(root, pattern) {
|
|
17
|
+
// Support simple patterns: "packages/*", "apps/*", "."
|
|
18
|
+
if (pattern === "." || pattern === "./") {
|
|
19
|
+
return [root];
|
|
20
|
+
}
|
|
21
|
+
const normalized = pattern.replace(/^\.\//, "").replace(/\/$/, "");
|
|
22
|
+
if (normalized.endsWith("/*")) {
|
|
23
|
+
const parent = path.join(root, normalized.slice(0, -2));
|
|
24
|
+
try {
|
|
25
|
+
const entries = await fs.readdir(parent, { withFileTypes: true });
|
|
26
|
+
return entries
|
|
27
|
+
.filter((e) => e.isDirectory() && !e.name.startsWith("."))
|
|
28
|
+
.map((e) => path.join(parent, e.name));
|
|
29
|
+
}
|
|
30
|
+
catch {
|
|
31
|
+
return [];
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
const candidate = path.join(root, normalized);
|
|
35
|
+
try {
|
|
36
|
+
const st = await fs.stat(candidate);
|
|
37
|
+
return st.isDirectory() ? [candidate] : [];
|
|
38
|
+
}
|
|
39
|
+
catch {
|
|
40
|
+
return [];
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
async function packageFromDir(root, dir) {
|
|
44
|
+
const pkg = await readJson(path.join(dir, "package.json"));
|
|
45
|
+
if (!pkg)
|
|
46
|
+
return null;
|
|
47
|
+
const name = typeof pkg.name === "string" ? pkg.name : path.basename(dir);
|
|
48
|
+
return {
|
|
49
|
+
name,
|
|
50
|
+
relativePath: toPosixRelative(root, dir) || ".",
|
|
51
|
+
absolutePath: dir,
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
export async function detectMonorepo(rootInput) {
|
|
55
|
+
const root = resolveRepoRoot(rootInput);
|
|
56
|
+
const limitations = [];
|
|
57
|
+
const packages = [];
|
|
58
|
+
const rootPkg = await readJson(path.join(root, "package.json"));
|
|
59
|
+
let tool = "none";
|
|
60
|
+
const patterns = [];
|
|
61
|
+
if (rootPkg) {
|
|
62
|
+
const workspaces = rootPkg.workspaces;
|
|
63
|
+
if (Array.isArray(workspaces)) {
|
|
64
|
+
tool = "npm-workspaces";
|
|
65
|
+
for (const p of workspaces) {
|
|
66
|
+
if (typeof p === "string")
|
|
67
|
+
patterns.push(p);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
else if (workspaces && typeof workspaces === "object" && !Array.isArray(workspaces)) {
|
|
71
|
+
const pkgs = workspaces.packages;
|
|
72
|
+
if (Array.isArray(pkgs)) {
|
|
73
|
+
tool = "npm-workspaces";
|
|
74
|
+
for (const p of pkgs) {
|
|
75
|
+
if (typeof p === "string")
|
|
76
|
+
patterns.push(p);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
try {
|
|
82
|
+
const pnpmRaw = await fs.readFile(path.join(root, "pnpm-workspace.yaml"), "utf8");
|
|
83
|
+
tool = "pnpm-workspaces";
|
|
84
|
+
for (const line of pnpmRaw.split(/\r?\n/)) {
|
|
85
|
+
const m = line.match(/^\s*-\s+['"]?([^'"]+)['"]?\s*$/);
|
|
86
|
+
if (m?.[1])
|
|
87
|
+
patterns.push(m[1]);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
catch {
|
|
91
|
+
// optional
|
|
92
|
+
}
|
|
93
|
+
const seen = new Set();
|
|
94
|
+
for (const pattern of patterns) {
|
|
95
|
+
const dirs = await resolveGlobDirs(root, pattern);
|
|
96
|
+
for (const dir of dirs) {
|
|
97
|
+
const pkg = await packageFromDir(root, dir);
|
|
98
|
+
if (!pkg)
|
|
99
|
+
continue;
|
|
100
|
+
if (seen.has(pkg.absolutePath))
|
|
101
|
+
continue;
|
|
102
|
+
seen.add(pkg.absolutePath);
|
|
103
|
+
packages.push(pkg);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
if (packages.length === 0) {
|
|
107
|
+
const self = await packageFromDir(root, root);
|
|
108
|
+
if (self) {
|
|
109
|
+
packages.push(self);
|
|
110
|
+
}
|
|
111
|
+
limitations.push("No workspace packages discovered; treating repository as a single package");
|
|
112
|
+
}
|
|
113
|
+
packages.sort((a, b) => a.relativePath.localeCompare(b.relativePath));
|
|
114
|
+
return {
|
|
115
|
+
root,
|
|
116
|
+
isMonorepo: tool !== "none",
|
|
117
|
+
tool,
|
|
118
|
+
packages,
|
|
119
|
+
limitations,
|
|
120
|
+
};
|
|
121
|
+
}
|
|
@@ -1,10 +1,13 @@
|
|
|
1
1
|
import { parseProjectMcpConfigs } from "../mcp/parse.js";
|
|
2
2
|
import { createIgnoreIndex, parseIgnoreFile, relativizeIgnorePatterns } from "./ignore.js";
|
|
3
3
|
import { TextCache } from "./text-cache.js";
|
|
4
|
+
const ROOT_IGNORE_FILES = new Set([".gitignore", ".cursorignore", ".geminiignore", ".aiderignore"]);
|
|
4
5
|
export async function buildRuleContext(options) {
|
|
5
6
|
const textCache = new TextCache(options.root, options.maxFileSizeBytes);
|
|
6
7
|
const gitignorePatterns = [];
|
|
7
8
|
const cursorignorePatterns = [];
|
|
9
|
+
const geminiignorePatterns = [];
|
|
10
|
+
const aiderignorePatterns = [];
|
|
8
11
|
const rootGitignore = await textCache.read(".gitignore");
|
|
9
12
|
if (rootGitignore.text) {
|
|
10
13
|
gitignorePatterns.push(...parseIgnoreFile(rootGitignore.text));
|
|
@@ -13,12 +16,20 @@ export async function buildRuleContext(options) {
|
|
|
13
16
|
if (rootCursorignore.text) {
|
|
14
17
|
cursorignorePatterns.push(...parseIgnoreFile(rootCursorignore.text));
|
|
15
18
|
}
|
|
19
|
+
const rootGeminiignore = await textCache.read(".geminiignore");
|
|
20
|
+
if (rootGeminiignore.text) {
|
|
21
|
+
geminiignorePatterns.push(...parseIgnoreFile(rootGeminiignore.text));
|
|
22
|
+
}
|
|
23
|
+
const rootAiderignore = await textCache.read(".aiderignore");
|
|
24
|
+
if (rootAiderignore.text) {
|
|
25
|
+
aiderignorePatterns.push(...parseIgnoreFile(rootAiderignore.text));
|
|
26
|
+
}
|
|
16
27
|
for (const file of options.discovery.files) {
|
|
17
28
|
const base = file.relativePath.split("/").pop() ?? file.relativePath;
|
|
18
|
-
if (
|
|
29
|
+
if (!ROOT_IGNORE_FILES.has(base)) {
|
|
19
30
|
continue;
|
|
20
31
|
}
|
|
21
|
-
if (
|
|
32
|
+
if (ROOT_IGNORE_FILES.has(file.relativePath)) {
|
|
22
33
|
continue;
|
|
23
34
|
}
|
|
24
35
|
const cached = await textCache.read(file.relativePath);
|
|
@@ -32,13 +43,21 @@ export async function buildRuleContext(options) {
|
|
|
32
43
|
if (base === ".gitignore") {
|
|
33
44
|
gitignorePatterns.push(...patterns);
|
|
34
45
|
}
|
|
35
|
-
else {
|
|
46
|
+
else if (base === ".cursorignore") {
|
|
36
47
|
cursorignorePatterns.push(...patterns);
|
|
37
48
|
}
|
|
49
|
+
else if (base === ".geminiignore") {
|
|
50
|
+
geminiignorePatterns.push(...patterns);
|
|
51
|
+
}
|
|
52
|
+
else if (base === ".aiderignore") {
|
|
53
|
+
aiderignorePatterns.push(...patterns);
|
|
54
|
+
}
|
|
38
55
|
}
|
|
39
56
|
const ignore = createIgnoreIndex({
|
|
40
57
|
gitignorePatterns,
|
|
41
58
|
cursorignorePatterns,
|
|
59
|
+
geminiignorePatterns,
|
|
60
|
+
aiderignorePatterns,
|
|
42
61
|
});
|
|
43
62
|
const mcp = await parseProjectMcpConfigs(options.root, options.maxFileSizeBytes);
|
|
44
63
|
const instructionPaths = new Set();
|
|
@@ -33,6 +33,16 @@ async function agentsStillExposed(context, relativePath, configured) {
|
|
|
33
33
|
if (agent === "codex" && (await codexDeniesPath(context, relativePath))) {
|
|
34
34
|
continue;
|
|
35
35
|
}
|
|
36
|
+
if (agent === "gemini-cli" &&
|
|
37
|
+
(context.ignore.matchesGeminiignore(relativePath) ||
|
|
38
|
+
context.ignore.matchesGeminiignore(`${relativePath}/`))) {
|
|
39
|
+
continue;
|
|
40
|
+
}
|
|
41
|
+
if (agent === "aider" &&
|
|
42
|
+
(context.ignore.matchesAiderignore(relativePath) ||
|
|
43
|
+
context.ignore.matchesAiderignore(`${relativePath}/`))) {
|
|
44
|
+
continue;
|
|
45
|
+
}
|
|
36
46
|
exposed.push(agent);
|
|
37
47
|
}
|
|
38
48
|
return exposed;
|
|
@@ -78,7 +88,7 @@ export const generatedDirectoryRule = {
|
|
|
78
88
|
title: "Generated directory may enter agent context",
|
|
79
89
|
message: `${relativePath}/ (${entry.label}) is present and no project ignore pattern was detected`,
|
|
80
90
|
whyItMatters: "Generated directories are usually low-value for coding agents and can bloat indexing/context when not excluded.",
|
|
81
|
-
recommendation: `Add an ignore pattern covering ${relativePath}/ to .gitignore and agent exclusions (.cursorignore, Claude Code Read deny,
|
|
91
|
+
recommendation: `Add an ignore pattern covering ${relativePath}/ to .gitignore and agent exclusions (.cursorignore, Claude Code Read deny, Codex filesystem deny, .geminiignore, and/or .aiderignore).`,
|
|
82
92
|
affectedAgents: affected,
|
|
83
93
|
evidence: { path: relativePath },
|
|
84
94
|
fixability: "safe",
|
|
@@ -7,6 +7,12 @@ const INSTRUCTION_KINDS = new Set([
|
|
|
7
7
|
"claude-md",
|
|
8
8
|
"claude-local-md",
|
|
9
9
|
"claude-rule-md",
|
|
10
|
+
"copilot-instructions",
|
|
11
|
+
"copilot-path-instructions",
|
|
12
|
+
"windsurf-rule-md",
|
|
13
|
+
"windsurf-legacy-rules",
|
|
14
|
+
"gemini-md",
|
|
15
|
+
"aider-conventions",
|
|
10
16
|
]);
|
|
11
17
|
export const largeInstructionFileRule = {
|
|
12
18
|
id: "context/large-instruction-file",
|
|
@@ -26,7 +26,15 @@ export const largeLogFileRule = {
|
|
|
26
26
|
if (context.ignore.matchesGitignore(file.relativePath)) {
|
|
27
27
|
continue;
|
|
28
28
|
}
|
|
29
|
-
const defaultAgents = [
|
|
29
|
+
const defaultAgents = [
|
|
30
|
+
"cursor",
|
|
31
|
+
"claude-code",
|
|
32
|
+
"codex",
|
|
33
|
+
"copilot",
|
|
34
|
+
"windsurf",
|
|
35
|
+
"gemini-cli",
|
|
36
|
+
"aider",
|
|
37
|
+
];
|
|
30
38
|
const candidates = configured.length > 0 ? configured : defaultAgents;
|
|
31
39
|
const affected = [];
|
|
32
40
|
for (const agent of candidates) {
|
|
@@ -39,6 +47,13 @@ export const largeLogFileRule = {
|
|
|
39
47
|
if (agent === "codex" && (await codexDeniesPath(context, file.relativePath))) {
|
|
40
48
|
continue;
|
|
41
49
|
}
|
|
50
|
+
if (agent === "gemini-cli" && context.ignore.matchesGeminiignore(file.relativePath)) {
|
|
51
|
+
continue;
|
|
52
|
+
}
|
|
53
|
+
if (agent === "aider" && context.ignore.matchesAiderignore(file.relativePath)) {
|
|
54
|
+
continue;
|
|
55
|
+
}
|
|
56
|
+
// Copilot / Windsurf: no official project deny/ignore path.
|
|
42
57
|
affected.push(agent);
|
|
43
58
|
}
|
|
44
59
|
if (affected.length === 0) {
|
|
@@ -1,14 +1,22 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Minimal gitignore-style pattern matching for .gitignore / .
|
|
2
|
+
* Minimal gitignore-style pattern matching for .gitignore / agent ignore files.
|
|
3
3
|
* Supports common patterns used in agent ignore files — not a full gitignore clone.
|
|
4
4
|
*/
|
|
5
5
|
export interface IgnoreIndex {
|
|
6
6
|
gitignorePatterns: string[];
|
|
7
7
|
cursorignorePatterns: string[];
|
|
8
|
+
geminiignorePatterns: string[];
|
|
9
|
+
aiderignorePatterns: string[];
|
|
8
10
|
matchesGitignore(relativePath: string): boolean;
|
|
9
11
|
matchesCursorignore(relativePath: string): boolean;
|
|
12
|
+
matchesGeminiignore(relativePath: string): boolean;
|
|
13
|
+
matchesAiderignore(relativePath: string): boolean;
|
|
10
14
|
/** Whether a path appears excluded for Cursor agent/indexing purposes. */
|
|
11
15
|
isExcludedForCursor(relativePath: string): boolean;
|
|
16
|
+
/** Gemini CLI respects .geminiignore and .gitignore. */
|
|
17
|
+
isExcludedForGemini(relativePath: string): boolean;
|
|
18
|
+
/** Aider respects .aiderignore and typically .gitignore. */
|
|
19
|
+
isExcludedForAider(relativePath: string): boolean;
|
|
12
20
|
}
|
|
13
21
|
export declare function parseIgnoreFile(content: string): string[];
|
|
14
22
|
/**
|
|
@@ -25,4 +33,6 @@ export declare function pathMatchesAny(relativePath: string, patterns: string[])
|
|
|
25
33
|
export declare function createIgnoreIndex(options: {
|
|
26
34
|
gitignorePatterns: string[];
|
|
27
35
|
cursorignorePatterns: string[];
|
|
36
|
+
geminiignorePatterns?: string[];
|
|
37
|
+
aiderignorePatterns?: string[];
|
|
28
38
|
}): IgnoreIndex;
|
|
@@ -50,7 +50,6 @@ export function matchIgnorePattern(relativePath, pattern) {
|
|
|
50
50
|
if (pat.endsWith("/")) {
|
|
51
51
|
const dir = pat.slice(0, -1);
|
|
52
52
|
if (!dir.includes("/")) {
|
|
53
|
-
// `build/` matches build at any directory level (gitignore semantics)
|
|
54
53
|
matched =
|
|
55
54
|
pathNorm === dir ||
|
|
56
55
|
pathNorm.endsWith(`/${dir}`) ||
|
|
@@ -114,15 +113,25 @@ const CURSOR_DEFAULT_ENV_PATTERNS = [".env", ".env.*", "**/.env", "**/.env.*"];
|
|
|
114
113
|
export function createIgnoreIndex(options) {
|
|
115
114
|
const gitignorePatterns = options.gitignorePatterns;
|
|
116
115
|
const cursorignorePatterns = options.cursorignorePatterns;
|
|
116
|
+
const geminiignorePatterns = options.geminiignorePatterns ?? [];
|
|
117
|
+
const aiderignorePatterns = options.aiderignorePatterns ?? [];
|
|
117
118
|
return {
|
|
118
119
|
gitignorePatterns,
|
|
119
120
|
cursorignorePatterns,
|
|
121
|
+
geminiignorePatterns,
|
|
122
|
+
aiderignorePatterns,
|
|
120
123
|
matchesGitignore(relativePath) {
|
|
121
124
|
return pathMatchesAny(relativePath, gitignorePatterns);
|
|
122
125
|
},
|
|
123
126
|
matchesCursorignore(relativePath) {
|
|
124
127
|
return pathMatchesAny(relativePath, cursorignorePatterns);
|
|
125
128
|
},
|
|
129
|
+
matchesGeminiignore(relativePath) {
|
|
130
|
+
return pathMatchesAny(relativePath, geminiignorePatterns);
|
|
131
|
+
},
|
|
132
|
+
matchesAiderignore(relativePath) {
|
|
133
|
+
return pathMatchesAny(relativePath, aiderignorePatterns);
|
|
134
|
+
},
|
|
126
135
|
isExcludedForCursor(relativePath) {
|
|
127
136
|
if (pathMatchesAny(relativePath, cursorignorePatterns)) {
|
|
128
137
|
return true;
|
|
@@ -132,5 +141,17 @@ export function createIgnoreIndex(options) {
|
|
|
132
141
|
}
|
|
133
142
|
return pathMatchesAny(relativePath, CURSOR_DEFAULT_ENV_PATTERNS);
|
|
134
143
|
},
|
|
144
|
+
isExcludedForGemini(relativePath) {
|
|
145
|
+
if (pathMatchesAny(relativePath, geminiignorePatterns)) {
|
|
146
|
+
return true;
|
|
147
|
+
}
|
|
148
|
+
return pathMatchesAny(relativePath, gitignorePatterns);
|
|
149
|
+
},
|
|
150
|
+
isExcludedForAider(relativePath) {
|
|
151
|
+
if (pathMatchesAny(relativePath, aiderignorePatterns)) {
|
|
152
|
+
return true;
|
|
153
|
+
}
|
|
154
|
+
return pathMatchesAny(relativePath, gitignorePatterns);
|
|
155
|
+
},
|
|
135
156
|
};
|
|
136
157
|
}
|
|
@@ -6,6 +6,13 @@ const EMPTYABLE_KINDS = new Set([
|
|
|
6
6
|
"claude-md",
|
|
7
7
|
"claude-local-md",
|
|
8
8
|
"claude-rule-md",
|
|
9
|
+
"copilot-instructions",
|
|
10
|
+
"copilot-path-instructions",
|
|
11
|
+
"windsurf-rule-md",
|
|
12
|
+
"windsurf-legacy-rules",
|
|
13
|
+
"gemini-md",
|
|
14
|
+
"aider-conventions",
|
|
15
|
+
"aider-conf",
|
|
9
16
|
]);
|
|
10
17
|
export const emptyInstructionsRule = {
|
|
11
18
|
id: "instructions/empty-instructions",
|
|
@@ -60,6 +60,27 @@ function agentsWithoutClearExclusion(context, relativePath) {
|
|
|
60
60
|
}
|
|
61
61
|
if (agent.id === "codex") {
|
|
62
62
|
affected.push("codex");
|
|
63
|
+
continue;
|
|
64
|
+
}
|
|
65
|
+
if (agent.id === "copilot") {
|
|
66
|
+
// No official project-local Copilot deny/ignore file; treat as exposed when present.
|
|
67
|
+
affected.push("copilot");
|
|
68
|
+
continue;
|
|
69
|
+
}
|
|
70
|
+
if (agent.id === "windsurf") {
|
|
71
|
+
affected.push("windsurf");
|
|
72
|
+
continue;
|
|
73
|
+
}
|
|
74
|
+
if (agent.id === "gemini-cli") {
|
|
75
|
+
if (!context.ignore.isExcludedForGemini(relativePath)) {
|
|
76
|
+
affected.push("gemini-cli");
|
|
77
|
+
}
|
|
78
|
+
continue;
|
|
79
|
+
}
|
|
80
|
+
if (agent.id === "aider") {
|
|
81
|
+
if (!context.ignore.isExcludedForAider(relativePath)) {
|
|
82
|
+
affected.push("aider");
|
|
83
|
+
}
|
|
63
84
|
}
|
|
64
85
|
}
|
|
65
86
|
return affected;
|
|
@@ -95,7 +116,7 @@ export const envFileExposureRule = {
|
|
|
95
116
|
severity: kind === "backup" ? "warning" : "critical",
|
|
96
117
|
title: "Sensitive environment file present in repository",
|
|
97
118
|
message: `Sensitive environment file present in the repository: ${file.relativePath}`,
|
|
98
|
-
whyItMatters: "No supported coding-agent configuration was detected, so agent-specific exposure was not asserted.
|
|
119
|
+
whyItMatters: "No supported coding-agent configuration was detected, so agent-specific exposure was not asserted. Repository hygiene still applies: the file is high-risk if tracked or shared.",
|
|
99
120
|
recommendation: "Keep runtime environment files out of version control and rotate credentials if the file may have been shared.",
|
|
100
121
|
affectedAgents: [],
|
|
101
122
|
evidence: {
|
|
@@ -127,7 +148,7 @@ export const envFileExposureRule = {
|
|
|
127
148
|
title: "Sensitive environment file may enter agent context",
|
|
128
149
|
message: `Sensitive environment file detected and no relevant exclusion was found for: ${file.relativePath}`,
|
|
129
150
|
whyItMatters: "Environment files frequently hold API keys and credentials. If readable by an AI coding agent, those values may be included in prompts or logs.",
|
|
130
|
-
recommendation: "Add an agent-specific exclusion (for example .cursorignore, a Claude Code Read deny rule,
|
|
151
|
+
recommendation: "Add an agent-specific exclusion (for example .cursorignore, a Claude Code Read deny rule, a Codex filesystem deny, .geminiignore, or .aiderignore), keep the file out of version control, and rotate any credentials that may have been exposed.",
|
|
131
152
|
affectedAgents: affected,
|
|
132
153
|
evidence: {
|
|
133
154
|
path: file.relativePath,
|
|
@@ -58,7 +58,7 @@ export const privateKeyFileRule = {
|
|
|
58
58
|
message: `Possible ${label} detected at ${file.relativePath}`,
|
|
59
59
|
whyItMatters: agentsPresent
|
|
60
60
|
? "Credential material in the working tree can be read by AI coding agents and may leak into model context or logs. Filename heuristics only — contents were not inspected."
|
|
61
|
-
: "Credential material in the working tree is high-risk repository content. No supported coding-agent configuration was detected, so agent-specific exposure was not asserted. Filename heuristics only — contents were not inspected.",
|
|
61
|
+
: "Credential material in the working tree is high-risk repository content. No supported coding-agent configuration was detected, so agent-specific exposure was not asserted. Repository hygiene still applies. Filename heuristics only — contents were not inspected.",
|
|
62
62
|
recommendation: "Remove the file from the repository, rotate the credential, add ignore rules, and never commit replacements.",
|
|
63
63
|
affectedAgents: affected,
|
|
64
64
|
evidence: { path: file.relativePath, detail: label },
|
|
@@ -49,7 +49,7 @@ export async function scan(options = {}) {
|
|
|
49
49
|
const agentsPresent = agents.some((a) => a.detected || a.configured);
|
|
50
50
|
const agentSecurityAnalysis = agentsPresent ? "full" : "limited";
|
|
51
51
|
if (agentSecurityAnalysis === "limited") {
|
|
52
|
-
warnings.push("
|
|
52
|
+
warnings.push("Agent-specific exposure checks are limited (no supported agent config); repository hygiene still applied.");
|
|
53
53
|
}
|
|
54
54
|
const scoringStarted = performance.now();
|
|
55
55
|
const scores = computeReadinessScores(findings);
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Minimal JSON Schema subset validator for AgentDoctor v2 report schemas.
|
|
3
|
+
* Supports: type, required, properties, items, enum, integer/string/boolean/object/array/null unions.
|
|
4
|
+
* Not a full draft-2020 implementation — sufficient for shipped schemas without new deps.
|
|
5
|
+
*/
|
|
6
|
+
export declare function validateAgainstSchema(schema: Record<string, unknown>, value: unknown, path?: string): string[];
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Minimal JSON Schema subset validator for AgentDoctor v2 report schemas.
|
|
3
|
+
* Supports: type, required, properties, items, enum, integer/string/boolean/object/array/null unions.
|
|
4
|
+
* Not a full draft-2020 implementation — sufficient for shipped schemas without new deps.
|
|
5
|
+
*/
|
|
6
|
+
export function validateAgainstSchema(schema, value, path = "$") {
|
|
7
|
+
const errors = [];
|
|
8
|
+
const type = schema.type;
|
|
9
|
+
if (type !== undefined) {
|
|
10
|
+
const types = Array.isArray(type) ? type : [type];
|
|
11
|
+
if (!types.some((t) => matchesType(t, value))) {
|
|
12
|
+
errors.push(`${path}: expected type ${types.join("|")}`);
|
|
13
|
+
return errors;
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
if (value === null || value === undefined) {
|
|
17
|
+
return errors;
|
|
18
|
+
}
|
|
19
|
+
if (schema.enum && Array.isArray(schema.enum) && !schema.enum.includes(value)) {
|
|
20
|
+
errors.push(`${path}: value not in enum`);
|
|
21
|
+
}
|
|
22
|
+
if (typeof value === "object" && !Array.isArray(value) && schema.properties) {
|
|
23
|
+
const obj = value;
|
|
24
|
+
const props = schema.properties;
|
|
25
|
+
const required = Array.isArray(schema.required) ? schema.required : [];
|
|
26
|
+
for (const key of required) {
|
|
27
|
+
if (!(key in obj))
|
|
28
|
+
errors.push(`${path}.${key}: required`);
|
|
29
|
+
}
|
|
30
|
+
for (const [key, childSchema] of Object.entries(props)) {
|
|
31
|
+
if (key in obj) {
|
|
32
|
+
errors.push(...validateAgainstSchema(childSchema, obj[key], `${path}.${key}`));
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
if (Array.isArray(value) && schema.items && typeof schema.items === "object") {
|
|
37
|
+
for (let i = 0; i < value.length; i += 1) {
|
|
38
|
+
errors.push(...validateAgainstSchema(schema.items, value[i], `${path}[${i}]`));
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
return errors;
|
|
42
|
+
}
|
|
43
|
+
function matchesType(type, value) {
|
|
44
|
+
switch (type) {
|
|
45
|
+
case "string":
|
|
46
|
+
return typeof value === "string";
|
|
47
|
+
case "integer":
|
|
48
|
+
return typeof value === "number" && Number.isInteger(value);
|
|
49
|
+
case "number":
|
|
50
|
+
return typeof value === "number" && Number.isFinite(value);
|
|
51
|
+
case "boolean":
|
|
52
|
+
return typeof value === "boolean";
|
|
53
|
+
case "object":
|
|
54
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
55
|
+
case "array":
|
|
56
|
+
return Array.isArray(value);
|
|
57
|
+
case "null":
|
|
58
|
+
return value === null;
|
|
59
|
+
default:
|
|
60
|
+
return true;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { Finding, Scores } from "../../types/index.js";
|
|
2
2
|
/**
|
|
3
3
|
* v1 readiness scores from post-dedupe findings.
|
|
4
|
-
* Spec: docs/scoring.md
|
|
4
|
+
* Spec: docs/reference/scoring.md
|
|
5
5
|
*/
|
|
6
6
|
export declare function computeReadinessScores(findings: readonly Finding[]): Scores;
|
|
@@ -10,7 +10,7 @@ const SEVERITY_SORT_RANK = {
|
|
|
10
10
|
};
|
|
11
11
|
/**
|
|
12
12
|
* v1 readiness scores from post-dedupe findings.
|
|
13
|
-
* Spec: docs/scoring.md
|
|
13
|
+
* Spec: docs/reference/scoring.md
|
|
14
14
|
*/
|
|
15
15
|
export function computeReadinessScores(findings) {
|
|
16
16
|
const overallRaw = 100 - sumDeductions(findings);
|
|
@@ -29,6 +29,10 @@ export function computeReadinessScores(findings) {
|
|
|
29
29
|
cursor: scoreSubset(findings.filter((f) => f.affectedAgents.includes("cursor"))),
|
|
30
30
|
"claude-code": scoreSubset(findings.filter((f) => f.affectedAgents.includes("claude-code"))),
|
|
31
31
|
codex: scoreSubset(findings.filter((f) => f.affectedAgents.includes("codex"))),
|
|
32
|
+
copilot: scoreSubset(findings.filter((f) => f.affectedAgents.includes("copilot"))),
|
|
33
|
+
windsurf: scoreSubset(findings.filter((f) => f.affectedAgents.includes("windsurf"))),
|
|
34
|
+
"gemini-cli": scoreSubset(findings.filter((f) => f.affectedAgents.includes("gemini-cli"))),
|
|
35
|
+
aider: scoreSubset(findings.filter((f) => f.affectedAgents.includes("aider"))),
|
|
32
36
|
},
|
|
33
37
|
};
|
|
34
38
|
}
|
|
@@ -2,6 +2,6 @@ import type { Scores } from "../../types/index.js";
|
|
|
2
2
|
/**
|
|
3
3
|
* Historical filesScanned-based stub. Kept for unit tests that document the
|
|
4
4
|
* pre-v1 placeholder behavior. Production scans use computeReadinessScores
|
|
5
|
-
* (docs/scoring.md) — do not call this from scan().
|
|
5
|
+
* (docs/reference/scoring.md) — do not call this from scan().
|
|
6
6
|
*/
|
|
7
7
|
export declare function computePlaceholderScores(filesScanned: number): Scores;
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Historical filesScanned-based stub. Kept for unit tests that document the
|
|
3
3
|
* pre-v1 placeholder behavior. Production scans use computeReadinessScores
|
|
4
|
-
* (docs/scoring.md) — do not call this from scan().
|
|
4
|
+
* (docs/reference/scoring.md) — do not call this from scan().
|
|
5
5
|
*/
|
|
6
6
|
export function computePlaceholderScores(filesScanned) {
|
|
7
7
|
const base = 72;
|
|
@@ -21,6 +21,10 @@ export function computePlaceholderScores(filesScanned) {
|
|
|
21
21
|
cursor: clamp(overall - 5),
|
|
22
22
|
"claude-code": clamp(overall - 8),
|
|
23
23
|
codex: clamp(overall - 6),
|
|
24
|
+
copilot: clamp(overall - 7),
|
|
25
|
+
windsurf: clamp(overall - 6),
|
|
26
|
+
"gemini-cli": clamp(overall - 5),
|
|
27
|
+
aider: clamp(overall - 7),
|
|
24
28
|
},
|
|
25
29
|
};
|
|
26
30
|
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
export interface SecretFinding {
|
|
2
|
+
file: string;
|
|
3
|
+
line: number;
|
|
4
|
+
ruleId: string;
|
|
5
|
+
/** Always redacted — never contains the secret value */
|
|
6
|
+
redactedSnippet: string;
|
|
7
|
+
severity: "critical" | "warning";
|
|
8
|
+
}
|
|
9
|
+
export interface SecretScanReport {
|
|
10
|
+
root: string;
|
|
11
|
+
enabled: boolean;
|
|
12
|
+
filesScanned: number;
|
|
13
|
+
findings: SecretFinding[];
|
|
14
|
+
limitations: string[];
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Opt-in content secret scan. Findings never include raw secret values.
|
|
18
|
+
*/
|
|
19
|
+
export declare function scanSecrets(options: {
|
|
20
|
+
root: string;
|
|
21
|
+
enabled: boolean;
|
|
22
|
+
maxFiles?: number;
|
|
23
|
+
}): Promise<SecretScanReport>;
|