ai-guard-plugins 1.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.
@@ -0,0 +1,124 @@
1
+ import type { Plugin } from '@opencode-ai/plugin';
2
+
3
+ const SKIP_PATHS = ['node_modules', 'dist', '.next', 'build', '.turbo', 'migrations'];
4
+ const CODE_EXTS = new Set(['ts', 'tsx', 'js', 'jsx', 'mts', 'mjs', 'cts', 'cjs']);
5
+ const DOC_FILES = new Set(['readme.md', 'readme', 'changelog.md', 'contributing.md']);
6
+
7
+ interface Violation {
8
+ rule: string;
9
+ message: string;
10
+ }
11
+
12
+ function check(content: string, filePath: string): Violation[] {
13
+ const violations: Violation[] = [];
14
+ const basename = filePath.split('/').pop() ?? '';
15
+ const ext = basename.includes('.') ? (basename.split('.').pop()?.toLowerCase() ?? '') : '';
16
+ const isIndex = /^index\.(ts|tsx|js|jsx|mts|mjs|cts|cjs)$/i.test(basename);
17
+
18
+ if (DOC_FILES.has(basename.toLowerCase())) {
19
+ violations.push({
20
+ rule: 'DOCUMENTATION',
21
+ message: `Creating ${basename}. Do not create documentation files unless explicitly requested.`,
22
+ });
23
+ }
24
+
25
+ if (basename.startsWith('_')) {
26
+ violations.push({
27
+ rule: 'UNDERSCORE FILE',
28
+ message: `Filename starts with underscore (${basename}). Do not use underscore prefixes.`,
29
+ });
30
+ }
31
+
32
+ if (!CODE_EXTS.has(ext)) return violations;
33
+
34
+ if (/\bas\s+(any|unknown)\b/.test(content)) {
35
+ violations.push({
36
+ rule: 'TYPE CAST',
37
+ message: "'as any' or 'as unknown' found. Use the most specific correct type.",
38
+ });
39
+ }
40
+
41
+ if (/\bas\s+[A-Z][A-Za-z]+/.test(content)) {
42
+ violations.push({
43
+ rule: 'TYPE ASSERTION',
44
+ message: "'as Type' found. Avoid type assertions — use type narrowing or generics instead.",
45
+ });
46
+ }
47
+
48
+ if (/\brequire\s*\(/.test(content)) {
49
+ violations.push({ rule: 'REQUIRE', message: 'require() found. Use static imports.' });
50
+ }
51
+
52
+ if (/@ts-ignore|@ts-expect-error/.test(content)) {
53
+ violations.push({
54
+ rule: 'TS SUPPRESSION',
55
+ message: '@ts-ignore or @ts-expect-error found. Fix the type error properly.',
56
+ });
57
+ }
58
+
59
+ if (/\bTODO\b|\bFIXME\b|\bHACK\b|\bplaceholder\b|\/\/\s*later\b/i.test(content)) {
60
+ violations.push({
61
+ rule: 'PLACEHOLDER',
62
+ message: 'TODO/FIXME/placeholder/later found. Production-ready code only.',
63
+ });
64
+ }
65
+
66
+ if (/(const|let|var|function)\s+_[a-zA-Z]/.test(content)) {
67
+ violations.push({
68
+ rule: 'UNDERSCORE NAMING',
69
+ message: 'Variable/function starting with underscore. Do not use underscore prefixes.',
70
+ });
71
+ }
72
+
73
+ if (isIndex) {
74
+ const lines = content
75
+ .split('\n')
76
+ .filter(
77
+ (l) =>
78
+ l.trim() &&
79
+ !/^\s*(export\s+[{*]|export\s+type|export\s+default|import\s|\/\/|\/\*)/.test(l),
80
+ );
81
+ if (lines.length > 2) {
82
+ violations.push({
83
+ rule: 'INDEX LOGIC',
84
+ message: 'Logic found in index file. Index files may only re-export modules.',
85
+ });
86
+ }
87
+ }
88
+
89
+ if (!isIndex) {
90
+ const reexports = (content.match(/export\s+\{[^}]*\}\s+from\s+/g) || []).length;
91
+ if (reexports > 2) {
92
+ violations.push({ rule: 'RE-EXPORTS', message: 'Multiple re-exports in non-index file.' });
93
+ }
94
+ }
95
+
96
+ return violations;
97
+ }
98
+
99
+ export const CursorrulesEnforcerPlugin: Plugin = async () => {
100
+ return {
101
+ 'tool.execute.before': async (input, output) => {
102
+ if (input.tool !== 'write' && input.tool !== 'edit') return;
103
+
104
+ const args = (output?.args ?? input.args) as Record<string, unknown> | undefined;
105
+ if (!args) return;
106
+
107
+ const filePath = (args.filePath ?? args.file_path) as string | undefined;
108
+ if (!filePath) return;
109
+ const normalizedPath = filePath.replace(/\\/g, '/');
110
+ if (SKIP_PATHS.some((p) => normalizedPath.split('/').includes(p))) return;
111
+
112
+ const content = (input.tool === "edit" ? (args.newString ?? args.new_string ?? "") : (args.content ?? "")) as string;
113
+ if (!content) return;
114
+
115
+ const violations = check(content, filePath);
116
+ if (violations.length === 0) return;
117
+
118
+ const msg = violations.map((v) => `- ${v.rule}: ${v.message}`).join('\n');
119
+ throw new Error(
120
+ `CURSORRULES VIOLATIONS in ${filePath.split('/').pop()}:\n${msg}\n\nFix these before proceeding.`,
121
+ );
122
+ },
123
+ };
124
+ };
@@ -0,0 +1,107 @@
1
+ import type { Plugin } from '@opencode-ai/plugin';
2
+
3
+ const CODE_EXTS = new Set(['.ts', '.tsx', '.js', '.jsx']);
4
+ const SKIP_PATHS = ['node_modules', 'dist', '.next', 'build', '.turbo', 'migrations'];
5
+ const SAFE_TOKEN = /^[a-z0-9]+$/;
6
+
7
+ function getExt(filePath: string): string {
8
+ const dot = filePath.lastIndexOf('.');
9
+ return dot === -1 ? '' : filePath.slice(dot);
10
+ }
11
+
12
+ function tokenize(filename: string): string[] {
13
+ const noExt = filename.replace(/\.[^.]+$/, '');
14
+ return noExt
15
+ .split(/[-_.]/)
16
+ .map((t) => t.toLowerCase())
17
+ .filter((t) => t.length >= 3 && SAFE_TOKEN.test(t));
18
+ }
19
+
20
+ function toPascalCase(name: string): string {
21
+ return name
22
+ .replace(/\.[^.]+$/, '')
23
+ .split(/[-_.]/)
24
+ .map((w) => w.charAt(0).toUpperCase() + w.slice(1))
25
+ .join('');
26
+ }
27
+
28
+ export const DryGuardPlugin: Plugin = async ({ $, directory }) => {
29
+ return {
30
+ 'tool.execute.before': async (input, output) => {
31
+ if (input.tool !== 'write') return;
32
+
33
+ const args = (output?.args ?? input.args) as Record<string, unknown> | undefined;
34
+ if (!args) return;
35
+
36
+ const filePath = (args.filePath ?? args.file_path) as string | undefined;
37
+ if (!filePath) return;
38
+
39
+ if (!CODE_EXTS.has(getExt(filePath))) return;
40
+ const normalizedPath = filePath.replace(/\\/g, '/');
41
+ if (SKIP_PATHS.some((p) => normalizedPath.split('/').includes(p))) return;
42
+
43
+ const basename = filePath.split('/').pop() ?? '';
44
+ const rawDirname = filePath.split('/').slice(0, -1).join('/');
45
+ const dirname = rawDirname.startsWith('/') ? rawDirname : `${directory}/${rawDirname}`;
46
+ const tokens = tokenize(basename);
47
+ const projectDir = directory;
48
+
49
+ const warnings: string[] = [];
50
+
51
+ try {
52
+ const similarFiles = new Set<string>();
53
+ for (const token of tokens) {
54
+ const result =
55
+ await $`find ${projectDir} -type f \( -name "*.ts" -o -name "*.tsx" -o -name "*.js" -o -name "*.jsx" \) ! -path "*/node_modules/*" ! -path "*/dist/*" ! -path "*/.next/*" ! -path "*/.turbo/*" 2>/dev/null | grep -Fi -- ${token} | grep -Fv -- ${filePath} | head -8`
56
+ .quiet()
57
+ .nothrow();
58
+ const found = String(result.stdout).trim();
59
+ if (found) {
60
+ for (const f of found.split('\n')) {
61
+ if (f.trim()) similarFiles.add(f.trim());
62
+ }
63
+ }
64
+ }
65
+
66
+ if (similarFiles.size > 0) {
67
+ const files = [...similarFiles].slice(0, 10).join('\n');
68
+ warnings.push(
69
+ `SIMILAR FILES (${similarFiles.size} found matching tokens from '${basename}'):\n${files}`,
70
+ );
71
+ }
72
+
73
+ if (dirname) {
74
+ const existing =
75
+ await $`find ${dirname} -maxdepth 1 -type f \( -name "*.ts" -o -name "*.tsx" -o -name "*.js" -o -name "*.jsx" \) 2>/dev/null`
76
+ .quiet()
77
+ .nothrow();
78
+ const existingFiles = String(existing.stdout).trim().split('\n').filter(Boolean);
79
+ if (existingFiles.length > 0) {
80
+ warnings.push(
81
+ `TARGET DIR already has ${existingFiles.length} file(s):\n${existingFiles.slice(0, 15).join('\n')}`,
82
+ );
83
+ }
84
+ }
85
+
86
+ const likelyExport = toPascalCase(basename);
87
+ if (likelyExport.length >= 3 && SAFE_TOKEN.test(likelyExport.toLowerCase())) {
88
+ const collisions =
89
+ await $`grep -rEl -- "export.*${likelyExport}" ${projectDir}/src ${projectDir}/packages ${projectDir}/lib ${projectDir}/app 2>/dev/null | grep -Fv -- ${filePath} | grep -v node_modules | head -5`
90
+ .quiet()
91
+ .nothrow();
92
+ const found = String(collisions.stdout).trim();
93
+ if (found) {
94
+ warnings.push(`SYMBOL COLLISION — '${likelyExport}' already exported from:\n${found}`);
95
+ }
96
+ }
97
+ } catch {
98
+ return;
99
+ }
100
+
101
+ if (warnings.length === 0) return;
102
+
103
+ const message = `[DRY-GUARD] Before creating ${basename}:\n\n${warnings.join('\n\n')}\n\nConsider extending an existing file instead of creating a new one.`;
104
+ console.warn(message);
105
+ },
106
+ };
107
+ };
@@ -0,0 +1,15 @@
1
+ # Auto Lint Fix
2
+
3
+ After EVERY file write or edit to a TypeScript/JavaScript file, run:
4
+
5
+ ```bash
6
+ "${BIOME_PATH:-biome}" check --fix --unsafe <filepath>
7
+ ```
8
+
9
+ ## Rules
10
+
11
+ - Only run on code files: .ts, .tsx, .js, .jsx, .mts, .mjs, .cts, .cjs
12
+ - Skip files in: node_modules, dist, .next, build, .turbo
13
+ - If `BIOME_PATH` environment variable is set, use that path instead of `biome`
14
+ - If Biome is not installed, skip formatting (do not fail)
15
+ - Do not manually fix formatting issues that Biome handles automatically
@@ -0,0 +1,12 @@
1
+ # Auto Summarize Input
2
+
3
+ When receiving input that exceeds 10,000 characters, summarize it before processing.
4
+
5
+ ## Summarization rules
6
+
7
+ - Preserve ALL code snippets verbatim
8
+ - Preserve ALL file paths verbatim
9
+ - Preserve ALL error messages verbatim
10
+ - Preserve ALL requirements and specifications verbatim
11
+ - Remove ONLY: redundancy, repetition, and filler text
12
+ - Note the original character count and reduction percentage
@@ -0,0 +1,21 @@
1
+ # Billing Guard
2
+
3
+ If ANY API call, tool execution, or command output contains text matching these patterns, IMMEDIATELY stop all operations:
4
+
5
+ - "no payment method" or "add a payment method"
6
+ - "payment required"
7
+ - "account is not active" with "billing"
8
+ - "billing details"
9
+ - "quota exceeded"
10
+ - "insufficient funds"
11
+ - "rate limit" with "billing"
12
+ - "plan expired"
13
+ - "subscription inactive"
14
+
15
+ ## Required behavior
16
+
17
+ 1. STOP all current operations immediately
18
+ 2. Do NOT retry the failed operation
19
+ 3. Do NOT delegate or spawn sub-tasks
20
+ 4. Notify the user that billing needs to be fixed before continuing
21
+ 5. Do NOT attempt workarounds or alternative approaches — the billing issue must be resolved first
@@ -0,0 +1,27 @@
1
+ # Capture Insights
2
+
3
+ Monitor your own outputs and actions for these recurring anti-patterns. When detected, apply the corresponding lesson immediately.
4
+
5
+ ## Tracked patterns
6
+
7
+ | Pattern | Lesson |
8
+ |---------|--------|
9
+ | DRY violation ("already exists", "duplicate") | Check existing files before creating new ones. Extract shared utils for clear reuse. |
10
+ | Unused code (imports, variables, dead code) | Biome --fix handles unused imports/variables automatically. |
11
+ | Type safety bypass (as any, ts-ignore) | Never use as any or ts-ignore. Use the most specific correct type. |
12
+ | Scope creep (refactoring while fixing) | Make the minimal change needed. Do not refactor while fixing. |
13
+ | Circular dependencies | Restructure module boundaries to break circular dependencies. |
14
+ | Re-export overuse (barrel files with logic) | Index files may only re-export modules, never contain logic. |
15
+ | Path resolution (process.cwd assumptions) | Never assume process.cwd() equals project root in monorepos. |
16
+ | Migration issues | Verify schema and data integrity after migrations. Make migrations idempotent. |
17
+ | Workarounds and hacks | Document why the workaround was needed and plan a proper fix. |
18
+ | Performance guessing | Profile before optimizing. Measure, do not guess. |
19
+ | Security (secrets, credentials, tokens) | Review credentials handling. Never commit secrets. |
20
+ | Test mock overreach | Mock behavior (modules), not implementation details. |
21
+
22
+ ## Required behavior
23
+
24
+ When you detect one of these patterns in your own work:
25
+ 1. Stop and acknowledge the pattern
26
+ 2. Apply the corresponding lesson
27
+ 3. Correct the action before proceeding
@@ -0,0 +1,71 @@
1
+ # Code Standards Enforcer
2
+
3
+ These rules apply to every code file write and edit. Violations must be fixed before proceeding.
4
+
5
+ ## Type safety
6
+
7
+ - Never use `as any` or `as unknown`. Use the most specific correct type.
8
+ - Never use `as Type` assertions. Use type narrowing, generics, or satisfies instead.
9
+ - Never use `@ts-ignore` or `@ts-expect-error`. Fix the underlying type error.
10
+
11
+ ```typescript
12
+ // Bad
13
+ const data = response as any;
14
+ const user = getUser() as UserType;
15
+ // @ts-ignore
16
+ brokenCall();
17
+
18
+ // Good
19
+ const data: ApiResponse = response;
20
+ const user = getUser(); // return type already UserType
21
+ ```
22
+
23
+ ## Import style
24
+
25
+ - Never use `require()`. Use static `import` statements.
26
+
27
+ ```typescript
28
+ // Bad
29
+ const fs = require('fs');
30
+
31
+ // Good
32
+ import fs from 'node:fs';
33
+ ```
34
+
35
+ ## No placeholders
36
+
37
+ - Never leave `TODO`, `FIXME`, `HACK`, `placeholder`, or `// later` comments. All code must be production-ready.
38
+
39
+ ## Naming
40
+
41
+ - Never prefix variables or functions with underscore (`_`).
42
+ - Never prefix filenames with underscore.
43
+
44
+ ```typescript
45
+ // Bad
46
+ const _count = 0;
47
+ function _helper() {}
48
+
49
+ // Good
50
+ const count = 0;
51
+ function helper() {}
52
+ ```
53
+
54
+ ## Index files
55
+
56
+ - Index/barrel files (`index.ts`, `index.js`) may ONLY contain re-exports. No business logic.
57
+ - Non-index files may have at most 2 re-export statements.
58
+
59
+ ```typescript
60
+ // Good index.ts
61
+ export { UserService } from './user-service';
62
+ export { AuthService } from './auth-service';
63
+
64
+ // Bad index.ts
65
+ export { UserService } from './user-service';
66
+ const cache = new Map(); // logic does not belong here
67
+ ```
68
+
69
+ ## Documentation files
70
+
71
+ - Do not create README.md, CHANGELOG.md, or CONTRIBUTING.md unless explicitly requested.
@@ -0,0 +1,25 @@
1
+ # DRY Guard
2
+
3
+ Before creating ANY new code file (.ts, .tsx, .js, .jsx), perform these checks:
4
+
5
+ ## 1. Similar file search
6
+
7
+ Tokenize the new filename by splitting on `-`, `_`, and `.` (ignore tokens shorter than 3 characters). Search the project for existing files whose names contain any of those tokens.
8
+
9
+ Skip directories: node_modules, dist, .next, build, .turbo, migrations
10
+
11
+ ## 2. Target directory check
12
+
13
+ List existing code files in the target directory. If the directory already contains files, consider whether the new file's logic belongs in one of them.
14
+
15
+ ## 3. Symbol collision check
16
+
17
+ Convert the filename to PascalCase (e.g., `format-date.ts` becomes `FormatDate`). Search `src/`, `packages/`, `lib/`, and `app/` for existing exports matching that name.
18
+
19
+ ## Required behavior
20
+
21
+ - If similar files are found: review them before creating a new file
22
+ - If the symbol already exists: extend the existing file instead
23
+ - If the target directory has related files: consolidate rather than create
24
+
25
+ Do NOT create a new file when extending an existing one would be cleaner.