@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
- ### Changed
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
- - Publish as scoped package `@praneeth_54/agentdoctor` (unscoped `agentdoctor` blocked by npm as too similar to `agent-doctor`)
18
+ Patch release.
13
19
 
14
20
  ### Fixed
15
21
 
16
- - Track fake credential fixtures required by CI (were excluded by `.gitignore`)
17
- - Align package and docs URLs with the public GitHub repository
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
- ### Planned
25
+ ### Testing
22
26
 
23
- - Deterministic readiness scoring
24
- - Safe automatic fixes
25
- - Packaged GitHub Action
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.0-beta...HEAD
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
+ [![npm](https://img.shields.io/npm/v/@praneeth_54/agentdoctor)](https://www.npmjs.com/package/@praneeth_54/agentdoctor)
4
+ [![License](https://img.shields.io/github/license/pranee54/AgentDoctor)](LICENSE)
5
+ [![GitHub Stars](https://img.shields.io/github/stars/pranee54/AgentDoctor)](https://github.com/pranee54/AgentDoctor/stargazers)
6
+ [![CI](https://img.shields.io/github/actions/workflow/status/pranee54/AgentDoctor/ci.yml?branch=main&label=CI)](https://github.com/pranee54/AgentDoctor/actions)
7
+ [![Node](https://img.shields.io/node/v/@praneeth_54/agentdoctor)](https://nodejs.org)
8
+ [![Release](https://img.shields.io/badge/release-v0.1.1--beta-orange)](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
- [![CI](https://img.shields.io/github/actions/workflow/status/pranee54/AgentDoctor/ci.yml?branch=main&label=CI)](https://github.com/pranee54/AgentDoctor/actions)
19
- [![npm](https://img.shields.io/npm/v/@praneeth_54/agentdoctor?label=npm)](https://www.npmjs.com/package/@praneeth_54/agentdoctor)
20
- [![Node](https://img.shields.io/node/v/@praneeth_54/agentdoctor)](https://nodejs.org)
21
- [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](LICENSE)
22
- [![Release](https://img.shields.io/badge/release-v0.1.0--beta-orange)](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.0-beta
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.0-beta",
228
+ "version": "0.1.1-beta",
228
229
  "repository": {
229
230
  "primaryFramework": "nextjs",
230
231
  "primaryLanguage": "typescript",
@@ -1,4 +1,4 @@
1
- export declare const PACKAGE_VERSION = "0.1.0-beta";
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.0-beta";
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
- * Conservative local path extraction:
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 isConservativeLocalPath(value) {
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
- // Reject traversal escape attempts — still check but flag separately
55
- return true;
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
- // Resolve relative to instruction file directory, stay inside root
85
- const baseDir = path.posix.dirname(file.relativePath);
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
- const absolute = path.resolve(context.root, resolvedRelative);
105
- if (!isPathInsideRoot(context.root, absolute)) {
106
- continue;
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: { path: file.relativePath, detail: `missing=${resolvedRelative}` },
175
+ evidence: {
176
+ path: file.relativePath,
177
+ detail: `missing=${resolved.primaryRelative}`,
178
+ },
124
179
  fixability: "manual",
125
180
  });
126
181
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@praneeth_54/agentdoctor",
3
- "version": "0.1.0-beta",
3
+ "version": "0.1.1-beta",
4
4
  "description": "Audit AI coding agent configuration in a repository — local, deterministic, no API key.",
5
5
  "type": "module",
6
6
  "bin": {