@praneeth_54/agentdoctor 0.1.0-beta → 0.1.1-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
CHANGED
|
@@ -7,22 +7,26 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
-
###
|
|
10
|
+
### Planned
|
|
11
|
+
|
|
12
|
+
- Deterministic readiness scoring
|
|
13
|
+
- Safe automatic fixes
|
|
14
|
+
- Packaged GitHub Action
|
|
15
|
+
|
|
16
|
+
## [0.1.1-beta] — 2026-07-29
|
|
11
17
|
|
|
12
|
-
|
|
18
|
+
Patch release.
|
|
13
19
|
|
|
14
20
|
### Fixed
|
|
15
21
|
|
|
16
|
-
-
|
|
17
|
-
-
|
|
18
|
-
- Release workflow selects notes by tag and attaches the npm tarball
|
|
19
|
-
- CLI help text matches shipped capabilities (`--min-score` no-op until scoring)
|
|
22
|
+
- False missing-path warnings for repository-root paths referenced from nested instruction files
|
|
23
|
+
- CSS-like backtick tokens such as `.content` being interpreted as filesystem paths
|
|
20
24
|
|
|
21
|
-
###
|
|
25
|
+
### Testing
|
|
22
26
|
|
|
23
|
-
-
|
|
24
|
-
-
|
|
25
|
-
-
|
|
27
|
+
- Added real-world regression fixtures
|
|
28
|
+
- Added 17 regression tests
|
|
29
|
+
- Total test suite is now 91 tests
|
|
26
30
|
|
|
27
31
|
## [0.1.0-beta] — 2026-07-29
|
|
28
32
|
|
|
@@ -40,6 +44,10 @@ First public beta.
|
|
|
40
44
|
- Programmatic `scan()` API
|
|
41
45
|
- Fixture-based unit and integration tests
|
|
42
46
|
|
|
47
|
+
### Changed
|
|
48
|
+
|
|
49
|
+
- Published npm package as `@praneeth_54/agentdoctor` (unscoped `agentdoctor` blocked by npm as too similar to `agent-doctor`)
|
|
50
|
+
|
|
43
51
|
### Security
|
|
44
52
|
|
|
45
53
|
- Conservative wording for exposure findings
|
|
@@ -54,5 +62,6 @@ First public beta.
|
|
|
54
62
|
- Not a complete secret scanner
|
|
55
63
|
- Git “tracked secret” detection deferred
|
|
56
64
|
|
|
57
|
-
[Unreleased]: https://github.com/pranee54/AgentDoctor/compare/v0.1.
|
|
65
|
+
[Unreleased]: https://github.com/pranee54/AgentDoctor/compare/v0.1.1-beta...HEAD
|
|
66
|
+
[0.1.1-beta]: https://github.com/pranee54/AgentDoctor/releases/tag/v0.1.1-beta
|
|
58
67
|
[0.1.0-beta]: https://github.com/pranee54/AgentDoctor/releases/tag/v0.1.0-beta
|
package/README.md
CHANGED
|
@@ -1,5 +1,12 @@
|
|
|
1
1
|
# AgentDoctor
|
|
2
2
|
|
|
3
|
+
[](https://www.npmjs.com/package/@praneeth_54/agentdoctor)
|
|
4
|
+
[](LICENSE)
|
|
5
|
+
[](https://github.com/pranee54/AgentDoctor/stargazers)
|
|
6
|
+
[](https://github.com/pranee54/AgentDoctor/actions)
|
|
7
|
+
[](https://nodejs.org)
|
|
8
|
+
[](CHANGELOG.md)
|
|
9
|
+
|
|
3
10
|
**Lighthouse for AI coding agents.**
|
|
4
11
|
|
|
5
12
|
AgentDoctor audits a repository’s AI coding agent configuration — instructions, ignore rules, permissions, and MCP setup — using local static analysis. No API key. No upload by default.
|
|
@@ -15,12 +22,6 @@ npx @praneeth_54/agentdoctor
|
|
|
15
22
|
| **Install** | `npx @praneeth_54/agentdoctor` or `npm install -g @praneeth_54/agentdoctor` |
|
|
16
23
|
| **Requires** | Node.js 20+ |
|
|
17
24
|
|
|
18
|
-
[](https://github.com/pranee54/AgentDoctor/actions)
|
|
19
|
-
[](https://www.npmjs.com/package/@praneeth_54/agentdoctor)
|
|
20
|
-
[](https://nodejs.org)
|
|
21
|
-
[](LICENSE)
|
|
22
|
-
[](CHANGELOG.md)
|
|
23
|
-
|
|
24
25
|
---
|
|
25
26
|
|
|
26
27
|
## Demo
|
|
@@ -28,7 +29,7 @@ npx @praneeth_54/agentdoctor
|
|
|
28
29
|
```text
|
|
29
30
|
$ npx @praneeth_54/agentdoctor
|
|
30
31
|
|
|
31
|
-
AgentDoctor v0.1.
|
|
32
|
+
AgentDoctor v0.1.1-beta
|
|
32
33
|
|
|
33
34
|
Repository
|
|
34
35
|
Framework: Next.js
|
|
@@ -87,13 +88,13 @@ AgentDoctor analyzes configuration files. It does not run agents or edit your pr
|
|
|
87
88
|
### One-shot
|
|
88
89
|
|
|
89
90
|
```bash
|
|
90
|
-
npx @praneeth_54/agentdoctor
|
|
91
|
+
npx @praneeth_54/agentdoctor@0.1.1-beta
|
|
91
92
|
```
|
|
92
93
|
|
|
93
94
|
### Global (optional)
|
|
94
95
|
|
|
95
96
|
```bash
|
|
96
|
-
npm install -g @praneeth_54/agentdoctor
|
|
97
|
+
npm install -g @praneeth_54/agentdoctor@0.1.1-beta
|
|
97
98
|
agentdoctor
|
|
98
99
|
```
|
|
99
100
|
|
|
@@ -224,7 +225,7 @@ npx @praneeth_54/agentdoctor --json
|
|
|
224
225
|
|
|
225
226
|
```json
|
|
226
227
|
{
|
|
227
|
-
"version": "0.1.
|
|
228
|
+
"version": "0.1.1-beta",
|
|
228
229
|
"repository": {
|
|
229
230
|
"primaryFramework": "nextjs",
|
|
230
231
|
"primaryLanguage": "typescript",
|
package/dist/constants.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export declare const PACKAGE_VERSION = "0.1.
|
|
1
|
+
export declare const PACKAGE_VERSION = "0.1.1-beta";
|
|
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.
|
|
1
|
+
export const PACKAGE_VERSION = "0.1.1-beta";
|
|
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([
|
|
@@ -1,2 +1,22 @@
|
|
|
1
1
|
import type { RuleDefinition } from "../types.js";
|
|
2
|
+
export type ResolvedPathReference = {
|
|
3
|
+
status: "ok";
|
|
4
|
+
pathsToCheck: string[];
|
|
5
|
+
primaryRelative: string;
|
|
6
|
+
} | {
|
|
7
|
+
status: "escape";
|
|
8
|
+
attempted: string;
|
|
9
|
+
};
|
|
10
|
+
/**
|
|
11
|
+
* Conservative local path classification for markdown links / backticks.
|
|
12
|
+
* Precision is more important than recall for this rule.
|
|
13
|
+
*/
|
|
14
|
+
export declare function isLikelyLocalPathReference(value: string): boolean;
|
|
15
|
+
export declare function extractPathCandidates(text: string): string[];
|
|
16
|
+
/**
|
|
17
|
+
* Resolve a candidate path reference with repository-aware semantics:
|
|
18
|
+
* - `./` and `../` → relative to the instruction file directory
|
|
19
|
+
* - otherwise → repository-root relative (avoids nesting under `.cursor/rules/`)
|
|
20
|
+
*/
|
|
21
|
+
export declare function resolveInstructionPathReference(instructionRelativePath: string, candidate: string): ResolvedPathReference;
|
|
2
22
|
export declare const missingPathReferenceRule: RuleDefinition;
|
|
@@ -10,49 +10,97 @@ const INSTRUCTION_KINDS = new Set([
|
|
|
10
10
|
"claude-local-md",
|
|
11
11
|
"claude-rule-md",
|
|
12
12
|
]);
|
|
13
|
-
/**
|
|
14
|
-
|
|
15
|
-
* - Markdown links: [text](relative/path.ext)
|
|
16
|
-
* - Backticked paths with separators or extensions
|
|
17
|
-
*/
|
|
18
|
-
function extractPathCandidates(text) {
|
|
19
|
-
const found = new Set();
|
|
20
|
-
for (const match of text.matchAll(/\[[^\]]*]\(([^)]+)\)/g)) {
|
|
21
|
-
const target = (match[1] ?? "").trim();
|
|
22
|
-
if (isConservativeLocalPath(target)) {
|
|
23
|
-
found.add(stripAnchor(target));
|
|
24
|
-
}
|
|
25
|
-
}
|
|
26
|
-
for (const match of text.matchAll(/`([^`]+)`/g)) {
|
|
27
|
-
const target = (match[1] ?? "").trim();
|
|
28
|
-
if (isConservativeLocalPath(target)) {
|
|
29
|
-
found.add(stripAnchor(target));
|
|
30
|
-
}
|
|
31
|
-
}
|
|
32
|
-
return [...found];
|
|
33
|
-
}
|
|
13
|
+
/** Extensions accepted for bare filenames (no `/`) — precision over recall. */
|
|
14
|
+
const BARE_FILE_EXTENSION = /\.(mdc?|txt|tsx?|jsx?|mjs|cjs|json|ya?ml|toml|php|py|rb|go|rs|java|kt|css|scss|html|vue|svelte|sh|bash|zsh|sql|xml|svg|png|jpe?g|gif|webp|wasm|lock)$/i;
|
|
34
15
|
function stripAnchor(value) {
|
|
35
16
|
return value.split("#")[0] ?? value;
|
|
36
17
|
}
|
|
37
|
-
function
|
|
18
|
+
function isExplicitRelativePath(value) {
|
|
19
|
+
return value.startsWith("./") || value.startsWith("../");
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Conservative local path classification for markdown links / backticks.
|
|
23
|
+
* Precision is more important than recall for this rule.
|
|
24
|
+
*/
|
|
25
|
+
export function isLikelyLocalPathReference(value) {
|
|
38
26
|
if (!value || value.length > 200)
|
|
39
27
|
return false;
|
|
40
28
|
if (/^(https?:|mailto:|file:)/i.test(value))
|
|
41
29
|
return false;
|
|
42
30
|
if (value.startsWith("#"))
|
|
43
31
|
return false;
|
|
32
|
+
if (value.startsWith("--"))
|
|
33
|
+
return false;
|
|
44
34
|
if (value.includes("://"))
|
|
45
35
|
return false;
|
|
46
36
|
if (value.includes("\n") || value.includes(" "))
|
|
47
37
|
return false;
|
|
48
|
-
// Must look like a path: has / or a file extension
|
|
49
|
-
if (!value.includes("/") && !/\.[a-z0-9]{1,8}$/i.test(value))
|
|
50
|
-
return false;
|
|
51
|
-
// Reject shell-looking fragments
|
|
52
38
|
if (/[;&|$]/.test(value))
|
|
53
39
|
return false;
|
|
54
|
-
|
|
55
|
-
|
|
40
|
+
if (value.startsWith("/") || /^[A-Za-z]:[\\/]/.test(value))
|
|
41
|
+
return false;
|
|
42
|
+
// CSS class / id-like bare selectors: `.content`, `.button`
|
|
43
|
+
if (/^\.[A-Za-z_][\w-]*$/.test(value))
|
|
44
|
+
return false;
|
|
45
|
+
if (value.includes("/")) {
|
|
46
|
+
// Reject empty segments like `foo//bar` while allowing trailing slash dirs
|
|
47
|
+
const normalized = value.replace(/\/+$/, "");
|
|
48
|
+
if (!normalized || normalized.split("/").some((part) => part === "")) {
|
|
49
|
+
return false;
|
|
50
|
+
}
|
|
51
|
+
return true;
|
|
52
|
+
}
|
|
53
|
+
// Bare filename: require a known file extension (not `.content`-style tokens)
|
|
54
|
+
return BARE_FILE_EXTENSION.test(value) && !value.startsWith(".");
|
|
55
|
+
}
|
|
56
|
+
export function extractPathCandidates(text) {
|
|
57
|
+
const found = new Set();
|
|
58
|
+
for (const match of text.matchAll(/\[[^\]]*]\(([^)]+)\)/g)) {
|
|
59
|
+
const target = stripAnchor((match[1] ?? "").trim());
|
|
60
|
+
if (isLikelyLocalPathReference(target)) {
|
|
61
|
+
found.add(target);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
for (const match of text.matchAll(/`([^`]+)`/g)) {
|
|
65
|
+
const target = stripAnchor((match[1] ?? "").trim());
|
|
66
|
+
if (isLikelyLocalPathReference(target)) {
|
|
67
|
+
found.add(target);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
return [...found];
|
|
71
|
+
}
|
|
72
|
+
function isEscapingRepoRelative(relativePath) {
|
|
73
|
+
return relativePath === ".." || relativePath.startsWith("../");
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Resolve a candidate path reference with repository-aware semantics:
|
|
77
|
+
* - `./` and `../` → relative to the instruction file directory
|
|
78
|
+
* - otherwise → repository-root relative (avoids nesting under `.cursor/rules/`)
|
|
79
|
+
*/
|
|
80
|
+
export function resolveInstructionPathReference(instructionRelativePath, candidate) {
|
|
81
|
+
const instructionDir = path.posix.dirname(instructionRelativePath);
|
|
82
|
+
if (isExplicitRelativePath(candidate)) {
|
|
83
|
+
const joined = instructionDir === "."
|
|
84
|
+
? path.posix.normalize(candidate)
|
|
85
|
+
: path.posix.normalize(path.posix.join(instructionDir, candidate));
|
|
86
|
+
if (isEscapingRepoRelative(joined)) {
|
|
87
|
+
return { status: "escape", attempted: joined };
|
|
88
|
+
}
|
|
89
|
+
return {
|
|
90
|
+
status: "ok",
|
|
91
|
+
pathsToCheck: [joined],
|
|
92
|
+
primaryRelative: joined,
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
const rootRelative = path.posix.normalize(candidate);
|
|
96
|
+
if (isEscapingRepoRelative(rootRelative) || rootRelative.startsWith("/")) {
|
|
97
|
+
return { status: "escape", attempted: rootRelative };
|
|
98
|
+
}
|
|
99
|
+
return {
|
|
100
|
+
status: "ok",
|
|
101
|
+
pathsToCheck: [rootRelative],
|
|
102
|
+
primaryRelative: rootRelative,
|
|
103
|
+
};
|
|
56
104
|
}
|
|
57
105
|
export const missingPathReferenceRule = {
|
|
58
106
|
id: "instructions/missing-path-reference",
|
|
@@ -81,10 +129,8 @@ export const missingPathReferenceRule = {
|
|
|
81
129
|
}
|
|
82
130
|
const candidates = extractPathCandidates(cached.text);
|
|
83
131
|
for (const candidate of candidates) {
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
const resolvedRelative = path.posix.normalize(baseDir === "." ? candidate : path.posix.join(baseDir, candidate));
|
|
87
|
-
if (resolvedRelative.startsWith("../") || resolvedRelative === "..") {
|
|
132
|
+
const resolved = resolveInstructionPathReference(file.relativePath, candidate);
|
|
133
|
+
if (resolved.status === "escape") {
|
|
88
134
|
findings.push({
|
|
89
135
|
ruleId: "instructions/missing-path-reference",
|
|
90
136
|
category: "instructions",
|
|
@@ -101,11 +147,17 @@ export const missingPathReferenceRule = {
|
|
|
101
147
|
});
|
|
102
148
|
continue;
|
|
103
149
|
}
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
150
|
+
let exists = false;
|
|
151
|
+
for (const relativePath of resolved.pathsToCheck) {
|
|
152
|
+
const absolute = path.resolve(context.root, relativePath);
|
|
153
|
+
if (!isPathInsideRoot(context.root, absolute)) {
|
|
154
|
+
continue;
|
|
155
|
+
}
|
|
156
|
+
if (await pathExistsInsideRoot(context.root, relativePath)) {
|
|
157
|
+
exists = true;
|
|
158
|
+
break;
|
|
159
|
+
}
|
|
107
160
|
}
|
|
108
|
-
const exists = await pathExistsInsideRoot(context.root, resolvedRelative);
|
|
109
161
|
if (exists) {
|
|
110
162
|
continue;
|
|
111
163
|
}
|
|
@@ -120,7 +172,10 @@ export const missingPathReferenceRule = {
|
|
|
120
172
|
affectedAgents: context.agents
|
|
121
173
|
.filter((a) => a.configPaths.includes(file.relativePath))
|
|
122
174
|
.map((a) => a.id),
|
|
123
|
-
evidence: {
|
|
175
|
+
evidence: {
|
|
176
|
+
path: file.relativePath,
|
|
177
|
+
detail: `missing=${resolved.primaryRelative}`,
|
|
178
|
+
},
|
|
124
179
|
fixability: "manual",
|
|
125
180
|
});
|
|
126
181
|
}
|