@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.
- package/CHANGELOG.md +58 -0
- package/LICENSE +21 -0
- package/README.md +348 -0
- package/dist/agents/claude/adapter.d.ts +1 -0
- package/dist/agents/claude/adapter.js +1 -0
- package/dist/agents/claude/detector.d.ts +16 -0
- package/dist/agents/claude/detector.js +225 -0
- package/dist/agents/codex/adapter.d.ts +1 -0
- package/dist/agents/codex/adapter.js +1 -0
- package/dist/agents/codex/detector.d.ts +17 -0
- package/dist/agents/codex/detector.js +151 -0
- package/dist/agents/cursor/adapter.d.ts +1 -0
- package/dist/agents/cursor/adapter.js +1 -0
- package/dist/agents/cursor/detector.d.ts +17 -0
- package/dist/agents/cursor/detector.js +198 -0
- package/dist/agents/detect-agents.d.ts +17 -0
- package/dist/agents/detect-agents.js +23 -0
- package/dist/agents/inspect.d.ts +36 -0
- package/dist/agents/inspect.js +165 -0
- package/dist/agents/registry.d.ts +7 -0
- package/dist/agents/registry.js +11 -0
- package/dist/agents/types.d.ts +52 -0
- package/dist/agents/types.js +15 -0
- package/dist/cli/commands/doctor.d.ts +5 -0
- package/dist/cli/commands/doctor.js +22 -0
- package/dist/cli/commands/explain.d.ts +5 -0
- package/dist/cli/commands/explain.js +51 -0
- package/dist/cli/commands/fix.d.ts +8 -0
- package/dist/cli/commands/fix.js +19 -0
- package/dist/cli/commands/scan.d.ts +10 -0
- package/dist/cli/commands/scan.js +59 -0
- package/dist/cli/index.d.ts +2 -0
- package/dist/cli/index.js +3 -0
- package/dist/cli/program.d.ts +3 -0
- package/dist/cli/program.js +124 -0
- package/dist/constants.d.ts +5 -0
- package/dist/constants.js +38 -0
- package/dist/core/mcp/parse.d.ts +7 -0
- package/dist/core/mcp/parse.js +303 -0
- package/dist/core/mcp/types.d.ts +33 -0
- package/dist/core/mcp/types.js +1 -0
- package/dist/core/rules/build-context.d.ts +10 -0
- package/dist/core/rules/build-context.js +31 -0
- package/dist/core/rules/context/generated-directory.d.ts +2 -0
- package/dist/core/rules/context/generated-directory.js +89 -0
- package/dist/core/rules/context/large-instruction-file.d.ts +2 -0
- package/dist/core/rules/context/large-instruction-file.js +56 -0
- package/dist/core/rules/context/large-log-file.d.ts +2 -0
- package/dist/core/rules/context/large-log-file.js +45 -0
- package/dist/core/rules/dedupe.d.ts +13 -0
- package/dist/core/rules/dedupe.js +53 -0
- package/dist/core/rules/ignore.d.ts +23 -0
- package/dist/core/rules/ignore.js +97 -0
- package/dist/core/rules/instructions/duplicate-content.d.ts +2 -0
- package/dist/core/rules/instructions/duplicate-content.js +99 -0
- package/dist/core/rules/instructions/empty-instructions.d.ts +2 -0
- package/dist/core/rules/instructions/empty-instructions.js +53 -0
- package/dist/core/rules/instructions/missing-path-reference.d.ts +2 -0
- package/dist/core/rules/instructions/missing-path-reference.js +131 -0
- package/dist/core/rules/mcp/mcp-rules.d.ts +3 -0
- package/dist/core/rules/mcp/mcp-rules.js +83 -0
- package/dist/core/rules/registry.d.ts +6 -0
- package/dist/core/rules/registry.js +31 -0
- package/dist/core/rules/run-rules.d.ts +16 -0
- package/dist/core/rules/run-rules.js +36 -0
- package/dist/core/rules/security/claude-bypass-permissions.d.ts +7 -0
- package/dist/core/rules/security/claude-bypass-permissions.js +52 -0
- package/dist/core/rules/security/env-file-exposure.d.ts +2 -0
- package/dist/core/rules/security/env-file-exposure.js +87 -0
- package/dist/core/rules/security/mcp-broad-filesystem.d.ts +2 -0
- package/dist/core/rules/security/mcp-broad-filesystem.js +42 -0
- package/dist/core/rules/security/private-key-file.d.ts +2 -0
- package/dist/core/rules/security/private-key-file.js +60 -0
- package/dist/core/rules/text-cache.d.ts +19 -0
- package/dist/core/rules/text-cache.js +112 -0
- package/dist/core/rules/thresholds.d.ts +13 -0
- package/dist/core/rules/thresholds.js +13 -0
- package/dist/core/rules/types.d.ts +57 -0
- package/dist/core/rules/types.js +23 -0
- package/dist/core/scanner/scan.d.ts +7 -0
- package/dist/core/scanner/scan.js +70 -0
- package/dist/core/scoring/placeholder.d.ts +7 -0
- package/dist/core/scoring/placeholder.js +29 -0
- package/dist/detectors/framework.d.ts +17 -0
- package/dist/detectors/framework.js +148 -0
- package/dist/detectors/language.d.ts +14 -0
- package/dist/detectors/language.js +122 -0
- package/dist/detectors/monorepo.d.ts +13 -0
- package/dist/detectors/monorepo.js +44 -0
- package/dist/detectors/package-manager.d.ts +14 -0
- package/dist/detectors/package-manager.js +78 -0
- package/dist/detectors/project.d.ts +10 -0
- package/dist/detectors/project.js +60 -0
- package/dist/discovery/files.d.ts +15 -0
- package/dist/discovery/files.js +118 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.js +7 -0
- package/dist/reporters/json/report.d.ts +5 -0
- package/dist/reporters/json/report.js +84 -0
- package/dist/reporters/terminal/report.d.ts +8 -0
- package/dist/reporters/terminal/report.js +147 -0
- package/dist/security/redaction.d.ts +9 -0
- package/dist/security/redaction.js +26 -0
- package/dist/types/index.d.ts +137 -0
- package/dist/types/index.js +6 -0
- package/dist/utils/colors.d.ts +14 -0
- package/dist/utils/colors.js +26 -0
- package/dist/utils/fs.d.ts +17 -0
- package/dist/utils/fs.js +63 -0
- package/dist/utils/path.d.ts +17 -0
- package/dist/utils/path.js +37 -0
- package/package.json +80 -0
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import { sanitizeForOutput } from "../../utils/path.js";
|
|
2
|
+
/**
|
|
3
|
+
* Stable machine-readable scan output. No decorative terminal formatting.
|
|
4
|
+
*/
|
|
5
|
+
export function renderJsonReport(result) {
|
|
6
|
+
const payload = {
|
|
7
|
+
version: result.version,
|
|
8
|
+
repository: {
|
|
9
|
+
root: sanitizeForOutput(result.repository.root),
|
|
10
|
+
languages: result.repository.languages,
|
|
11
|
+
primaryLanguage: result.repository.primaryLanguage,
|
|
12
|
+
frameworks: result.repository.frameworks,
|
|
13
|
+
primaryFramework: result.repository.primaryFramework,
|
|
14
|
+
packageManagers: result.repository.packageManagers,
|
|
15
|
+
primaryPackageManager: result.repository.primaryPackageManager,
|
|
16
|
+
monorepo: result.repository.monorepo,
|
|
17
|
+
filesScanned: result.repository.filesScanned,
|
|
18
|
+
},
|
|
19
|
+
agents: result.agents.map((agent) => ({
|
|
20
|
+
id: agent.id,
|
|
21
|
+
displayName: agent.displayName,
|
|
22
|
+
detected: agent.detected,
|
|
23
|
+
configured: agent.configured,
|
|
24
|
+
status: agent.status,
|
|
25
|
+
summary: sanitizeForOutput(agent.summary),
|
|
26
|
+
configFiles: agent.configFiles.map((file) => ({
|
|
27
|
+
relativePath: sanitizeForOutput(file.relativePath),
|
|
28
|
+
kind: file.kind,
|
|
29
|
+
sizeBytes: file.sizeBytes,
|
|
30
|
+
empty: file.empty,
|
|
31
|
+
readable: file.readable,
|
|
32
|
+
legacy: file.legacy,
|
|
33
|
+
scope: file.scope,
|
|
34
|
+
...(file.parseError !== undefined
|
|
35
|
+
? { parseError: sanitizeForOutput(file.parseError) }
|
|
36
|
+
: {}),
|
|
37
|
+
})),
|
|
38
|
+
configPaths: agent.configPaths.map(sanitizeForOutput),
|
|
39
|
+
diagnostics: agent.diagnostics.map((diagnostic) => ({
|
|
40
|
+
code: diagnostic.code,
|
|
41
|
+
severity: diagnostic.severity,
|
|
42
|
+
message: sanitizeForOutput(diagnostic.message),
|
|
43
|
+
...(diagnostic.file !== undefined ? { file: sanitizeForOutput(diagnostic.file) } : {}),
|
|
44
|
+
})),
|
|
45
|
+
metadata: agent.metadata,
|
|
46
|
+
})),
|
|
47
|
+
findings: result.findings.map((finding) => ({
|
|
48
|
+
id: finding.id,
|
|
49
|
+
ruleId: finding.ruleId,
|
|
50
|
+
category: finding.category,
|
|
51
|
+
severity: finding.severity,
|
|
52
|
+
title: sanitizeForOutput(finding.title),
|
|
53
|
+
message: sanitizeForOutput(finding.message),
|
|
54
|
+
whyItMatters: sanitizeForOutput(finding.whyItMatters),
|
|
55
|
+
...(finding.recommendation !== undefined
|
|
56
|
+
? { recommendation: sanitizeForOutput(finding.recommendation) }
|
|
57
|
+
: {}),
|
|
58
|
+
affectedAgents: finding.affectedAgents,
|
|
59
|
+
...(finding.evidence !== undefined
|
|
60
|
+
? {
|
|
61
|
+
evidence: {
|
|
62
|
+
...(finding.evidence.path !== undefined
|
|
63
|
+
? { path: sanitizeForOutput(finding.evidence.path) }
|
|
64
|
+
: {}),
|
|
65
|
+
...(finding.evidence.line !== undefined ? { line: finding.evidence.line } : {}),
|
|
66
|
+
...(finding.evidence.detail !== undefined
|
|
67
|
+
? { detail: sanitizeForOutput(finding.evidence.detail) }
|
|
68
|
+
: {}),
|
|
69
|
+
},
|
|
70
|
+
}
|
|
71
|
+
: {}),
|
|
72
|
+
fixability: finding.fixability,
|
|
73
|
+
})),
|
|
74
|
+
summary: result.summary,
|
|
75
|
+
scoringAvailable: result.scoringAvailable,
|
|
76
|
+
scores: result.scores,
|
|
77
|
+
timing: result.timing,
|
|
78
|
+
diagnostics: {
|
|
79
|
+
warnings: result.diagnostics.warnings.map(sanitizeForOutput),
|
|
80
|
+
errors: result.diagnostics.errors.map(sanitizeForOutput),
|
|
81
|
+
},
|
|
82
|
+
};
|
|
83
|
+
return `${JSON.stringify(payload, null, 2)}\n`;
|
|
84
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { ScanResult } from "../../types/index.js";
|
|
2
|
+
export interface TerminalReportOptions {
|
|
3
|
+
verbose?: boolean;
|
|
4
|
+
}
|
|
5
|
+
/**
|
|
6
|
+
* Terminal report: agents and actionable findings.
|
|
7
|
+
*/
|
|
8
|
+
export declare function renderTerminalReport(result: ScanResult, options?: TerminalReportOptions): string;
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
import { PACKAGE_VERSION } from "../../constants.js";
|
|
2
|
+
import { AGENT_DISPLAY_NAMES } from "../../constants.js";
|
|
3
|
+
import { formatFramework } from "../../detectors/framework.js";
|
|
4
|
+
import { formatLanguage } from "../../detectors/language.js";
|
|
5
|
+
import { formatMonorepo } from "../../detectors/monorepo.js";
|
|
6
|
+
import { formatPackageManager } from "../../detectors/package-manager.js";
|
|
7
|
+
import { colors, symbolFail, symbolInfo, symbolOk, symbolWarn } from "../../utils/colors.js";
|
|
8
|
+
import { sanitizeTerminalText } from "../../security/redaction.js";
|
|
9
|
+
const DEFAULT_FINDING_LIMIT = 12;
|
|
10
|
+
function groupFindings(findings) {
|
|
11
|
+
return {
|
|
12
|
+
critical: findings.filter((f) => f.severity === "critical"),
|
|
13
|
+
warning: findings.filter((f) => f.severity === "warning"),
|
|
14
|
+
info: findings.filter((f) => f.severity === "info"),
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
function agentGlyph(configured, detected) {
|
|
18
|
+
if (configured) {
|
|
19
|
+
return symbolOk();
|
|
20
|
+
}
|
|
21
|
+
if (detected) {
|
|
22
|
+
return symbolWarn();
|
|
23
|
+
}
|
|
24
|
+
return colors.dim("–");
|
|
25
|
+
}
|
|
26
|
+
function formatAgents(agents) {
|
|
27
|
+
return agents.map((id) => AGENT_DISPLAY_NAMES[id] ?? id).join(", ");
|
|
28
|
+
}
|
|
29
|
+
function renderFindingBlock(finding, verbose) {
|
|
30
|
+
const lines = [];
|
|
31
|
+
const icon = finding.severity === "critical"
|
|
32
|
+
? symbolFail()
|
|
33
|
+
: finding.severity === "warning"
|
|
34
|
+
? symbolWarn()
|
|
35
|
+
: symbolInfo();
|
|
36
|
+
lines.push(` ${icon} ${sanitizeTerminalText(finding.title)}`);
|
|
37
|
+
if (finding.evidence?.path) {
|
|
38
|
+
lines.push(colors.dim(` ${sanitizeTerminalText(finding.evidence.path)}`));
|
|
39
|
+
}
|
|
40
|
+
if (finding.affectedAgents.length > 0) {
|
|
41
|
+
lines.push(colors.dim(` Affected: ${formatAgents(finding.affectedAgents)}`));
|
|
42
|
+
}
|
|
43
|
+
lines.push(` ${sanitizeTerminalText(finding.message)}`);
|
|
44
|
+
if (verbose) {
|
|
45
|
+
lines.push(colors.dim(` Why: ${sanitizeTerminalText(finding.whyItMatters)}`));
|
|
46
|
+
}
|
|
47
|
+
if (finding.recommendation) {
|
|
48
|
+
lines.push(` Fix: ${sanitizeTerminalText(finding.recommendation)}`);
|
|
49
|
+
}
|
|
50
|
+
lines.push("");
|
|
51
|
+
return lines;
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Terminal report: agents and actionable findings.
|
|
55
|
+
*/
|
|
56
|
+
export function renderTerminalReport(result, options = {}) {
|
|
57
|
+
const lines = [];
|
|
58
|
+
const verbose = options.verbose ?? false;
|
|
59
|
+
lines.push("");
|
|
60
|
+
lines.push(colors.bold(`🩺 AgentDoctor`) + colors.dim(` v${PACKAGE_VERSION}`));
|
|
61
|
+
lines.push("");
|
|
62
|
+
lines.push(colors.dim("Scanning repository..."));
|
|
63
|
+
lines.push("");
|
|
64
|
+
lines.push(colors.bold("Repository"));
|
|
65
|
+
lines.push(` Framework: ${sanitizeTerminalText(formatFramework(result.repository.primaryFramework))}`);
|
|
66
|
+
lines.push(` Language: ${sanitizeTerminalText(formatLanguage(result.repository.primaryLanguage))}`);
|
|
67
|
+
lines.push(` Package manager: ${sanitizeTerminalText(formatPackageManager(result.repository.primaryPackageManager))}`);
|
|
68
|
+
if (result.repository.monorepo !== "none") {
|
|
69
|
+
lines.push(` Monorepo: ${sanitizeTerminalText(formatMonorepo(result.repository.monorepo))}`);
|
|
70
|
+
}
|
|
71
|
+
lines.push(` Files scanned: ${result.repository.filesScanned}`);
|
|
72
|
+
lines.push("");
|
|
73
|
+
lines.push(colors.bold("AI Coding Agents"));
|
|
74
|
+
lines.push("");
|
|
75
|
+
for (const agent of result.agents) {
|
|
76
|
+
const glyph = agentGlyph(agent.configured, agent.detected);
|
|
77
|
+
const status = agent.configured ? "configured" : agent.detected ? "detected" : "not configured";
|
|
78
|
+
lines.push(`${glyph} ${pad(sanitizeTerminalText(agent.displayName), 12)} ${colors.dim(status)}`);
|
|
79
|
+
if (verbose && agent.configured) {
|
|
80
|
+
lines.push(colors.dim(` ${sanitizeTerminalText(agent.summary)}`));
|
|
81
|
+
for (const configPath of agent.configPaths) {
|
|
82
|
+
lines.push(colors.dim(` · ${sanitizeTerminalText(configPath)}`));
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
lines.push("");
|
|
87
|
+
const grouped = groupFindings(result.findings);
|
|
88
|
+
const total = result.summary.total;
|
|
89
|
+
lines.push(colors.bold("Findings"));
|
|
90
|
+
lines.push("");
|
|
91
|
+
if (total === 0) {
|
|
92
|
+
lines.push(` ${symbolOk()} No findings`);
|
|
93
|
+
lines.push("");
|
|
94
|
+
}
|
|
95
|
+
else {
|
|
96
|
+
let shown = 0;
|
|
97
|
+
const limit = verbose ? Number.POSITIVE_INFINITY : DEFAULT_FINDING_LIMIT;
|
|
98
|
+
for (const severity of ["critical", "warning", "info"]) {
|
|
99
|
+
const list = grouped[severity];
|
|
100
|
+
if (list.length === 0) {
|
|
101
|
+
continue;
|
|
102
|
+
}
|
|
103
|
+
const header = severity === "critical"
|
|
104
|
+
? colors.red(colors.bold("CRITICAL"))
|
|
105
|
+
: severity === "warning"
|
|
106
|
+
? colors.yellow(colors.bold("WARNING"))
|
|
107
|
+
: colors.cyan(colors.bold("INFO"));
|
|
108
|
+
lines.push(header);
|
|
109
|
+
lines.push("");
|
|
110
|
+
for (const finding of list) {
|
|
111
|
+
if (shown >= limit) {
|
|
112
|
+
break;
|
|
113
|
+
}
|
|
114
|
+
lines.push(...renderFindingBlock(finding, verbose));
|
|
115
|
+
shown += 1;
|
|
116
|
+
}
|
|
117
|
+
if (shown >= limit) {
|
|
118
|
+
break;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
if (total > shown) {
|
|
122
|
+
lines.push(colors.dim(` … and ${total - shown} more. Re-run with --verbose to see additional detail.`));
|
|
123
|
+
lines.push("");
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
lines.push(colors.bold("Summary"));
|
|
127
|
+
lines.push("");
|
|
128
|
+
lines.push(` ${result.summary.critical} critical`);
|
|
129
|
+
lines.push(` ${result.summary.warning} warning`);
|
|
130
|
+
lines.push(` ${result.summary.info} info`);
|
|
131
|
+
lines.push("");
|
|
132
|
+
lines.push(colors.dim("Scoring: not included in this release"));
|
|
133
|
+
lines.push("");
|
|
134
|
+
if (verbose) {
|
|
135
|
+
lines.push(colors.dim("Timing"));
|
|
136
|
+
lines.push(` Discovery: ${result.timing.discoveryMs}ms`);
|
|
137
|
+
lines.push(` Detection: ${result.timing.detectionMs}ms`);
|
|
138
|
+
lines.push(` Agents: ${result.timing.agentsMs}ms`);
|
|
139
|
+
lines.push(` Rules: ${result.timing.rulesMs}ms`);
|
|
140
|
+
lines.push(` Total: ${result.timing.totalMs}ms`);
|
|
141
|
+
lines.push("");
|
|
142
|
+
}
|
|
143
|
+
return lines.join("\n");
|
|
144
|
+
}
|
|
145
|
+
function pad(value, width) {
|
|
146
|
+
return value.padEnd(width, " ");
|
|
147
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Never print secret values. Report paths and risk only.
|
|
3
|
+
*/
|
|
4
|
+
export declare function redactSecretValue(_value: string): string;
|
|
5
|
+
/**
|
|
6
|
+
* Strip control characters that could hijack terminal output.
|
|
7
|
+
*/
|
|
8
|
+
export declare function sanitizeTerminalText(value: string): string;
|
|
9
|
+
export declare function looksLikeSensitiveFilename(filename: string): boolean;
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Never print secret values. Report paths and risk only.
|
|
3
|
+
*/
|
|
4
|
+
export function redactSecretValue(_value) {
|
|
5
|
+
return "[REDACTED]";
|
|
6
|
+
}
|
|
7
|
+
/**
|
|
8
|
+
* Strip control characters that could hijack terminal output.
|
|
9
|
+
*/
|
|
10
|
+
export function sanitizeTerminalText(value) {
|
|
11
|
+
// eslint-disable-next-line no-control-regex -- intentional control-char stripping
|
|
12
|
+
return value.replace(/[\u0000-\u0008\u000B\u000C\u000E-\u001F\u007F]/g, "");
|
|
13
|
+
}
|
|
14
|
+
const SENSITIVE_BASENAME_PATTERNS = [
|
|
15
|
+
/^\.env(\..+)?$/i,
|
|
16
|
+
/.*credentials.*\.json$/i,
|
|
17
|
+
/.*secret.*/i,
|
|
18
|
+
/id_rsa$/i,
|
|
19
|
+
/id_ed25519$/i,
|
|
20
|
+
/\.pem$/i,
|
|
21
|
+
/\.key$/i,
|
|
22
|
+
];
|
|
23
|
+
export function looksLikeSensitiveFilename(filename) {
|
|
24
|
+
const base = filename.split(/[/\\]/).pop() ?? filename;
|
|
25
|
+
return SENSITIVE_BASENAME_PATTERNS.some((pattern) => pattern.test(base));
|
|
26
|
+
}
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
import type { AgentDetectionResult } from "../agents/types.js";
|
|
2
|
+
/**
|
|
3
|
+
* AgentDoctor core types.
|
|
4
|
+
* Designed so scanning stays independent of terminal reporting.
|
|
5
|
+
*/
|
|
6
|
+
export type Severity = "critical" | "warning" | "info";
|
|
7
|
+
export type RuleCategory = "security" | "context" | "instructions" | "mcp" | "performance" | "compatibility";
|
|
8
|
+
export type LanguageId = "typescript" | "javascript" | "python" | "dart" | "php" | "go" | "rust" | "java" | "kotlin" | "unknown";
|
|
9
|
+
export type FrameworkId = "nextjs" | "react" | "vue" | "nuxt" | "svelte" | "nodejs" | "express" | "nestjs" | "flutter" | "laravel" | "django" | "fastapi" | "unknown";
|
|
10
|
+
export type PackageManagerId = "npm" | "pnpm" | "yarn" | "bun" | "composer" | "pip" | "poetry" | "cargo" | "gradle" | "unknown";
|
|
11
|
+
export type MonorepoToolId = "npm-workspaces" | "pnpm-workspaces" | "turborepo" | "nx" | "none";
|
|
12
|
+
export type AgentId = "cursor" | "claude-code" | "codex";
|
|
13
|
+
export interface DiscoveredFile {
|
|
14
|
+
/** Absolute path */
|
|
15
|
+
absolutePath: string;
|
|
16
|
+
/** Path relative to repository root, POSIX-style */
|
|
17
|
+
relativePath: string;
|
|
18
|
+
sizeBytes: number;
|
|
19
|
+
isSymlink: boolean;
|
|
20
|
+
}
|
|
21
|
+
export interface DiscoveryResult {
|
|
22
|
+
files: DiscoveredFile[];
|
|
23
|
+
directoriesSkipped: string[];
|
|
24
|
+
filesSkippedOversized: number;
|
|
25
|
+
permissionErrors: string[];
|
|
26
|
+
elapsedMs: number;
|
|
27
|
+
}
|
|
28
|
+
export interface RepositoryInfo {
|
|
29
|
+
root: string;
|
|
30
|
+
languages: LanguageId[];
|
|
31
|
+
primaryLanguage: LanguageId;
|
|
32
|
+
frameworks: FrameworkId[];
|
|
33
|
+
primaryFramework: FrameworkId;
|
|
34
|
+
packageManagers: PackageManagerId[];
|
|
35
|
+
primaryPackageManager: PackageManagerId;
|
|
36
|
+
monorepo: MonorepoToolId;
|
|
37
|
+
filesScanned: number;
|
|
38
|
+
}
|
|
39
|
+
export type { AgentConfigStatus, AgentConfigFile, AgentConfigFileKind, AgentConfigScope, AgentDiagnostic, AgentDetectionResult, } from "../agents/types.js";
|
|
40
|
+
/** @deprecated Prefer AgentDetectionResult — kept for compatibility. */
|
|
41
|
+
export type AgentPresence = AgentDetectionResult;
|
|
42
|
+
export interface FindingEvidence {
|
|
43
|
+
path?: string;
|
|
44
|
+
line?: number;
|
|
45
|
+
detail?: string;
|
|
46
|
+
}
|
|
47
|
+
export type Fixability = "safe" | "review" | "manual" | "none";
|
|
48
|
+
export interface Finding {
|
|
49
|
+
id: string;
|
|
50
|
+
ruleId: string;
|
|
51
|
+
category: RuleCategory;
|
|
52
|
+
severity: Severity;
|
|
53
|
+
title: string;
|
|
54
|
+
message: string;
|
|
55
|
+
whyItMatters: string;
|
|
56
|
+
recommendation?: string;
|
|
57
|
+
affectedAgents: AgentId[];
|
|
58
|
+
evidence?: FindingEvidence;
|
|
59
|
+
fixability: Fixability;
|
|
60
|
+
}
|
|
61
|
+
export interface FindingsSummary {
|
|
62
|
+
critical: number;
|
|
63
|
+
warning: number;
|
|
64
|
+
info: number;
|
|
65
|
+
total: number;
|
|
66
|
+
}
|
|
67
|
+
export interface CategoryScores {
|
|
68
|
+
security: number;
|
|
69
|
+
context: number;
|
|
70
|
+
instructions: number;
|
|
71
|
+
mcp: number;
|
|
72
|
+
compatibility: number;
|
|
73
|
+
performance: number;
|
|
74
|
+
}
|
|
75
|
+
export interface AgentScores {
|
|
76
|
+
cursor: number;
|
|
77
|
+
"claude-code": number;
|
|
78
|
+
codex: number;
|
|
79
|
+
}
|
|
80
|
+
export interface Scores {
|
|
81
|
+
overall: number;
|
|
82
|
+
categories: CategoryScores;
|
|
83
|
+
agents: AgentScores;
|
|
84
|
+
}
|
|
85
|
+
export interface TimingInfo {
|
|
86
|
+
discoveryMs: number;
|
|
87
|
+
detectionMs: number;
|
|
88
|
+
agentsMs: number;
|
|
89
|
+
rulesMs: number;
|
|
90
|
+
scoringMs: number;
|
|
91
|
+
totalMs: number;
|
|
92
|
+
}
|
|
93
|
+
export interface ScanDiagnostics {
|
|
94
|
+
warnings: string[];
|
|
95
|
+
errors: string[];
|
|
96
|
+
}
|
|
97
|
+
export interface ScanResult {
|
|
98
|
+
version: string;
|
|
99
|
+
repository: RepositoryInfo;
|
|
100
|
+
agents: AgentDetectionResult[];
|
|
101
|
+
findings: Finding[];
|
|
102
|
+
summary: FindingsSummary;
|
|
103
|
+
/**
|
|
104
|
+
* Readiness scores when scoring is available.
|
|
105
|
+
* Null while scoring is unavailable — do not treat as readiness.
|
|
106
|
+
*/
|
|
107
|
+
scores: Scores | null;
|
|
108
|
+
/** False until the readiness scoring model ships. */
|
|
109
|
+
scoringAvailable: boolean;
|
|
110
|
+
timing: TimingInfo;
|
|
111
|
+
diagnostics: ScanDiagnostics;
|
|
112
|
+
}
|
|
113
|
+
export interface ScanOptions {
|
|
114
|
+
cwd?: string;
|
|
115
|
+
/** Max file size to read/stat deeply; oversized files are counted but skipped for content */
|
|
116
|
+
maxFileSizeBytes?: number;
|
|
117
|
+
verbose?: boolean;
|
|
118
|
+
/** Optional filter for which rule IDs to run */
|
|
119
|
+
includeRules?: string[];
|
|
120
|
+
excludeRules?: string[];
|
|
121
|
+
}
|
|
122
|
+
export interface CliOptions {
|
|
123
|
+
path?: string;
|
|
124
|
+
json?: boolean;
|
|
125
|
+
ci?: boolean;
|
|
126
|
+
verbose?: boolean;
|
|
127
|
+
minScore?: number;
|
|
128
|
+
dryRun?: boolean;
|
|
129
|
+
yes?: boolean;
|
|
130
|
+
}
|
|
131
|
+
export declare const EXIT_CODES: {
|
|
132
|
+
readonly SUCCESS: 0;
|
|
133
|
+
readonly ISSUES_OR_THRESHOLD: 1;
|
|
134
|
+
readonly USAGE_ERROR: 2;
|
|
135
|
+
readonly INTERNAL_ERROR: 3;
|
|
136
|
+
};
|
|
137
|
+
export type ExitCode = (typeof EXIT_CODES)[keyof typeof EXIT_CODES];
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export declare const colors: {
|
|
2
|
+
bold: (s: string) => string;
|
|
3
|
+
dim: (s: string) => string;
|
|
4
|
+
cyan: (s: string) => string;
|
|
5
|
+
green: (s: string) => string;
|
|
6
|
+
yellow: (s: string) => string;
|
|
7
|
+
red: (s: string) => string;
|
|
8
|
+
magenta: (s: string) => string;
|
|
9
|
+
white: (s: string) => string;
|
|
10
|
+
};
|
|
11
|
+
export declare function symbolOk(): string;
|
|
12
|
+
export declare function symbolWarn(): string;
|
|
13
|
+
export declare function symbolFail(): string;
|
|
14
|
+
export declare function symbolInfo(): string;
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import pc from "picocolors";
|
|
2
|
+
const useColor = !("NO_COLOR" in process.env) &&
|
|
3
|
+
process.env["FORCE_COLOR"] !== "0" &&
|
|
4
|
+
process.stdout.isTTY === true;
|
|
5
|
+
export const colors = {
|
|
6
|
+
bold: (s) => (useColor ? pc.bold(s) : s),
|
|
7
|
+
dim: (s) => (useColor ? pc.dim(s) : s),
|
|
8
|
+
cyan: (s) => (useColor ? pc.cyan(s) : s),
|
|
9
|
+
green: (s) => (useColor ? pc.green(s) : s),
|
|
10
|
+
yellow: (s) => (useColor ? pc.yellow(s) : s),
|
|
11
|
+
red: (s) => (useColor ? pc.red(s) : s),
|
|
12
|
+
magenta: (s) => (useColor ? pc.magenta(s) : s),
|
|
13
|
+
white: (s) => (useColor ? pc.white(s) : s),
|
|
14
|
+
};
|
|
15
|
+
export function symbolOk() {
|
|
16
|
+
return colors.green("✓");
|
|
17
|
+
}
|
|
18
|
+
export function symbolWarn() {
|
|
19
|
+
return colors.yellow("!");
|
|
20
|
+
}
|
|
21
|
+
export function symbolFail() {
|
|
22
|
+
return colors.red("✗");
|
|
23
|
+
}
|
|
24
|
+
export function symbolInfo() {
|
|
25
|
+
return colors.cyan("i");
|
|
26
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
export declare function pathExists(target: string): Promise<boolean>;
|
|
2
|
+
export declare function isDirectory(target: string): Promise<boolean>;
|
|
3
|
+
export declare function readTextFile(filePath: string, maxBytes: number): Promise<string | null>;
|
|
4
|
+
export declare function readJsonFile<T>(filePath: string, maxBytes: number): Promise<{
|
|
5
|
+
ok: true;
|
|
6
|
+
data: T;
|
|
7
|
+
} | {
|
|
8
|
+
ok: false;
|
|
9
|
+
error: string;
|
|
10
|
+
}>;
|
|
11
|
+
export declare function listDirectorySafe(dirPath: string): Promise<{
|
|
12
|
+
ok: true;
|
|
13
|
+
entries: string[];
|
|
14
|
+
} | {
|
|
15
|
+
ok: false;
|
|
16
|
+
error: string;
|
|
17
|
+
}>;
|
package/dist/utils/fs.js
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import fs from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
export async function pathExists(target) {
|
|
4
|
+
try {
|
|
5
|
+
await fs.access(target);
|
|
6
|
+
return true;
|
|
7
|
+
}
|
|
8
|
+
catch {
|
|
9
|
+
return false;
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
export async function isDirectory(target) {
|
|
13
|
+
try {
|
|
14
|
+
const stat = await fs.stat(target);
|
|
15
|
+
return stat.isDirectory();
|
|
16
|
+
}
|
|
17
|
+
catch {
|
|
18
|
+
return false;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
export async function readTextFile(filePath, maxBytes) {
|
|
22
|
+
try {
|
|
23
|
+
const handle = await fs.open(filePath, "r");
|
|
24
|
+
try {
|
|
25
|
+
const stat = await handle.stat();
|
|
26
|
+
if (stat.size > maxBytes) {
|
|
27
|
+
return null;
|
|
28
|
+
}
|
|
29
|
+
const buffer = Buffer.alloc(Number(stat.size));
|
|
30
|
+
await handle.read(buffer, 0, buffer.length, 0);
|
|
31
|
+
return buffer.toString("utf8");
|
|
32
|
+
}
|
|
33
|
+
finally {
|
|
34
|
+
await handle.close();
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
catch {
|
|
38
|
+
return null;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
export async function readJsonFile(filePath, maxBytes) {
|
|
42
|
+
const text = await readTextFile(filePath, maxBytes);
|
|
43
|
+
if (text === null) {
|
|
44
|
+
return { ok: false, error: `Unable to read or file too large: ${path.basename(filePath)}` };
|
|
45
|
+
}
|
|
46
|
+
try {
|
|
47
|
+
return { ok: true, data: JSON.parse(text) };
|
|
48
|
+
}
|
|
49
|
+
catch (error) {
|
|
50
|
+
const message = error instanceof Error ? error.message : "Invalid JSON";
|
|
51
|
+
return { ok: false, error: message };
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
export async function listDirectorySafe(dirPath) {
|
|
55
|
+
try {
|
|
56
|
+
const entries = await fs.readdir(dirPath);
|
|
57
|
+
return { ok: true, entries };
|
|
58
|
+
}
|
|
59
|
+
catch (error) {
|
|
60
|
+
const message = error instanceof Error ? error.message : "Unable to read directory";
|
|
61
|
+
return { ok: false, error: message };
|
|
62
|
+
}
|
|
63
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Resolve and normalize a user-supplied path safely.
|
|
3
|
+
* Rejects empty strings; does not follow symlinks here.
|
|
4
|
+
*/
|
|
5
|
+
export declare function resolveRepoRoot(inputPath: string): string;
|
|
6
|
+
/**
|
|
7
|
+
* Produce a POSIX-style relative path for stable cross-platform reporting.
|
|
8
|
+
*/
|
|
9
|
+
export declare function toPosixRelative(from: string, to: string): string;
|
|
10
|
+
/**
|
|
11
|
+
* Ensure a candidate path stays within a root (defense against traversal).
|
|
12
|
+
*/
|
|
13
|
+
export declare function isPathInsideRoot(root: string, candidate: string): boolean;
|
|
14
|
+
/**
|
|
15
|
+
* Strip ANSI / control characters from strings destined for JSON or logs.
|
|
16
|
+
*/
|
|
17
|
+
export declare function sanitizeForOutput(value: string): string;
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
/**
|
|
3
|
+
* Resolve and normalize a user-supplied path safely.
|
|
4
|
+
* Rejects empty strings; does not follow symlinks here.
|
|
5
|
+
*/
|
|
6
|
+
export function resolveRepoRoot(inputPath) {
|
|
7
|
+
const trimmed = inputPath.trim();
|
|
8
|
+
if (!trimmed) {
|
|
9
|
+
throw new Error("Path must not be empty");
|
|
10
|
+
}
|
|
11
|
+
return path.resolve(trimmed);
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Produce a POSIX-style relative path for stable cross-platform reporting.
|
|
15
|
+
*/
|
|
16
|
+
export function toPosixRelative(from, to) {
|
|
17
|
+
return path.relative(from, to).split(path.sep).join("/");
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Ensure a candidate path stays within a root (defense against traversal).
|
|
21
|
+
*/
|
|
22
|
+
export function isPathInsideRoot(root, candidate) {
|
|
23
|
+
const normalizedRoot = path.resolve(root);
|
|
24
|
+
const normalizedCandidate = path.resolve(candidate);
|
|
25
|
+
if (normalizedCandidate === normalizedRoot) {
|
|
26
|
+
return true;
|
|
27
|
+
}
|
|
28
|
+
const relative = path.relative(normalizedRoot, normalizedCandidate);
|
|
29
|
+
return relative !== "" && !relative.startsWith("..") && !path.isAbsolute(relative);
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Strip ANSI / control characters from strings destined for JSON or logs.
|
|
33
|
+
*/
|
|
34
|
+
export function sanitizeForOutput(value) {
|
|
35
|
+
// eslint-disable-next-line no-control-regex -- intentional control-char stripping
|
|
36
|
+
return value.replace(/[\u0000-\u0008\u000B\u000C\u000E-\u001F\u007F]/g, "");
|
|
37
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@praneeth_54/agentdoctor",
|
|
3
|
+
"version": "0.1.0-beta",
|
|
4
|
+
"description": "Audit AI coding agent configuration in a repository — local, deterministic, no API key.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"agentdoctor": "dist/cli/index.js"
|
|
8
|
+
},
|
|
9
|
+
"exports": {
|
|
10
|
+
".": {
|
|
11
|
+
"types": "./dist/index.d.ts",
|
|
12
|
+
"import": "./dist/index.js"
|
|
13
|
+
}
|
|
14
|
+
},
|
|
15
|
+
"main": "./dist/index.js",
|
|
16
|
+
"types": "./dist/index.d.ts",
|
|
17
|
+
"files": [
|
|
18
|
+
"dist",
|
|
19
|
+
"README.md",
|
|
20
|
+
"LICENSE",
|
|
21
|
+
"CHANGELOG.md"
|
|
22
|
+
],
|
|
23
|
+
"engines": {
|
|
24
|
+
"node": ">=20"
|
|
25
|
+
},
|
|
26
|
+
"scripts": {
|
|
27
|
+
"build": "tsc -p tsconfig.build.json",
|
|
28
|
+
"dev": "tsx src/cli/index.ts",
|
|
29
|
+
"typecheck": "tsc -p tsconfig.json --noEmit && tsc -p tsconfig.build.json --noEmit",
|
|
30
|
+
"lint": "eslint .",
|
|
31
|
+
"lint:fix": "eslint . --fix",
|
|
32
|
+
"format": "prettier --write .",
|
|
33
|
+
"format:check": "prettier --check .",
|
|
34
|
+
"test": "vitest run",
|
|
35
|
+
"test:watch": "vitest",
|
|
36
|
+
"verify": "npm run typecheck && npm run lint && npm run format:check && npm test && npm run build",
|
|
37
|
+
"prepublishOnly": "npm run verify"
|
|
38
|
+
},
|
|
39
|
+
"keywords": [
|
|
40
|
+
"ai",
|
|
41
|
+
"coding-agents",
|
|
42
|
+
"agent-config",
|
|
43
|
+
"cursor",
|
|
44
|
+
"claude-code",
|
|
45
|
+
"codex",
|
|
46
|
+
"mcp",
|
|
47
|
+
"security",
|
|
48
|
+
"linter",
|
|
49
|
+
"cli",
|
|
50
|
+
"static-analysis",
|
|
51
|
+
"developer-tools"
|
|
52
|
+
],
|
|
53
|
+
"author": "AgentDoctor Contributors",
|
|
54
|
+
"license": "MIT",
|
|
55
|
+
"repository": {
|
|
56
|
+
"type": "git",
|
|
57
|
+
"url": "git+https://github.com/pranee54/AgentDoctor.git"
|
|
58
|
+
},
|
|
59
|
+
"bugs": {
|
|
60
|
+
"url": "https://github.com/pranee54/AgentDoctor/issues"
|
|
61
|
+
},
|
|
62
|
+
"homepage": "https://github.com/pranee54/AgentDoctor#readme",
|
|
63
|
+
"publishConfig": {
|
|
64
|
+
"access": "public"
|
|
65
|
+
},
|
|
66
|
+
"dependencies": {
|
|
67
|
+
"commander": "^14.0.3",
|
|
68
|
+
"picocolors": "^1.1.1"
|
|
69
|
+
},
|
|
70
|
+
"devDependencies": {
|
|
71
|
+
"@eslint/js": "^9.39.2",
|
|
72
|
+
"@types/node": "^22.19.1",
|
|
73
|
+
"eslint": "^9.39.2",
|
|
74
|
+
"prettier": "^3.7.4",
|
|
75
|
+
"tsx": "^4.21.0",
|
|
76
|
+
"typescript": "^5.9.3",
|
|
77
|
+
"typescript-eslint": "^8.50.0",
|
|
78
|
+
"vitest": "^3.2.4"
|
|
79
|
+
}
|
|
80
|
+
}
|