@praneeth_54/agentdoctor 0.3.0-beta → 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +76 -1
- package/README.md +60 -30
- package/dist/agents/inspect.js +58 -51
- package/dist/cli/commands/explain.js +1 -1
- package/dist/cli/commands/fix.js +22 -6
- package/dist/cli/commands/scan.d.ts +5 -0
- package/dist/cli/commands/scan.js +25 -6
- package/dist/cli/commands/verify.d.ts +6 -0
- package/dist/cli/commands/verify.js +25 -11
- package/dist/cli/program.js +48 -7
- package/dist/constants.d.ts +1 -1
- package/dist/constants.js +1 -1
- package/dist/core/fix/apply.d.ts +5 -1
- package/dist/core/fix/apply.js +34 -14
- package/dist/core/fix/plan.d.ts +1 -1
- package/dist/core/fix/plan.js +110 -3
- package/dist/core/fix/render.d.ts +2 -0
- package/dist/core/fix/render.js +57 -9
- package/dist/core/fix/run.js +5 -1
- package/dist/core/fix/types.d.ts +1 -1
- package/dist/core/fix/types.js +5 -1
- package/dist/core/fix/writers/claude-settings.d.ts +16 -0
- package/dist/core/fix/writers/claude-settings.js +100 -0
- package/dist/core/fix/writers/codex-config.d.ts +22 -0
- package/dist/core/fix/writers/codex-config.js +183 -0
- package/dist/core/fix/writers/cursorignore.js +2 -8
- package/dist/core/path-resolution/index.d.ts +5 -0
- package/dist/core/path-resolution/index.js +5 -0
- package/dist/core/path-resolution/prepare.d.ts +33 -0
- package/dist/core/path-resolution/prepare.js +98 -0
- package/dist/core/policy/evaluate.d.ts +44 -0
- package/dist/core/policy/evaluate.js +120 -0
- package/dist/core/rules/claude-deny.d.ts +8 -0
- package/dist/core/rules/claude-deny.js +39 -0
- package/dist/core/rules/codex-deny.d.ts +5 -0
- package/dist/core/rules/codex-deny.js +42 -0
- package/dist/core/rules/context/generated-directory.js +43 -9
- package/dist/core/rules/context/large-log-file.js +27 -10
- package/dist/core/rules/ignore.js +2 -5
- package/dist/core/rules/instructions/missing-path-reference.d.ts +5 -1
- package/dist/core/rules/instructions/missing-path-reference.js +24 -6
- package/dist/core/rules/path-kind.d.ts +15 -2
- package/dist/core/rules/path-kind.js +113 -10
- package/dist/core/rules/security/env-file-exposure.js +9 -24
- package/dist/core/rules/text-cache.js +46 -40
- package/dist/discovery/files.js +7 -2
- package/dist/discovery/log-like.d.ts +2 -0
- package/dist/discovery/log-like.js +7 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +1 -0
- package/dist/reporters/github/annotations.d.ts +8 -0
- package/dist/reporters/github/annotations.js +42 -0
- package/dist/reporters/github/emit.d.ts +23 -0
- package/dist/reporters/github/emit.js +31 -0
- package/dist/reporters/github/summary.d.ts +20 -0
- package/dist/reporters/github/summary.js +111 -0
- package/dist/reporters/terminal/report.d.ts +5 -0
- package/dist/reporters/terminal/report.js +68 -5
- package/dist/reporters/verify/terminal.d.ts +4 -0
- package/dist/reporters/verify/terminal.js +26 -0
- package/dist/utils/fs.d.ts +5 -0
- package/dist/utils/fs.js +34 -0
- package/dist/utils/path.d.ts +5 -0
- package/dist/utils/path.js +16 -0
- package/package.json +11 -1
package/dist/cli/program.js
CHANGED
|
@@ -1,15 +1,16 @@
|
|
|
1
|
-
import { Command } from "commander";
|
|
1
|
+
import { Command, InvalidArgumentError } from "commander";
|
|
2
2
|
import { PACKAGE_VERSION } from "../constants.js";
|
|
3
|
+
import { parseFailOnRules, parseSeverityGate } from "../core/policy/evaluate.js";
|
|
4
|
+
import { EXIT_CODES } from "../types/index.js";
|
|
3
5
|
import { runDoctorCommand } from "./commands/doctor.js";
|
|
4
6
|
import { runExplainCommand } from "./commands/explain.js";
|
|
5
7
|
import { runFixCommand } from "./commands/fix.js";
|
|
6
8
|
import { resolveTargetArgument, runScanCommand } from "./commands/scan.js";
|
|
7
9
|
import { runVerifyCommand } from "./commands/verify.js";
|
|
8
|
-
import { EXIT_CODES } from "../types/index.js";
|
|
9
10
|
function parseMinScore(value) {
|
|
10
11
|
const parsed = Number(value);
|
|
11
12
|
if (!Number.isFinite(parsed) || parsed < 0 || parsed > 100) {
|
|
12
|
-
throw new
|
|
13
|
+
throw new InvalidArgumentError("--min-score must be a number between 0 and 100");
|
|
13
14
|
}
|
|
14
15
|
return parsed;
|
|
15
16
|
}
|
|
@@ -22,6 +23,24 @@ function readMinScore(options) {
|
|
|
22
23
|
}
|
|
23
24
|
return undefined;
|
|
24
25
|
}
|
|
26
|
+
function collectFailOnRules(value, previous) {
|
|
27
|
+
return [...previous, ...parseFailOnRules(value)];
|
|
28
|
+
}
|
|
29
|
+
function readFailOnSeverity(options) {
|
|
30
|
+
if (typeof options.failOnSeverity !== "string" || options.failOnSeverity.length === 0) {
|
|
31
|
+
return undefined;
|
|
32
|
+
}
|
|
33
|
+
return parseSeverityGate(options.failOnSeverity);
|
|
34
|
+
}
|
|
35
|
+
function readFailOnRules(options) {
|
|
36
|
+
if (Array.isArray(options.failOnRule)) {
|
|
37
|
+
return options.failOnRule.filter((item) => typeof item === "string");
|
|
38
|
+
}
|
|
39
|
+
if (typeof options.failOnRule === "string") {
|
|
40
|
+
return parseFailOnRules(options.failOnRule);
|
|
41
|
+
}
|
|
42
|
+
return [];
|
|
43
|
+
}
|
|
25
44
|
/**
|
|
26
45
|
* Scan flags are declared on both the root program and the `scan` subcommand
|
|
27
46
|
* so `--help` stays accurate. Commander stores overlapping flags on the parent
|
|
@@ -30,19 +49,29 @@ function readMinScore(options) {
|
|
|
30
49
|
function addScanOptions(command) {
|
|
31
50
|
return command
|
|
32
51
|
.option("--json", "Emit machine-readable JSON (no decorative output)", false)
|
|
33
|
-
.option("--ci", "CI mode (
|
|
52
|
+
.option("--ci", "CI mode: exit 1 when any critical finding exists (override with --fail-on-severity)", false)
|
|
34
53
|
.option("--verbose", "Show timing and extra diagnostics", false)
|
|
35
|
-
.option("--min-score <number>", "Exit 1 when overall readiness score is below this (0-100)", parseMinScore)
|
|
54
|
+
.option("--min-score <number>", "Exit 1 when overall readiness score is below this (0-100)", parseMinScore)
|
|
55
|
+
.option("--fail-on-severity <level>", "Exit 1 when any finding has this severity or higher (critical|warning|info)", parseSeverityGate)
|
|
56
|
+
.option("--fail-on-rule <id>", "Exit 1 when a finding matches this rule id (repeatable or comma-separated)", collectFailOnRules, [])
|
|
57
|
+
.option("--summary", "Write a GitHub Actions step summary when GITHUB_STEP_SUMMARY is set", false)
|
|
58
|
+
.option("--annotations", "Emit GitHub Actions annotations for findings (stderr)", false);
|
|
36
59
|
}
|
|
37
60
|
async function runScanFromCli(pathArg, command) {
|
|
38
61
|
const options = command.optsWithGlobals();
|
|
39
62
|
const minScore = readMinScore(options);
|
|
63
|
+
const failOnSeverity = readFailOnSeverity(options);
|
|
64
|
+
const failOnRules = readFailOnRules(options);
|
|
40
65
|
const code = await runScanCommand({
|
|
41
66
|
targetPath: resolveTargetArgument(pathArg),
|
|
42
67
|
json: Boolean(options.json),
|
|
43
68
|
ci: Boolean(options.ci),
|
|
44
69
|
verbose: Boolean(options.verbose),
|
|
70
|
+
summary: Boolean(options.summary),
|
|
71
|
+
annotations: Boolean(options.annotations),
|
|
45
72
|
...(minScore !== undefined ? { minScore } : {}),
|
|
73
|
+
...(failOnSeverity !== undefined ? { failOnSeverity } : {}),
|
|
74
|
+
...(failOnRules.length > 0 ? { failOnRules } : {}),
|
|
46
75
|
});
|
|
47
76
|
process.exitCode = code;
|
|
48
77
|
}
|
|
@@ -63,7 +92,7 @@ export function createProgram() {
|
|
|
63
92
|
});
|
|
64
93
|
program
|
|
65
94
|
.command("fix")
|
|
66
|
-
.description("Apply safe automatic fixes (Cursor .cursorignore for safe context findings)")
|
|
95
|
+
.description("Apply safe automatic fixes (Cursor .cursorignore, Claude Code Read deny, and Codex filesystem deny for safe context findings)")
|
|
67
96
|
.argument("[path]", "Repository path (default: current directory)")
|
|
68
97
|
.option("--dry-run", "Show proposed fixes without writing files", false)
|
|
69
98
|
.option("-y, --yes", "Skip confirmation prompts", false)
|
|
@@ -80,20 +109,32 @@ export function createProgram() {
|
|
|
80
109
|
.description("Re-scan and compare against a prior scan JSON baseline (Scan → Fix → Verify)")
|
|
81
110
|
.argument("[path]", "Repository path (default: current directory)")
|
|
82
111
|
.option("--json", "Emit machine-readable JSON", false)
|
|
83
|
-
.option("--ci", "CI mode: exit 1 when new findings appear (also honors
|
|
112
|
+
.option("--ci", "CI mode: exit 1 when new findings appear (also honors other policy flags)", false)
|
|
113
|
+
.option("--fail-on-new", "Exit 1 when new findings appear relative to the baseline", false)
|
|
84
114
|
.option("--verbose", "Show timing and extra diagnostics", false)
|
|
85
115
|
.option("--baseline <file>", "Prior scan JSON report (default: agentdoctor-report.json or .agentdoctor-baseline.json)")
|
|
86
116
|
.option("--min-score <number>", "Exit 1 when overall readiness score is below this (0-100)", parseMinScore)
|
|
117
|
+
.option("--fail-on-severity <level>", "Exit 1 when any finding has this severity or higher (critical|warning|info)", parseSeverityGate)
|
|
118
|
+
.option("--fail-on-rule <id>", "Exit 1 when a finding matches this rule id (repeatable or comma-separated)", collectFailOnRules, [])
|
|
119
|
+
.option("--summary", "Write a GitHub Actions step summary when GITHUB_STEP_SUMMARY is set", false)
|
|
120
|
+
.option("--annotations", "Emit GitHub Actions annotations for findings (stderr)", false)
|
|
87
121
|
.action(async (pathArg, _options, command) => {
|
|
88
122
|
const options = command.optsWithGlobals();
|
|
89
123
|
const minScore = readMinScore(options);
|
|
124
|
+
const failOnSeverity = readFailOnSeverity(options);
|
|
125
|
+
const failOnRules = readFailOnRules(options);
|
|
90
126
|
const code = await runVerifyCommand({
|
|
91
127
|
targetPath: resolveTargetArgument(pathArg),
|
|
92
128
|
json: Boolean(options.json),
|
|
93
129
|
ci: Boolean(options.ci),
|
|
130
|
+
failOnNew: Boolean(options.failOnNew),
|
|
94
131
|
verbose: Boolean(options.verbose),
|
|
132
|
+
summary: Boolean(options.summary),
|
|
133
|
+
annotations: Boolean(options.annotations),
|
|
95
134
|
...(typeof options.baseline === "string" ? { baselinePath: options.baseline } : {}),
|
|
96
135
|
...(minScore !== undefined ? { minScore } : {}),
|
|
136
|
+
...(failOnSeverity !== undefined ? { failOnSeverity } : {}),
|
|
137
|
+
...(failOnRules.length > 0 ? { failOnRules } : {}),
|
|
97
138
|
});
|
|
98
139
|
process.exitCode = code;
|
|
99
140
|
});
|
package/dist/constants.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export declare const PACKAGE_VERSION = "0.
|
|
1
|
+
export declare const PACKAGE_VERSION = "1.0.0";
|
|
2
2
|
export declare const DEFAULT_MAX_FILE_SIZE_BYTES: number;
|
|
3
3
|
/** Directories skipped during normal discovery (unless a rule needs them later). */
|
|
4
4
|
export declare const DEFAULT_IGNORE_DIRECTORIES: Set<string>;
|
package/dist/constants.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export const PACKAGE_VERSION = "0.
|
|
1
|
+
export const PACKAGE_VERSION = "1.0.0";
|
|
2
2
|
export const DEFAULT_MAX_FILE_SIZE_BYTES = 2 * 1024 * 1024; // 2 MiB
|
|
3
3
|
/** Directories skipped during normal discovery (unless a rule needs them later). */
|
|
4
4
|
export const DEFAULT_IGNORE_DIRECTORIES = new Set([
|
package/dist/core/fix/apply.d.ts
CHANGED
|
@@ -1,10 +1,14 @@
|
|
|
1
1
|
import type { FixApplyResult, FixPlan } from "./types.js";
|
|
2
|
+
import { previewClaudeSettingsActions, readClaudeSettings } from "./writers/claude-settings.js";
|
|
3
|
+
import { previewCodexConfigActions, readCodexConfig } from "./writers/codex-config.js";
|
|
2
4
|
import { previewCursorignoreActions, readCursorignore } from "./writers/cursorignore.js";
|
|
3
5
|
/**
|
|
4
6
|
* Apply (or dry-run) a fix plan.
|
|
5
|
-
*
|
|
7
|
+
* Safe writers: `.cursorignore`, Claude settings deny, Codex config.toml deny.
|
|
6
8
|
*/
|
|
7
9
|
export declare function applyFixPlan(plan: FixPlan, options: {
|
|
8
10
|
dryRun: boolean;
|
|
9
11
|
}): Promise<FixApplyResult>;
|
|
10
12
|
export { previewCursorignoreActions, readCursorignore };
|
|
13
|
+
export { previewClaudeSettingsActions, readClaudeSettings };
|
|
14
|
+
export { previewCodexConfigActions, readCodexConfig };
|
package/dist/core/fix/apply.js
CHANGED
|
@@ -1,27 +1,47 @@
|
|
|
1
|
+
import { previewClaudeSettingsActions, readClaudeSettings, writeClaudeSettings, } from "./writers/claude-settings.js";
|
|
2
|
+
import { previewCodexConfigActions, readCodexConfig, writeCodexConfig, } from "./writers/codex-config.js";
|
|
1
3
|
import { previewCursorignoreActions, readCursorignore, writeCursorignore, } from "./writers/cursorignore.js";
|
|
2
4
|
/**
|
|
3
5
|
* Apply (or dry-run) a fix plan.
|
|
4
|
-
*
|
|
6
|
+
* Safe writers: `.cursorignore`, Claude settings deny, Codex config.toml deny.
|
|
5
7
|
*/
|
|
6
8
|
export async function applyFixPlan(plan, options) {
|
|
7
|
-
const
|
|
8
|
-
const
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
9
|
+
const cursorContent = await readCursorignore(plan.root);
|
|
10
|
+
const claudeContent = await readClaudeSettings(plan.root);
|
|
11
|
+
const codexContent = await readCodexConfig(plan.root);
|
|
12
|
+
const cursorPreview = previewCursorignoreActions(cursorContent, plan.actions);
|
|
13
|
+
const claudePreview = previewClaudeSettingsActions(claudeContent, plan.actions);
|
|
14
|
+
const codexPreview = previewCodexConfigActions(codexContent, plan.actions);
|
|
15
|
+
const writtenFiles = [];
|
|
16
|
+
const changedFiles = [];
|
|
17
|
+
if (cursorPreview) {
|
|
18
|
+
changedFiles.push(cursorPreview.targetRelativePath);
|
|
19
|
+
if (!options.dryRun) {
|
|
20
|
+
await writeCursorignore(plan.root, cursorPreview.after);
|
|
21
|
+
writtenFiles.push(cursorPreview.targetRelativePath);
|
|
22
|
+
}
|
|
16
23
|
}
|
|
17
|
-
if (
|
|
18
|
-
|
|
24
|
+
if (claudePreview) {
|
|
25
|
+
changedFiles.push(claudePreview.targetRelativePath);
|
|
26
|
+
if (!options.dryRun) {
|
|
27
|
+
await writeClaudeSettings(plan.root, claudePreview.after);
|
|
28
|
+
writtenFiles.push(claudePreview.targetRelativePath);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
if (codexPreview) {
|
|
32
|
+
changedFiles.push(codexPreview.targetRelativePath);
|
|
33
|
+
if (!options.dryRun) {
|
|
34
|
+
await writeCodexConfig(plan.root, codexPreview.after);
|
|
35
|
+
writtenFiles.push(codexPreview.targetRelativePath);
|
|
36
|
+
}
|
|
19
37
|
}
|
|
20
38
|
return {
|
|
21
39
|
plan,
|
|
22
40
|
dryRun: options.dryRun,
|
|
23
|
-
writtenFiles
|
|
24
|
-
changedFiles
|
|
41
|
+
writtenFiles,
|
|
42
|
+
changedFiles,
|
|
25
43
|
};
|
|
26
44
|
}
|
|
27
45
|
export { previewCursorignoreActions, readCursorignore };
|
|
46
|
+
export { previewClaudeSettingsActions, readClaudeSettings };
|
|
47
|
+
export { previewCodexConfigActions, readCodexConfig };
|
package/dist/core/fix/plan.d.ts
CHANGED
|
@@ -2,7 +2,7 @@ import type { ScanResult } from "../../types/index.js";
|
|
|
2
2
|
import type { FixPlan } from "./types.js";
|
|
3
3
|
/**
|
|
4
4
|
* Build a fix plan from a scan result.
|
|
5
|
-
*
|
|
5
|
+
* Safe context exclusions: Cursor `.cursorignore`, Claude Code Read deny, Codex filesystem deny.
|
|
6
6
|
*/
|
|
7
7
|
export declare function buildFixPlan(result: ScanResult): Promise<FixPlan>;
|
|
8
8
|
/** Patterns that would be newly appended given current file contents. */
|
package/dist/core/fix/plan.js
CHANGED
|
@@ -1,11 +1,21 @@
|
|
|
1
1
|
import path from "node:path";
|
|
2
2
|
import { readTextFile } from "../../utils/fs.js";
|
|
3
|
+
import { settingsTextDeniesPath } from "../rules/claude-deny.js";
|
|
4
|
+
import { configTextDeniesPath } from "../rules/codex-deny.js";
|
|
3
5
|
import { createIgnoreIndex, parseIgnoreFile, pathMatchesAny } from "../rules/ignore.js";
|
|
4
6
|
import { isSafeContextFixRule, patternForFinding } from "./patterns.js";
|
|
7
|
+
import { denyRuleForFixPattern } from "./writers/claude-settings.js";
|
|
8
|
+
import { assertWritableCodexConfig, denyKeyForFixPattern } from "./writers/codex-config.js";
|
|
5
9
|
const DEFAULT_MAX_IGNORE_BYTES = 512 * 1024;
|
|
6
10
|
function cursorConfigured(result) {
|
|
7
11
|
return result.agents.some((a) => a.id === "cursor" && (a.detected || a.configured));
|
|
8
12
|
}
|
|
13
|
+
function claudeConfigured(result) {
|
|
14
|
+
return result.agents.some((a) => a.id === "claude-code" && (a.detected || a.configured));
|
|
15
|
+
}
|
|
16
|
+
function codexConfigured(result) {
|
|
17
|
+
return result.agents.some((a) => a.id === "codex" && (a.detected || a.configured));
|
|
18
|
+
}
|
|
9
19
|
async function loadCursorignorePatterns(root) {
|
|
10
20
|
const filePath = path.join(root, ".cursorignore");
|
|
11
21
|
const text = await readTextFile(filePath, DEFAULT_MAX_IGNORE_BYTES);
|
|
@@ -14,9 +24,15 @@ async function loadCursorignorePatterns(root) {
|
|
|
14
24
|
}
|
|
15
25
|
return parseIgnoreFile(text);
|
|
16
26
|
}
|
|
27
|
+
async function loadClaudeSettingsText(root) {
|
|
28
|
+
return readTextFile(path.join(root, ".claude/settings.json"), DEFAULT_MAX_IGNORE_BYTES);
|
|
29
|
+
}
|
|
30
|
+
async function loadCodexConfigText(root) {
|
|
31
|
+
return readTextFile(path.join(root, ".codex/config.toml"), DEFAULT_MAX_IGNORE_BYTES);
|
|
32
|
+
}
|
|
17
33
|
/**
|
|
18
34
|
* Build a fix plan from a scan result.
|
|
19
|
-
*
|
|
35
|
+
* Safe context exclusions: Cursor `.cursorignore`, Claude Code Read deny, Codex filesystem deny.
|
|
20
36
|
*/
|
|
21
37
|
export async function buildFixPlan(result) {
|
|
22
38
|
const root = result.repository.root;
|
|
@@ -27,7 +43,11 @@ export async function buildFixPlan(result) {
|
|
|
27
43
|
gitignorePatterns: [],
|
|
28
44
|
cursorignorePatterns: existingCursorPatterns,
|
|
29
45
|
});
|
|
46
|
+
const claudeSettingsText = await loadClaudeSettingsText(root);
|
|
47
|
+
const codexConfigText = await loadCodexConfigText(root);
|
|
30
48
|
const hasCursor = cursorConfigured(result);
|
|
49
|
+
const hasClaude = claudeConfigured(result);
|
|
50
|
+
const hasCodex = codexConfigured(result);
|
|
31
51
|
for (const finding of result.findings) {
|
|
32
52
|
if (finding.fixability !== "safe") {
|
|
33
53
|
skipped.push({
|
|
@@ -60,6 +80,8 @@ export async function buildFixPlan(result) {
|
|
|
60
80
|
continue;
|
|
61
81
|
}
|
|
62
82
|
const wantsCursor = finding.affectedAgents.includes("cursor");
|
|
83
|
+
const wantsClaude = finding.affectedAgents.includes("claude-code");
|
|
84
|
+
const wantsCodex = finding.affectedAgents.includes("codex");
|
|
63
85
|
if (wantsCursor && hasCursor) {
|
|
64
86
|
if (cursorIndex.matchesCursorignore(evidencePath) ||
|
|
65
87
|
cursorIndex.matchesCursorignore(pattern)) {
|
|
@@ -80,8 +102,54 @@ export async function buildFixPlan(result) {
|
|
|
80
102
|
reason: "Cursor not detected; skipping .cursorignore fix",
|
|
81
103
|
});
|
|
82
104
|
}
|
|
105
|
+
if (wantsClaude && hasClaude) {
|
|
106
|
+
const denyRule = denyRuleForFixPattern(pattern, evidencePath);
|
|
107
|
+
if (claudeSettingsText && settingsTextDeniesPath(claudeSettingsText, evidencePath)) {
|
|
108
|
+
skipped.push({
|
|
109
|
+
findingId: finding.id,
|
|
110
|
+
ruleId: finding.ruleId,
|
|
111
|
+
reason: "Already excluded by Claude Code Read deny",
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
else {
|
|
115
|
+
mergeClaudeAction(actionsByKey, finding, pattern, evidencePath, denyRule);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
else if (wantsClaude && !hasClaude) {
|
|
119
|
+
skipped.push({
|
|
120
|
+
findingId: finding.id,
|
|
121
|
+
ruleId: finding.ruleId,
|
|
122
|
+
reason: "Claude Code not detected; skipping settings deny fix",
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
if (wantsCodex && hasCodex) {
|
|
126
|
+
const denyKey = denyKeyForFixPattern(pattern, evidencePath);
|
|
127
|
+
if (codexConfigText && configTextDeniesPath(codexConfigText, evidencePath)) {
|
|
128
|
+
skipped.push({
|
|
129
|
+
findingId: finding.id,
|
|
130
|
+
ruleId: finding.ruleId,
|
|
131
|
+
reason: "Already excluded by Codex filesystem deny",
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
else if (codexConfigText) {
|
|
135
|
+
// Refuse unwritable/invalid config the same way Claude Fix refuses bad JSON —
|
|
136
|
+
// do not silently skip and leave Codex findings uncleared after a partial Cursor write.
|
|
137
|
+
assertWritableCodexConfig(codexConfigText);
|
|
138
|
+
mergeCodexAction(actionsByKey, finding, pattern, evidencePath, denyKey);
|
|
139
|
+
}
|
|
140
|
+
else {
|
|
141
|
+
mergeCodexAction(actionsByKey, finding, pattern, evidencePath, denyKey);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
else if (wantsCodex && !hasCodex) {
|
|
145
|
+
skipped.push({
|
|
146
|
+
findingId: finding.id,
|
|
147
|
+
ruleId: finding.ruleId,
|
|
148
|
+
reason: "Codex not detected; skipping config.toml deny fix",
|
|
149
|
+
});
|
|
150
|
+
}
|
|
83
151
|
for (const agent of finding.affectedAgents) {
|
|
84
|
-
if (agent === "cursor") {
|
|
152
|
+
if (agent === "cursor" || agent === "claude-code" || agent === "codex") {
|
|
85
153
|
continue;
|
|
86
154
|
}
|
|
87
155
|
skipped.push({
|
|
@@ -117,6 +185,46 @@ function mergeCursorAction(actionsByKey, finding, pattern, evidencePath) {
|
|
|
117
185
|
description: `Exclude ${evidencePath} from Cursor agent context via .cursorignore`,
|
|
118
186
|
});
|
|
119
187
|
}
|
|
188
|
+
function mergeClaudeAction(actionsByKey, finding, pattern, evidencePath, denyRule) {
|
|
189
|
+
const key = `claude-code:.claude/settings.json:${denyRule}`;
|
|
190
|
+
const existing = actionsByKey.get(key);
|
|
191
|
+
if (existing) {
|
|
192
|
+
if (!existing.findingIds.includes(finding.id)) {
|
|
193
|
+
existing.findingIds.push(finding.id);
|
|
194
|
+
}
|
|
195
|
+
return;
|
|
196
|
+
}
|
|
197
|
+
actionsByKey.set(key, {
|
|
198
|
+
id: key,
|
|
199
|
+
kind: "append-ignore-pattern",
|
|
200
|
+
agent: "claude-code",
|
|
201
|
+
targetRelativePath: ".claude/settings.json",
|
|
202
|
+
pattern,
|
|
203
|
+
evidencePath,
|
|
204
|
+
findingIds: [finding.id],
|
|
205
|
+
description: `Exclude ${evidencePath} from Claude Code via permissions.deny ${denyRule}`,
|
|
206
|
+
});
|
|
207
|
+
}
|
|
208
|
+
function mergeCodexAction(actionsByKey, finding, pattern, evidencePath, denyKey) {
|
|
209
|
+
const key = `codex:.codex/config.toml:${denyKey}`;
|
|
210
|
+
const existing = actionsByKey.get(key);
|
|
211
|
+
if (existing) {
|
|
212
|
+
if (!existing.findingIds.includes(finding.id)) {
|
|
213
|
+
existing.findingIds.push(finding.id);
|
|
214
|
+
}
|
|
215
|
+
return;
|
|
216
|
+
}
|
|
217
|
+
actionsByKey.set(key, {
|
|
218
|
+
id: key,
|
|
219
|
+
kind: "append-ignore-pattern",
|
|
220
|
+
agent: "codex",
|
|
221
|
+
targetRelativePath: ".codex/config.toml",
|
|
222
|
+
pattern,
|
|
223
|
+
evidencePath,
|
|
224
|
+
findingIds: [finding.id],
|
|
225
|
+
description: `Exclude ${evidencePath} from Codex via filesystem deny "${denyKey}"`,
|
|
226
|
+
});
|
|
227
|
+
}
|
|
120
228
|
/** Patterns that would be newly appended given current file contents. */
|
|
121
229
|
export function missingPatternsForCursorignore(currentContent, patterns) {
|
|
122
230
|
const existing = currentContent === null ? [] : parseIgnoreFile(currentContent);
|
|
@@ -129,7 +237,6 @@ export function missingPatternsForCursorignore(currentContent, patterns) {
|
|
|
129
237
|
existing.some((p) => p === pattern)) {
|
|
130
238
|
continue;
|
|
131
239
|
}
|
|
132
|
-
// Also skip if an existing pattern already excludes this path
|
|
133
240
|
const evidenceGuess = pattern.endsWith("/") ? pattern.slice(0, -1) : pattern;
|
|
134
241
|
if (pathMatchesAny(evidenceGuess, existing) || pathMatchesAny(pattern, existing)) {
|
|
135
242
|
continue;
|
|
@@ -2,5 +2,7 @@ import type { FixApplyResult, FixPlan } from "./types.js";
|
|
|
2
2
|
export declare function renderFixPlanTerminal(plan: FixPlan, options: {
|
|
3
3
|
dryRun: boolean;
|
|
4
4
|
cursorContent: string | null;
|
|
5
|
+
claudeSettingsContent?: string | null;
|
|
6
|
+
codexConfigContent?: string | null;
|
|
5
7
|
applyResult?: FixApplyResult;
|
|
6
8
|
}): string;
|
package/dist/core/fix/render.js
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { previewClaudeSettingsActions } from "./writers/claude-settings.js";
|
|
2
|
+
import { previewCodexConfigActions } from "./writers/codex-config.js";
|
|
1
3
|
import { previewCursorignoreActions } from "./writers/cursorignore.js";
|
|
2
4
|
export function renderFixPlanTerminal(plan, options) {
|
|
3
5
|
const lines = [];
|
|
@@ -14,10 +16,16 @@ export function renderFixPlanTerminal(plan, options) {
|
|
|
14
16
|
lines.push(` - ${reason} (${count})`);
|
|
15
17
|
}
|
|
16
18
|
lines.push("");
|
|
17
|
-
lines.push("
|
|
19
|
+
lines.push("Next");
|
|
20
|
+
lines.push(" Inspect a finding: agentdoctor explain <rule-id> (ids in scan --json)");
|
|
21
|
+
lines.push(" Confirm with verify: agentdoctor verify --baseline agentdoctor-report.json");
|
|
22
|
+
lines.push(" No baseline yet? agentdoctor scan --json > agentdoctor-report.json");
|
|
18
23
|
}
|
|
19
24
|
else {
|
|
20
25
|
lines.push(" Scan found nothing that Fix can change.");
|
|
26
|
+
lines.push("");
|
|
27
|
+
lines.push("Next");
|
|
28
|
+
lines.push(" Re-scan: agentdoctor scan");
|
|
21
29
|
}
|
|
22
30
|
lines.push("");
|
|
23
31
|
return lines.join("\n");
|
|
@@ -28,14 +36,41 @@ export function renderFixPlanTerminal(plan, options) {
|
|
|
28
36
|
lines.push(` pattern: ${action.pattern}`);
|
|
29
37
|
lines.push(` findings: ${action.findingIds.length}`);
|
|
30
38
|
}
|
|
31
|
-
const
|
|
32
|
-
|
|
39
|
+
const cursorPreview = previewCursorignoreActions(options.cursorContent, plan.actions);
|
|
40
|
+
const claudePreview = previewClaudeSettingsActions(options.claudeSettingsContent ?? null, plan.actions);
|
|
41
|
+
const codexPreview = previewCodexConfigActions(options.codexConfigContent ?? null, plan.actions);
|
|
42
|
+
if (cursorPreview || claudePreview || codexPreview) {
|
|
33
43
|
lines.push("");
|
|
34
44
|
lines.push(" File changes:");
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
lines.push(
|
|
45
|
+
let shown = false;
|
|
46
|
+
if (cursorPreview) {
|
|
47
|
+
lines.push(` ${cursorPreview.targetRelativePath} (+${cursorPreview.patternsToAdd.length} pattern(s))`);
|
|
48
|
+
lines.push("");
|
|
49
|
+
for (const previewLine of cursorPreview.preview.split("\n")) {
|
|
50
|
+
lines.push(` ${previewLine}`);
|
|
51
|
+
}
|
|
52
|
+
shown = true;
|
|
53
|
+
}
|
|
54
|
+
if (claudePreview) {
|
|
55
|
+
if (shown) {
|
|
56
|
+
lines.push("");
|
|
57
|
+
}
|
|
58
|
+
lines.push(` ${claudePreview.targetRelativePath} (+${claudePreview.denyRulesToAdd.length} deny rule(s))`);
|
|
59
|
+
lines.push("");
|
|
60
|
+
for (const previewLine of claudePreview.preview.split("\n")) {
|
|
61
|
+
lines.push(` ${previewLine}`);
|
|
62
|
+
}
|
|
63
|
+
shown = true;
|
|
64
|
+
}
|
|
65
|
+
if (codexPreview) {
|
|
66
|
+
if (shown) {
|
|
67
|
+
lines.push("");
|
|
68
|
+
}
|
|
69
|
+
lines.push(` ${codexPreview.targetRelativePath} (+${codexPreview.denyKeysToAdd.length} deny key(s))`);
|
|
70
|
+
lines.push("");
|
|
71
|
+
for (const previewLine of codexPreview.preview.split("\n")) {
|
|
72
|
+
lines.push(` ${previewLine}`);
|
|
73
|
+
}
|
|
39
74
|
}
|
|
40
75
|
}
|
|
41
76
|
if (options.applyResult && !options.dryRun) {
|
|
@@ -50,12 +85,25 @@ export function renderFixPlanTerminal(plan, options) {
|
|
|
50
85
|
else if (options.dryRun) {
|
|
51
86
|
lines.push("");
|
|
52
87
|
lines.push(" No files were modified (dry-run).");
|
|
53
|
-
lines.push(" Re-run without --dry-run to apply.");
|
|
54
88
|
}
|
|
55
89
|
if (plan.skipped.length > 0) {
|
|
56
90
|
lines.push("");
|
|
57
|
-
lines.push(` Skipped: ${plan.skipped.length} finding(s)
|
|
91
|
+
lines.push(` Skipped: ${plan.skipped.length} finding(s):`);
|
|
92
|
+
const reasons = summarizeSkipReasons(plan.skipped.map((s) => s.reason));
|
|
93
|
+
for (const [reason, count] of reasons) {
|
|
94
|
+
lines.push(` - ${reason} (${count})`);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
lines.push("");
|
|
98
|
+
lines.push("Next");
|
|
99
|
+
if (options.dryRun && plan.actions.length > 0) {
|
|
100
|
+
lines.push(" Apply these changes: agentdoctor fix -y");
|
|
101
|
+
}
|
|
102
|
+
if (plan.skipped.length > 0) {
|
|
103
|
+
lines.push(" Review/manual findings stay open — inspect with: agentdoctor explain <rule-id>");
|
|
58
104
|
}
|
|
105
|
+
lines.push(" Confirm with verify: agentdoctor verify --baseline agentdoctor-report.json");
|
|
106
|
+
lines.push(" No baseline yet? agentdoctor scan --json > agentdoctor-report.json");
|
|
59
107
|
lines.push("");
|
|
60
108
|
return lines.join("\n");
|
|
61
109
|
}
|
package/dist/core/fix/run.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import readline from "node:readline/promises";
|
|
2
2
|
import { stdin as input, stdout as output } from "node:process";
|
|
3
3
|
import { scan } from "../scanner/scan.js";
|
|
4
|
-
import { applyFixPlan, readCursorignore } from "./apply.js";
|
|
4
|
+
import { applyFixPlan, readClaudeSettings, readCodexConfig, readCursorignore } from "./apply.js";
|
|
5
5
|
import { buildFixPlan } from "./plan.js";
|
|
6
6
|
import { renderFixPlanTerminal } from "./render.js";
|
|
7
7
|
/**
|
|
@@ -11,6 +11,8 @@ export async function runFix(options) {
|
|
|
11
11
|
const result = await scan({ cwd: options.cwd });
|
|
12
12
|
const plan = await buildFixPlan(result);
|
|
13
13
|
const cursorContent = await readCursorignore(plan.root);
|
|
14
|
+
const claudeSettingsContent = await readClaudeSettings(plan.root);
|
|
15
|
+
const codexConfigContent = await readCodexConfig(plan.root);
|
|
14
16
|
if (plan.actions.length === 0 || options.dryRun) {
|
|
15
17
|
const applyResult = await applyFixPlan(plan, { dryRun: true });
|
|
16
18
|
return { plan, applyResult, cancelled: false };
|
|
@@ -19,6 +21,8 @@ export async function runFix(options) {
|
|
|
19
21
|
const previewText = renderFixPlanTerminal(plan, {
|
|
20
22
|
dryRun: true,
|
|
21
23
|
cursorContent,
|
|
24
|
+
claudeSettingsContent,
|
|
25
|
+
codexConfigContent,
|
|
22
26
|
});
|
|
23
27
|
process.stdout.write(previewText);
|
|
24
28
|
const confirmed = await confirm("Apply these changes?");
|
package/dist/core/fix/types.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { AgentId } from "../../types/index.js";
|
|
2
2
|
export type FixKind = "append-ignore-pattern";
|
|
3
3
|
/** Allowlisted relative paths fix may create or modify. */
|
|
4
|
-
export declare const FIX_PATH_ALLOWLIST: readonly [".cursorignore"];
|
|
4
|
+
export declare const FIX_PATH_ALLOWLIST: readonly [".cursorignore", ".claude/settings.json", ".codex/config.toml"];
|
|
5
5
|
export type FixAllowlistedPath = (typeof FIX_PATH_ALLOWLIST)[number];
|
|
6
6
|
export interface FixAction {
|
|
7
7
|
id: string;
|
package/dist/core/fix/types.js
CHANGED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { FixAction } from "../types.js";
|
|
2
|
+
declare const SETTINGS_RELATIVE = ".claude/settings.json";
|
|
3
|
+
export interface ClaudeSettingsWritePreview {
|
|
4
|
+
targetRelativePath: typeof SETTINGS_RELATIVE;
|
|
5
|
+
denyRulesToAdd: string[];
|
|
6
|
+
before: string;
|
|
7
|
+
after: string;
|
|
8
|
+
preview: string;
|
|
9
|
+
}
|
|
10
|
+
export declare function denyRuleForFixPattern(pattern: string, evidencePath: string): string;
|
|
11
|
+
export declare function missingClaudeDenyRules(currentContent: string | null, denyRules: string[]): string[];
|
|
12
|
+
export declare function buildClaudeSettingsContent(currentContent: string | null, denyRulesToAdd: string[]): string;
|
|
13
|
+
export declare function previewClaudeSettingsActions(currentContent: string | null, actions: FixAction[]): ClaudeSettingsWritePreview | null;
|
|
14
|
+
export declare function readClaudeSettings(root: string): Promise<string | null>;
|
|
15
|
+
export declare function writeClaudeSettings(root: string, content: string): Promise<void>;
|
|
16
|
+
export {};
|