@ateriss_/aiv-cli 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/README.md +954 -0
- package/dist/index.js +318 -0
- package/package.json +41 -0
- package/src/agents/architecture.ts +32 -0
- package/src/agents/base.ts +125 -0
- package/src/agents/business.ts +32 -0
- package/src/agents/index.ts +4 -0
- package/src/agents/security.ts +33 -0
- package/src/cache/index.ts +47 -0
- package/src/cli/banner.ts +32 -0
- package/src/cli/commands/agents.ts +49 -0
- package/src/cli/commands/config.ts +426 -0
- package/src/cli/commands/context.ts +131 -0
- package/src/cli/commands/init.ts +117 -0
- package/src/cli/commands/prs.ts +118 -0
- package/src/cli/commands/review.ts +171 -0
- package/src/cli/renderer.ts +102 -0
- package/src/cli/selector.ts +78 -0
- package/src/config/index.ts +241 -0
- package/src/context/builder.ts +199 -0
- package/src/context/generator.ts +138 -0
- package/src/context/manager.ts +83 -0
- package/src/context/tree.ts +90 -0
- package/src/git/github.ts +112 -0
- package/src/git/utils.ts +51 -0
- package/src/i18n/en.ts +203 -0
- package/src/i18n/es.ts +203 -0
- package/src/i18n/index.ts +57 -0
- package/src/index.ts +29 -0
- package/src/orchestrator/index.ts +110 -0
- package/src/providers/base.ts +16 -0
- package/src/providers/claude.ts +36 -0
- package/src/providers/factory.ts +84 -0
- package/src/providers/fallback.ts +47 -0
- package/src/providers/gemini.ts +58 -0
- package/src/providers/mock.ts +27 -0
- package/src/providers/openai.ts +41 -0
- package/src/types.ts +175 -0
- package/tsconfig.json +17 -0
- package/tsup.config.ts +12 -0
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
import { AgentResult, AgentFinding, PRDiff, AivRules } from '../types';
|
|
2
|
+
import { LLMProvider } from '../providers/base';
|
|
3
|
+
|
|
4
|
+
export interface AgentContext {
|
|
5
|
+
projectContext: string;
|
|
6
|
+
rules: AivRules;
|
|
7
|
+
diff: PRDiff;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export abstract class BaseAgent {
|
|
11
|
+
abstract readonly agentName: string;
|
|
12
|
+
abstract readonly systemPrompt: string;
|
|
13
|
+
|
|
14
|
+
constructor(protected provider: LLMProvider) {}
|
|
15
|
+
|
|
16
|
+
async run(ctx: AgentContext): Promise<AgentResult> {
|
|
17
|
+
const userMessage = this.buildUserMessage(ctx);
|
|
18
|
+
|
|
19
|
+
const response = await this.provider.complete(
|
|
20
|
+
[{ role: 'user', content: userMessage }],
|
|
21
|
+
this.systemPrompt,
|
|
22
|
+
4096,
|
|
23
|
+
);
|
|
24
|
+
|
|
25
|
+
return this.parseResponse(response.content);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
protected buildUserMessage(ctx: AgentContext): string {
|
|
29
|
+
const rulesSection = JSON.stringify(ctx.rules, null, 2);
|
|
30
|
+
const filesSummary = ctx.diff.files
|
|
31
|
+
.map(f => `${f.status.toUpperCase()} ${f.filename} (+${f.additions}/-${f.deletions})`)
|
|
32
|
+
.join('\n');
|
|
33
|
+
|
|
34
|
+
const patches = ctx.diff.files
|
|
35
|
+
.filter(f => f.patch)
|
|
36
|
+
.map(f => `### ${f.filename}\n\`\`\`diff\n${f.patch}\n\`\`\``)
|
|
37
|
+
.join('\n\n');
|
|
38
|
+
|
|
39
|
+
return `## PR: #${ctx.diff.pr.number} — ${ctx.diff.pr.title}
|
|
40
|
+
|
|
41
|
+
**Author:** ${ctx.diff.pr.author}
|
|
42
|
+
**Branch:** ${ctx.diff.pr.branch} → ${ctx.diff.pr.base}
|
|
43
|
+
**Description:** ${ctx.diff.pr.description ?? 'No description provided.'}
|
|
44
|
+
|
|
45
|
+
---
|
|
46
|
+
|
|
47
|
+
## Project Context
|
|
48
|
+
${ctx.projectContext}
|
|
49
|
+
|
|
50
|
+
---
|
|
51
|
+
|
|
52
|
+
## Business Rules (from .aiv/rules.yml)
|
|
53
|
+
\`\`\`json
|
|
54
|
+
${rulesSection}
|
|
55
|
+
\`\`\`
|
|
56
|
+
|
|
57
|
+
---
|
|
58
|
+
|
|
59
|
+
## Files Changed (${ctx.diff.files.length} files)
|
|
60
|
+
${filesSummary}
|
|
61
|
+
|
|
62
|
+
---
|
|
63
|
+
|
|
64
|
+
## Diff
|
|
65
|
+
${patches}
|
|
66
|
+
|
|
67
|
+
---
|
|
68
|
+
|
|
69
|
+
Analyze the above and return a JSON response matching this schema:
|
|
70
|
+
{
|
|
71
|
+
"summary": "string — concise summary of your findings",
|
|
72
|
+
"findings": [
|
|
73
|
+
{
|
|
74
|
+
"severity": "critical|high|medium|low|info",
|
|
75
|
+
"category": "string",
|
|
76
|
+
"title": "string",
|
|
77
|
+
"description": "string",
|
|
78
|
+
"file": "string (optional)",
|
|
79
|
+
"suggestion": "string (optional)"
|
|
80
|
+
}
|
|
81
|
+
],
|
|
82
|
+
"riskScore": 0-100,
|
|
83
|
+
"possibleRegressions": ["string"]
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
Return ONLY valid JSON. No markdown fences, no explanation outside the JSON.`;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
protected parseResponse(raw: string): AgentResult {
|
|
90
|
+
let parsed: any;
|
|
91
|
+
|
|
92
|
+
try {
|
|
93
|
+
const cleaned = raw.replace(/```json\n?/g, '').replace(/```\n?/g, '').trim();
|
|
94
|
+
parsed = JSON.parse(cleaned);
|
|
95
|
+
} catch {
|
|
96
|
+
return {
|
|
97
|
+
agentName: this.agentName,
|
|
98
|
+
findings: [{
|
|
99
|
+
severity: 'info',
|
|
100
|
+
category: 'parse-error',
|
|
101
|
+
title: 'Could not parse agent response',
|
|
102
|
+
description: raw.slice(0, 500),
|
|
103
|
+
}],
|
|
104
|
+
summary: 'Agent returned unparseable response.',
|
|
105
|
+
riskScore: 0,
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const findings: AgentFinding[] = (parsed.findings ?? []).map((f: any) => ({
|
|
110
|
+
severity: f.severity ?? 'info',
|
|
111
|
+
category: f.category ?? 'general',
|
|
112
|
+
title: f.title ?? 'Untitled',
|
|
113
|
+
description: f.description ?? '',
|
|
114
|
+
file: f.file,
|
|
115
|
+
suggestion: f.suggestion,
|
|
116
|
+
}));
|
|
117
|
+
|
|
118
|
+
return {
|
|
119
|
+
agentName: this.agentName,
|
|
120
|
+
findings,
|
|
121
|
+
summary: parsed.summary ?? '',
|
|
122
|
+
riskScore: Math.max(0, Math.min(100, parseInt(parsed.riskScore ?? '0'))),
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { BaseAgent } from './base';
|
|
2
|
+
import { LLMProvider } from '../providers/base';
|
|
3
|
+
|
|
4
|
+
export class BusinessReviewer extends BaseAgent {
|
|
5
|
+
readonly agentName = 'business';
|
|
6
|
+
|
|
7
|
+
readonly systemPrompt = `You are a senior business analyst and domain expert performing a code review.
|
|
8
|
+
|
|
9
|
+
Your job is to analyze Pull Request changes STRICTLY from a business and domain perspective.
|
|
10
|
+
|
|
11
|
+
Focus on:
|
|
12
|
+
- Business logic correctness: does the code behave as the domain requires?
|
|
13
|
+
- Domain rule violations: are any business invariants broken?
|
|
14
|
+
- Functional regressions: could this break existing behavior users depend on?
|
|
15
|
+
- Side effects on other business flows (billing, notifications, state machines, etc.)
|
|
16
|
+
- Missing required steps (auditing, logging, approvals, notifications)
|
|
17
|
+
- Incorrect calculations, status transitions, or conditional logic
|
|
18
|
+
- Data integrity concerns (missing validations, incorrect defaults)
|
|
19
|
+
|
|
20
|
+
You are NOT a linter, NOT a security scanner. You analyze MEANING, not syntax.
|
|
21
|
+
|
|
22
|
+
If the rules.yml specifies required_calls or required_checks for a module, verify they are present.
|
|
23
|
+
|
|
24
|
+
Be concrete. Reference specific lines or functions when relevant.
|
|
25
|
+
Assign a riskScore from 0 (no risk) to 100 (critical business risk).
|
|
26
|
+
|
|
27
|
+
Return only valid JSON as specified.`;
|
|
28
|
+
|
|
29
|
+
constructor(provider: LLMProvider) {
|
|
30
|
+
super(provider);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { BaseAgent } from './base';
|
|
2
|
+
import { LLMProvider } from '../providers/base';
|
|
3
|
+
|
|
4
|
+
export class SecurityReviewer extends BaseAgent {
|
|
5
|
+
readonly agentName = 'security';
|
|
6
|
+
|
|
7
|
+
readonly systemPrompt = `You are a senior application security engineer performing a code review.
|
|
8
|
+
|
|
9
|
+
Your job is to analyze Pull Request changes for security vulnerabilities and risks.
|
|
10
|
+
|
|
11
|
+
Focus on:
|
|
12
|
+
- Authentication and authorization flaws (missing checks, privilege escalation)
|
|
13
|
+
- Injection vulnerabilities (SQL, NoSQL, command, LDAP, template injection)
|
|
14
|
+
- Insecure direct object references (IDOR)
|
|
15
|
+
- Sensitive data exposure (logging secrets, returning PII, insecure storage)
|
|
16
|
+
- Cryptographic issues (weak algorithms, hardcoded secrets, insecure RNG)
|
|
17
|
+
- Input validation gaps (missing sanitization, unsafe deserialization)
|
|
18
|
+
- Race conditions or TOCTOU vulnerabilities
|
|
19
|
+
- Broken access control in API endpoints
|
|
20
|
+
- SSRF, path traversal, open redirects
|
|
21
|
+
- Dependency security (new packages with known issues)
|
|
22
|
+
|
|
23
|
+
Mark findings involving sensitive_modules from the rules with higher severity.
|
|
24
|
+
|
|
25
|
+
Be precise: name the vulnerability class (OWASP), the file, and the specific line or pattern.
|
|
26
|
+
Assign a riskScore from 0 (no security issues) to 100 (active vulnerability, block immediately).
|
|
27
|
+
|
|
28
|
+
Return only valid JSON as specified.`;
|
|
29
|
+
|
|
30
|
+
constructor(provider: LLMProvider) {
|
|
31
|
+
super(provider);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import * as fs from 'node:fs';
|
|
2
|
+
import * as path from 'node:path';
|
|
3
|
+
import { getAivDir } from '../config';
|
|
4
|
+
|
|
5
|
+
const CACHE_DIR = 'cache';
|
|
6
|
+
const TTL_MS = 1000 * 60 * 60; // 1 hour
|
|
7
|
+
|
|
8
|
+
export class Cache {
|
|
9
|
+
private readonly dir: string;
|
|
10
|
+
|
|
11
|
+
constructor(cwd: string = process.cwd()) {
|
|
12
|
+
this.dir = path.join(getAivDir(cwd), CACHE_DIR);
|
|
13
|
+
if (!fs.existsSync(this.dir)) {
|
|
14
|
+
fs.mkdirSync(this.dir, { recursive: true });
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
get<T>(key: string): T | null {
|
|
19
|
+
const file = this.keyPath(key);
|
|
20
|
+
if (!fs.existsSync(file)) return null;
|
|
21
|
+
|
|
22
|
+
try {
|
|
23
|
+
const raw = JSON.parse(fs.readFileSync(file, 'utf8'));
|
|
24
|
+
if (Date.now() - raw.ts > TTL_MS) {
|
|
25
|
+
fs.unlinkSync(file);
|
|
26
|
+
return null;
|
|
27
|
+
}
|
|
28
|
+
return raw.data as T;
|
|
29
|
+
} catch {
|
|
30
|
+
return null;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
set<T>(key: string, data: T): void {
|
|
35
|
+
fs.writeFileSync(this.keyPath(key), JSON.stringify({ ts: Date.now(), data }), 'utf8');
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
invalidate(key: string): void {
|
|
39
|
+
const file = this.keyPath(key);
|
|
40
|
+
if (fs.existsSync(file)) fs.unlinkSync(file);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
private keyPath(key: string): string {
|
|
44
|
+
const safe = key.replace(/[^a-zA-Z0-9_-]/g, '_');
|
|
45
|
+
return path.join(this.dir, `${safe}.json`);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import chalk from 'chalk';
|
|
2
|
+
|
|
3
|
+
// Read version at runtime to avoid stale compiled value
|
|
4
|
+
function getVersion(): string {
|
|
5
|
+
try {
|
|
6
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
7
|
+
const pkg = require('../../package.json') as { version: string };
|
|
8
|
+
return pkg.version;
|
|
9
|
+
} catch {
|
|
10
|
+
return '0.1.0';
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const ART = `
|
|
15
|
+
█████╗ ██╗ ██████╗ ██████╗ ██████╗ ███████╗██╗ ██╗██╗███████╗██╗ ██╗███████╗██████╗
|
|
16
|
+
██╔══██╗██║ ██╔══██╗██╔══██╗ ██╔══██╗██╔════╝██║ ██║██║██╔════╝██║ ██║██╔════╝██╔══██╗
|
|
17
|
+
███████║██║ ██████╔╝██████╔╝ ██████╔╝█████╗ ██║ ██║██║█████╗ ██║ █╗ ██║█████╗ ██████╔╝
|
|
18
|
+
██╔══██║██║ ██╔═══╝ ██╔══██╗ ██╔══██╗██╔══╝ ╚██╗ ██╔╝██║██╔══╝ ██║███╗██║██╔══╝ ██╔══██╗
|
|
19
|
+
██║ ██║██║ ██║ ██║ ██║ ██║ ██║███████╗ ╚████╔╝ ██║███████╗╚███╔███╔╝███████╗██║ ██║
|
|
20
|
+
╚═╝ ╚═╝╚═╝ ╚═╝ ╚═╝ ╚═╝ ╚═╝ ╚═╝╚══════╝ ╚═══╝ ╚═╝╚══════╝ ╚══╝╚══╝ ╚══════╝╚═╝ ╚═╝`;
|
|
21
|
+
|
|
22
|
+
export function printBanner(): void {
|
|
23
|
+
// Skip banner when output is piped or --json is requested
|
|
24
|
+
if (!process.stdout.isTTY || process.argv.includes('--json')) return;
|
|
25
|
+
|
|
26
|
+
const version = getVersion();
|
|
27
|
+
|
|
28
|
+
console.log(chalk.cyan(`\n AIV V${version} BETA`));
|
|
29
|
+
console.log(chalk.cyan(ART));
|
|
30
|
+
console.log(chalk.dim(' by Ateriss'));
|
|
31
|
+
console.log(chalk.white.bold(' AI Pull Request Reviewer\n'));
|
|
32
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { Command } from 'commander';
|
|
2
|
+
import chalk from 'chalk';
|
|
3
|
+
import { table } from 'table';
|
|
4
|
+
import { t } from '../../i18n';
|
|
5
|
+
|
|
6
|
+
export function agentsCommand(): Command {
|
|
7
|
+
return new Command('agents')
|
|
8
|
+
.alias('a')
|
|
9
|
+
.description('List available AI review agents')
|
|
10
|
+
.action(() => {
|
|
11
|
+
const tr = t();
|
|
12
|
+
const agents = [
|
|
13
|
+
{ name: 'business', desc: tr.agentBusinessDesc, focus: tr.agentBusinessFocus },
|
|
14
|
+
{ name: 'architecture', desc: tr.agentArchDesc, focus: tr.agentArchFocus },
|
|
15
|
+
{ name: 'security', desc: tr.agentSecDesc, focus: tr.agentSecFocus },
|
|
16
|
+
];
|
|
17
|
+
|
|
18
|
+
console.log(chalk.bold(tr.agentsTitle));
|
|
19
|
+
|
|
20
|
+
const rows = agents.map(a => [
|
|
21
|
+
chalk.cyan(a.name),
|
|
22
|
+
a.desc,
|
|
23
|
+
chalk.dim(a.focus),
|
|
24
|
+
]);
|
|
25
|
+
|
|
26
|
+
const header = [
|
|
27
|
+
chalk.bold(tr.agentsColAgent),
|
|
28
|
+
chalk.bold(tr.agentsColDesc),
|
|
29
|
+
chalk.bold(tr.agentsColFocus),
|
|
30
|
+
];
|
|
31
|
+
|
|
32
|
+
console.log(table([header, ...rows], {
|
|
33
|
+
border: {
|
|
34
|
+
topBody: '─', topJoin: '┬', topLeft: '╭', topRight: '╮',
|
|
35
|
+
bottomBody: '─', bottomJoin: '┴', bottomLeft: '╰', bottomRight: '╯',
|
|
36
|
+
bodyLeft: '│', bodyRight: '│', bodyJoin: '│',
|
|
37
|
+
joinBody: '─', joinLeft: '├', joinRight: '┤', joinJoin: '┼',
|
|
38
|
+
},
|
|
39
|
+
columns: [
|
|
40
|
+
{ width: 14 },
|
|
41
|
+
{ width: 44, wrapWord: true },
|
|
42
|
+
{ width: 52, wrapWord: true },
|
|
43
|
+
],
|
|
44
|
+
columnDefault: { paddingLeft: 1, paddingRight: 1 },
|
|
45
|
+
}));
|
|
46
|
+
|
|
47
|
+
console.log(chalk.dim(` ${tr.agentsFooter}\n`));
|
|
48
|
+
});
|
|
49
|
+
}
|