agentsafe 0.1.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/.gitattributes +5 -0
- package/.github/workflows/ci.yml +34 -0
- package/CHANGELOG.md +13 -0
- package/CONTRIBUTING.md +23 -0
- package/LICENSE +21 -0
- package/README.md +128 -0
- package/SECURITY.md +22 -0
- package/dist/cli.d.ts +7 -0
- package/dist/cli.js +77 -0
- package/dist/cli.js.map +1 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +4 -0
- package/dist/index.js.map +1 -0
- package/dist/rules/report.d.ts +4 -0
- package/dist/rules/report.js +71 -0
- package/dist/rules/report.js.map +1 -0
- package/dist/rules/scan.d.ts +2 -0
- package/dist/rules/scan.js +459 -0
- package/dist/rules/scan.js.map +1 -0
- package/dist/rules/types.d.ts +27 -0
- package/dist/rules/types.js +2 -0
- package/dist/rules/types.js.map +1 -0
- package/examples/safe-rules/.cursor/rules/typescript.mdc +15 -0
- package/examples/safe-rules/.cursorrules +1 -0
- package/examples/safe-rules/AGENTS.md +3 -0
- package/examples/safe-rules/CLAUDE.md +3 -0
- package/examples/safe-rules/GEMINI.md +3 -0
- package/examples/unsafe-rules/.cursor/rules/always-1.mdc +6 -0
- package/examples/unsafe-rules/.cursor/rules/always-2.mdc +6 -0
- package/examples/unsafe-rules/.cursor/rules/always-3.mdc +6 -0
- package/examples/unsafe-rules/.cursor/rules/always-4.mdc +6 -0
- package/examples/unsafe-rules/.cursor/rules/always-5.mdc +6 -0
- package/examples/unsafe-rules/.cursor/rules/always-6.mdc +6 -0
- package/examples/unsafe-rules/.cursor/rules/duplicate-a.mdc +6 -0
- package/examples/unsafe-rules/.cursor/rules/duplicate-b.mdc +7 -0
- package/examples/unsafe-rules/.cursor/rules/empty.mdc +5 -0
- package/examples/unsafe-rules/.cursor/rules/invalid-fields.mdc +7 -0
- package/examples/unsafe-rules/.cursor/rules/invalid-yaml.mdc +6 -0
- package/examples/unsafe-rules/.cursor/rules/no-frontmatter.mdc +1 -0
- package/examples/unsafe-rules/.cursorrules +1 -0
- package/examples/unsafe-rules/AGENTS.md +4 -0
- package/examples/unsafe-rules/CLAUDE.md +13 -0
- package/examples/unsafe-rules/GEMINI.md +3 -0
- package/package.json +26 -0
- package/src/cli.ts +104 -0
- package/src/index.ts +5 -0
- package/src/rules/report.ts +83 -0
- package/src/rules/scan.ts +544 -0
- package/src/rules/types.ts +36 -0
- package/test/scanner.test.mjs +136 -0
- package/tsconfig.json +17 -0
package/package.json
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "agentsafe",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Offline safety and hygiene toolkit for AI coding workspaces",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"agentsafe": "dist/index.js"
|
|
8
|
+
},
|
|
9
|
+
"scripts": {
|
|
10
|
+
"build": "tsc -p tsconfig.json",
|
|
11
|
+
"typecheck": "tsc -p tsconfig.json --noEmit",
|
|
12
|
+
"test": "npm run build && node --test",
|
|
13
|
+
"scan:example": "npm run build --silent && node dist/index.js rules scan examples/unsafe-rules --markdown"
|
|
14
|
+
},
|
|
15
|
+
"engines": {
|
|
16
|
+
"node": ">=20"
|
|
17
|
+
},
|
|
18
|
+
"dependencies": {
|
|
19
|
+
"yaml": "^2.8.0"
|
|
20
|
+
},
|
|
21
|
+
"devDependencies": {
|
|
22
|
+
"@types/node": "^22.15.0",
|
|
23
|
+
"typescript": "^5.8.3"
|
|
24
|
+
},
|
|
25
|
+
"license": "MIT"
|
|
26
|
+
}
|
package/src/cli.ts
ADDED
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { pathToFileURL } from "node:url";
|
|
4
|
+
import { resolve } from "node:path";
|
|
5
|
+
|
|
6
|
+
import {
|
|
7
|
+
formatJsonReport,
|
|
8
|
+
formatMarkdownReport,
|
|
9
|
+
formatTextReport,
|
|
10
|
+
} from "./rules/report.js";
|
|
11
|
+
import { scanRules } from "./rules/scan.js";
|
|
12
|
+
|
|
13
|
+
type OutputFormat = "text" | "json" | "markdown";
|
|
14
|
+
|
|
15
|
+
interface CliOptions {
|
|
16
|
+
directory: string;
|
|
17
|
+
format: OutputFormat;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
interface CliOutput {
|
|
21
|
+
stdout(value: string): void;
|
|
22
|
+
stderr(value: string): void;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const usage = `Usage:
|
|
26
|
+
agentsafe rules scan [directory] [--json | --markdown]
|
|
27
|
+
|
|
28
|
+
Scans AI rule files using local, static analysis only.
|
|
29
|
+
`;
|
|
30
|
+
|
|
31
|
+
export async function runCli(
|
|
32
|
+
args: string[],
|
|
33
|
+
cwd = process.cwd(),
|
|
34
|
+
output: CliOutput = {
|
|
35
|
+
stdout: (value) => process.stdout.write(value),
|
|
36
|
+
stderr: (value) => process.stderr.write(value),
|
|
37
|
+
},
|
|
38
|
+
): Promise<number> {
|
|
39
|
+
if (args.includes("--help") || args.includes("-h")) {
|
|
40
|
+
output.stdout(usage);
|
|
41
|
+
return 0;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
let options: CliOptions;
|
|
45
|
+
try {
|
|
46
|
+
options = parseArguments(args);
|
|
47
|
+
} catch (error) {
|
|
48
|
+
output.stderr(`${getErrorMessage(error)}\n\n${usage}`);
|
|
49
|
+
return 1;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
try {
|
|
53
|
+
const report = await scanRules(resolve(cwd, options.directory));
|
|
54
|
+
const formatted =
|
|
55
|
+
options.format === "json"
|
|
56
|
+
? formatJsonReport(report)
|
|
57
|
+
: options.format === "markdown"
|
|
58
|
+
? formatMarkdownReport(report)
|
|
59
|
+
: formatTextReport(report);
|
|
60
|
+
output.stdout(formatted);
|
|
61
|
+
return 0;
|
|
62
|
+
} catch (error) {
|
|
63
|
+
output.stderr(`AgentSafe scan failed: ${getErrorMessage(error)}\n`);
|
|
64
|
+
return 1;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function parseArguments(args: string[]): CliOptions {
|
|
69
|
+
if (args[0] !== "rules" || args[1] !== "scan") {
|
|
70
|
+
throw new Error("Unknown command.");
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
let format: OutputFormat = "text";
|
|
74
|
+
let directory = ".";
|
|
75
|
+
let hasDirectory = false;
|
|
76
|
+
|
|
77
|
+
for (const argument of args.slice(2)) {
|
|
78
|
+
if (argument === "--json" || argument === "--markdown") {
|
|
79
|
+
const requestedFormat = argument.slice(2) as OutputFormat;
|
|
80
|
+
if (format !== "text") {
|
|
81
|
+
throw new Error("Use only one report format flag.");
|
|
82
|
+
}
|
|
83
|
+
format = requestedFormat;
|
|
84
|
+
} else if (argument.startsWith("-")) {
|
|
85
|
+
throw new Error(`Unknown option: ${argument}`);
|
|
86
|
+
} else if (hasDirectory) {
|
|
87
|
+
throw new Error("Only one scan directory may be provided.");
|
|
88
|
+
} else {
|
|
89
|
+
directory = argument;
|
|
90
|
+
hasDirectory = true;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
return { directory, format };
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function getErrorMessage(error: unknown): string {
|
|
98
|
+
return error instanceof Error ? error.message : "Unknown error.";
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const invokedPath = process.argv[1];
|
|
102
|
+
if (invokedPath && pathToFileURL(invokedPath).href === import.meta.url) {
|
|
103
|
+
process.exitCode = await runCli(process.argv.slice(2));
|
|
104
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import type { Finding, ScanReport } from "./types.js";
|
|
2
|
+
|
|
3
|
+
export function formatJsonReport(report: ScanReport): string {
|
|
4
|
+
return `${JSON.stringify(report, null, 2)}\n`;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export function formatMarkdownReport(report: ScanReport): string {
|
|
8
|
+
const lines = [
|
|
9
|
+
"# AgentSafe Rules Scan",
|
|
10
|
+
"",
|
|
11
|
+
`- Target: \`${report.target}\``,
|
|
12
|
+
`- Files scanned: ${report.summary.files}`,
|
|
13
|
+
`- High severity: ${report.summary.high}`,
|
|
14
|
+
`- Warnings: ${report.summary.warnings}`,
|
|
15
|
+
`- Total findings: ${report.summary.total}`,
|
|
16
|
+
"",
|
|
17
|
+
"## Findings",
|
|
18
|
+
"",
|
|
19
|
+
];
|
|
20
|
+
|
|
21
|
+
if (report.findings.length === 0) {
|
|
22
|
+
lines.push("_No findings._");
|
|
23
|
+
} else {
|
|
24
|
+
lines.push("| Severity | Rule | Location | Message |");
|
|
25
|
+
lines.push("| --- | --- | --- | --- |");
|
|
26
|
+
for (const finding of report.findings) {
|
|
27
|
+
lines.push(
|
|
28
|
+
`| ${finding.severity.toUpperCase()} | \`${escapeMarkdown(finding.code)}\` | ${formatMarkdownLocation(finding)} | ${escapeMarkdown(finding.message)} |`,
|
|
29
|
+
);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
lines.push("", "## Scanned Files", "");
|
|
34
|
+
if (report.files.length === 0) {
|
|
35
|
+
lines.push("_No supported rule files found._");
|
|
36
|
+
} else {
|
|
37
|
+
for (const file of report.files) {
|
|
38
|
+
lines.push(`- \`${escapeMarkdown(file)}\``);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
return `${lines.join("\n")}\n`;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function formatTextReport(report: ScanReport): string {
|
|
46
|
+
const lines = [
|
|
47
|
+
"AgentSafe Rules Scan",
|
|
48
|
+
`Files scanned: ${report.summary.files}`,
|
|
49
|
+
`Findings: ${report.summary.total} (${report.summary.high} high, ${report.summary.warnings} warnings)`,
|
|
50
|
+
];
|
|
51
|
+
|
|
52
|
+
if (report.findings.length === 0) {
|
|
53
|
+
lines.push("", "No findings.");
|
|
54
|
+
} else {
|
|
55
|
+
lines.push("");
|
|
56
|
+
for (const finding of report.findings) {
|
|
57
|
+
lines.push(
|
|
58
|
+
`[${finding.severity.toUpperCase()}] ${formatTextLocation(finding)} ${finding.code}: ${finding.message}`,
|
|
59
|
+
);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
return `${lines.join("\n")}\n`;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function formatMarkdownLocation(finding: Finding): string {
|
|
67
|
+
if (!finding.path) {
|
|
68
|
+
return "project";
|
|
69
|
+
}
|
|
70
|
+
const location = finding.line ? `${finding.path}:${finding.line}` : finding.path;
|
|
71
|
+
return `\`${escapeMarkdown(location)}\``;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function formatTextLocation(finding: Finding): string {
|
|
75
|
+
if (!finding.path) {
|
|
76
|
+
return "project";
|
|
77
|
+
}
|
|
78
|
+
return finding.line ? `${finding.path}:${finding.line}` : finding.path;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function escapeMarkdown(value: string): string {
|
|
82
|
+
return value.replace(/\\/g, "\\\\").replace(/\|/g, "\\|").replace(/`/g, "\\`");
|
|
83
|
+
}
|