aico-ai 1.0.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/.aicoignore ADDED
@@ -0,0 +1,18 @@
1
+ # CodeGuard AI Ignore
2
+
3
+ # Dependencies
4
+ node_modules/
5
+ package-lock.json
6
+ yarn.lock
7
+ pnpm-lock.yaml
8
+
9
+ # Build outputs
10
+ dist/
11
+ build/
12
+
13
+ # Secrets
14
+ .env
15
+ .env.*
16
+
17
+ # Large files
18
+ *.log
package/.env.example ADDED
@@ -0,0 +1 @@
1
+ GROQ_API_KEY=your_groq_api_key_here
package/README.md ADDED
@@ -0,0 +1,68 @@
1
+ # Aico 🚀
2
+
3
+ **Aico** is an intelligent CLI tool that acts as a gatekeeper for your code. It uses AI (powered by Groq) to review your changes before you push them, ensuring high quality, security, and consistency across your projects.
4
+
5
+ ## Features
6
+
7
+ - 🔍 **AI Code Review**: Semantic analysis of your git diffs to catch bugs, security issues, and code smells.
8
+ - ✨ **Apply Fixes**: Automatically apply suggested improvements with a single click.
9
+ - 📝 **AI Commit Messages**: Generate high-quality, Conventional Commit messages based on your changes.
10
+ - 🛡️ **Git Hook Integration**: Seamlessly integrates with Husky to run reviews as a `pre-push` hook.
11
+ - 🤫 **Silent Mode**: Run reviews without blocking your workflow.
12
+ - 🌍 **Global Config**: Configure your API key once and use it across all your projects.
13
+
14
+ ## Installation
15
+
16
+ ```bash
17
+ npm install -g aico-gatekeeper
18
+ # or
19
+ npm install --save-dev aico-gatekeeper
20
+ ```
21
+
22
+ ## Getting Started
23
+
24
+ 1. **Initialize Aico**:
25
+ Run the following command to set up your API key (Groq) and optionally configure Git hooks.
26
+ ```bash
27
+ aico init
28
+ ```
29
+
30
+ 2. **Review your changes**:
31
+ Manually review your staged changes:
32
+ ```bash
33
+ aico review
34
+ ```
35
+
36
+ 3. **Commit with AI**:
37
+ Let Aico write your commit message:
38
+ ```bash
39
+ aico commit
40
+ ```
41
+
42
+ ## Usage
43
+
44
+ ### Commands
45
+
46
+ - `aico init`: Setup API keys and Git hooks.
47
+ - `aico review`: Analyze staged changes and suggest improvements.
48
+ - `aico commit`: Generate and apply an AI-suggested commit message.
49
+
50
+ ### Options
51
+
52
+ - `--silent`, `-s`: Run the review in silent mode (doesn't block the push).
53
+
54
+ ## Configuration
55
+
56
+ Aico stores its global configuration in `~/.aicorc`. You can also use environment variables:
57
+
58
+ ```bash
59
+ GROQ_API_KEY=your_key_here
60
+ ```
61
+
62
+ ## Why Aico?
63
+
64
+ While IDE extensions like Copilot or Cursor are great assistants, **Aico** is your project's **Gatekeeper**. It ensures that no matter what IDE your team uses, every piece of code meets the same quality standards before it reaches the remote repository.
65
+
66
+ ## License
67
+
68
+ ISC
package/index.js ADDED
@@ -0,0 +1,187 @@
1
+ #!/usr/bin/env node
2
+ import { getStagedDiff, applyFix } from './lib/git-utils.js';
3
+ import { reviewCode, generateCommitMessage } from './lib/ai-service.js';
4
+ import { parseAIResponse, displayIssues } from './lib/reviewer.js';
5
+ import pc from 'picocolors';
6
+ import enquirer from 'enquirer';
7
+ import fs from 'fs';
8
+ const { prompt } = enquirer;
9
+
10
+ import { saveGlobalConfig } from './lib/config-utils.js';
11
+ import { execSync } from 'child_process';
12
+
13
+ async function init() {
14
+ console.log(pc.bold(pc.blue('Aico: Initializing...')));
15
+
16
+ const { apiKey } = await prompt({
17
+ type: 'input',
18
+ name: 'apiKey',
19
+ message: 'Enter your Groq API Key:'
20
+ });
21
+
22
+ if (apiKey) {
23
+ saveGlobalConfig({ GROQ_API_KEY: apiKey });
24
+ console.log(pc.green('API Key saved globally in ~/.aicorc'));
25
+ }
26
+
27
+ const { setupHusky } = await prompt({
28
+ type: 'confirm',
29
+ name: 'setupHusky',
30
+ message: 'Would you like to setup Aico as a pre-push git hook?',
31
+ initial: true
32
+ });
33
+
34
+ if (setupHusky) {
35
+ try {
36
+ console.log(pc.blue('Setting up Husky...'));
37
+ execSync('npx husky init', { stdio: 'inherit' });
38
+ // Add aico to pre-push
39
+ const huskyPath = '.husky/pre-push';
40
+ const hookContent = '#!/bin/sh\nnpx aico review\n';
41
+ fs.writeFileSync(huskyPath, hookContent, { mode: 0o755 });
42
+ console.log(pc.green('Husky pre-push hook configured!'));
43
+ } catch (e) {
44
+ console.error(pc.red('Failed to setup Husky:'), e.message);
45
+ }
46
+ }
47
+ }
48
+
49
+ async function main() {
50
+ const args = process.argv.slice(2);
51
+ const command = args[0] || 'review';
52
+ const isSilent = args.includes('--silent') || args.includes('-s');
53
+
54
+ if (command === 'init') {
55
+ await init();
56
+ return;
57
+ }
58
+
59
+ if (command === 'commit') {
60
+ console.log(pc.bold(pc.blue('Aico: Generating commit message...')));
61
+ try {
62
+ const diff = await getStagedDiff();
63
+ if (!diff || diff.trim() === '') {
64
+ console.log(pc.yellow('No staged changes found to commit.'));
65
+ return;
66
+ }
67
+
68
+ let message = await generateCommitMessage(diff);
69
+
70
+ while (true) {
71
+ console.log(`\n${pc.bold('Suggested message:')} ${pc.green(message)}`);
72
+
73
+ const { action } = await prompt({
74
+ type: 'select',
75
+ name: 'action',
76
+ message: 'What would you like to do?',
77
+ choices: [
78
+ { name: 'accept', message: 'Accept and commit', value: 'accept' },
79
+ { name: 'edit', message: 'Edit message', value: 'edit' },
80
+ { name: 'regenerate', message: 'Regenerate', value: 'regenerate' },
81
+ { name: 'abort', message: 'Abort', value: 'abort' }
82
+ ]
83
+ });
84
+
85
+ if (action === 'accept') {
86
+ execSync(`git commit -m "${message.replace(/"/g, '\\"')}"`, { stdio: 'inherit' });
87
+ console.log(pc.green('Committed successfully!'));
88
+ return;
89
+ } else if (action === 'edit') {
90
+ const { newMessage } = await prompt({
91
+ type: 'input',
92
+ name: 'newMessage',
93
+ message: 'Edit commit message:',
94
+ initial: message
95
+ });
96
+ message = newMessage;
97
+ } else if (action === 'regenerate') {
98
+ console.log(pc.blue('Regenerating...'));
99
+ message = await generateCommitMessage(diff);
100
+ } else {
101
+ console.log(pc.red('Commit aborted.'));
102
+ return;
103
+ }
104
+ }
105
+ } catch (error) {
106
+ console.error(pc.red('Error during commit:'), error.message);
107
+ process.exit(1);
108
+ }
109
+ }
110
+
111
+ console.log(pc.bold(pc.blue('Aico: Analyzing your changes...')));
112
+
113
+ try {
114
+ const diff = await getStagedDiff();
115
+
116
+ if (!diff || diff.trim() === '') {
117
+ console.log(pc.yellow('No staged changes found to review.'));
118
+ return;
119
+ }
120
+
121
+ const aiResponse = await reviewCode(diff);
122
+ const issues = parseAIResponse(aiResponse);
123
+
124
+ if (issues.length === 0) {
125
+ console.log(pc.green('No issues found. Good job!'));
126
+ } else {
127
+ displayIssues(issues);
128
+
129
+ for (const issue of issues) {
130
+ console.log(pc.cyan(`\nReviewing issue in ${pc.bold(issue.file)}...`));
131
+ console.log(`${pc.yellow('Issue:')} ${issue.issue}`);
132
+
133
+ const choices = [
134
+ { name: 'skip', message: 'Skip this issue', value: 'skip' },
135
+ ];
136
+
137
+ if (issue.correctedCode) {
138
+ choices.unshift({ name: 'apply', message: 'Apply suggested fix', value: 'apply' });
139
+ }
140
+
141
+ const { action } = await prompt({
142
+ type: 'select',
143
+ name: 'action',
144
+ message: 'What would you like to do?',
145
+ choices: [...choices, { name: 'abort', message: 'Abort push', value: 'abort' }]
146
+ });
147
+
148
+ if (action === 'apply') {
149
+ const success = await applyFix(issue.file, issue.correctedCode);
150
+ if (success) {
151
+ console.log(pc.green(`Applied fix to ${issue.file}`));
152
+ }
153
+ } else if (action === 'abort') {
154
+ if (isSilent) {
155
+ console.log(pc.yellow('Warning: Review aborted with issues, but proceeding anyway (Silent Mode).'));
156
+ break;
157
+ }
158
+ console.log(pc.red('Push aborted.'));
159
+ process.exit(1);
160
+ }
161
+ }
162
+ }
163
+
164
+ if (!isSilent) {
165
+ const finalAction = await prompt({
166
+ type: 'confirm',
167
+ name: 'proceed',
168
+ message: 'Would you like to proceed with the push?',
169
+ initial: true
170
+ });
171
+
172
+ if (!finalAction.proceed) {
173
+ console.log(pc.red('Push aborted.'));
174
+ process.exit(1);
175
+ }
176
+ } else if (issues.length > 0) {
177
+ console.log(pc.yellow('\nReview completed with issues, but proceeding anyway (Silent Mode).'));
178
+ }
179
+
180
+ console.log(pc.green('Proceeding with push...'));
181
+ } catch (error) {
182
+ console.error(pc.red('Error during review:'), error.message);
183
+ process.exit(1);
184
+ }
185
+ }
186
+
187
+ main();
@@ -0,0 +1,99 @@
1
+ import Groq from 'groq-sdk';
2
+ import dotenv from 'dotenv';
3
+
4
+ import { getApiKey } from './config-utils.js';
5
+
6
+ dotenv.config();
7
+
8
+ const groq = new Groq({
9
+ apiKey: getApiKey(),
10
+ });
11
+
12
+ /**
13
+ * Sends code diff to Groq for review.
14
+ * @param {string} diff
15
+ * @returns {Promise<string>} AI review response.
16
+ */
17
+ export async function reviewCode(diff) {
18
+ const apiKey = getApiKey();
19
+ if (!apiKey) {
20
+ throw new Error('GROQ_API_KEY is not set. Run "aico init" to configure it.');
21
+ }
22
+
23
+ if (!diff || diff.trim() === '') {
24
+ return 'No changes detected to review.';
25
+ }
26
+
27
+ const systemPrompt = `
28
+ You are an expert code reviewer. Analyze the following git diff and provide constructive feedback.
29
+ Focus on:
30
+ 1. Security vulnerabilities.
31
+ 2. Performance bottlenecks.
32
+ 3. Code readability and clean code principles.
33
+ 4. Potential bugs.
34
+
35
+ Format your response as a list of issues. For each issue, provide:
36
+ - File: [filename]
37
+ - Issue: [description]
38
+ - Suggestion: [how to fix it]
39
+ - CorrectedCode: [the FULL corrected content of the file, wrapped in triple backticks]
40
+
41
+ Keep it concise and professional. Do not use emojis.
42
+ `;
43
+
44
+ try {
45
+ const chatCompletion = await groq.chat.completions.create({
46
+ messages: [
47
+ { role: 'system', content: systemPrompt },
48
+ { role: 'user', content: `Review this diff:\n\n${diff}` },
49
+ ],
50
+ model: 'llama-3.3-70b-versatile',
51
+ });
52
+
53
+ return chatCompletion.choices[0]?.message?.content || 'No feedback provided.';
54
+ } catch (error) {
55
+ console.error('Error during AI review:', error);
56
+ throw error;
57
+ }
58
+ }
59
+
60
+ /**
61
+ * Generates a commit message based on the diff.
62
+ * @param {string} diff
63
+ * @returns {Promise<string>} Suggested commit message.
64
+ */
65
+ export async function generateCommitMessage(diff) {
66
+ const apiKey = getApiKey();
67
+ if (!apiKey) {
68
+ throw new Error('GROQ_API_KEY is not set. Run "aico init" to configure it.');
69
+ }
70
+
71
+ if (!diff || diff.trim() === '') {
72
+ return 'chore: update files';
73
+ }
74
+
75
+ const systemPrompt = `
76
+ You are an expert at writing git commit messages following the Conventional Commits specification.
77
+ Analyze the diff and provide a single, concise commit message.
78
+ Format: <type>(<scope>): <description>
79
+ Types: feat, fix, docs, style, refactor, test, chore.
80
+ Description: Imperative, present tense, no period at the end.
81
+
82
+ Do not include any other text, only the commit message itself. Do not use emojis.
83
+ `;
84
+
85
+ try {
86
+ const chatCompletion = await groq.chat.completions.create({
87
+ messages: [
88
+ { role: 'system', content: systemPrompt },
89
+ { role: 'user', content: `Generate a commit message for this diff:\n\n${diff}` },
90
+ ],
91
+ model: 'llama-3.3-70b-versatile',
92
+ });
93
+
94
+ return chatCompletion.choices[0]?.message?.content?.trim() || 'chore: update files';
95
+ } catch (error) {
96
+ console.error('Error generating commit message:', error);
97
+ throw error;
98
+ }
99
+ }
@@ -0,0 +1,40 @@
1
+ import os from 'os';
2
+ import fs from 'fs';
3
+ import path from 'path';
4
+
5
+ const CONFIG_PATH = path.join(os.homedir(), '.aicorc');
6
+
7
+ /**
8
+ * Saves the API key to a global config file in the user's home directory.
9
+ * @param {string} apiKey
10
+ */
11
+ export function saveGlobalConfig(config) {
12
+ fs.writeFileSync(CONFIG_PATH, JSON.stringify(config, null, 2));
13
+ }
14
+
15
+ /**
16
+ * Loads the global config.
17
+ * @returns {object|null}
18
+ */
19
+ export function loadGlobalConfig() {
20
+ if (fs.existsSync(CONFIG_PATH)) {
21
+ try {
22
+ return JSON.parse(fs.readFileSync(CONFIG_PATH, 'utf-8'));
23
+ } catch (e) {
24
+ return null;
25
+ }
26
+ }
27
+ return null;
28
+ }
29
+
30
+ /**
31
+ * Gets the API key from environment variables or global config.
32
+ * @returns {string|null}
33
+ */
34
+ export function getApiKey() {
35
+ // Priority: Process Env > Local .env (handled by dotenv) > Global Config
36
+ if (process.env.GROQ_API_KEY) return process.env.GROQ_API_KEY;
37
+
38
+ const globalConfig = loadGlobalConfig();
39
+ return globalConfig?.GROQ_API_KEY || null;
40
+ }
@@ -0,0 +1,56 @@
1
+ import { simpleGit } from 'simple-git';
2
+
3
+ const git = simpleGit();
4
+
5
+ /**
6
+ * Gets the diff of staged changes.
7
+ * @returns {Promise<string>} The git diff string.
8
+ */
9
+ export async function getStagedDiff() {
10
+ try {
11
+ return await git.diff(['--cached']);
12
+ } catch (error) {
13
+ console.error('Error getting staged diff:', error);
14
+ return '';
15
+ }
16
+ }
17
+
18
+ /**
19
+ * Gets the diff between current branch and its upstream.
20
+ * Useful for pre-push hook.
21
+ * @returns {Promise<string>} The git diff string.
22
+ */
23
+ export async function getPushDiff() {
24
+ try {
25
+ // This gets the diff between the current branch and its remote tracking branch
26
+ return await git.diff(['@{u}..HEAD']);
27
+ } catch (error) {
28
+ // If no upstream is set, we might want to compare with a default branch or just return staged
29
+ console.warn('No upstream branch found. Falling back to staged changes.');
30
+ return await getStagedDiff();
31
+ }
32
+ }
33
+
34
+ import fs from 'fs';
35
+
36
+ /**
37
+ * Applies a suggested fix to a file.
38
+ * @param {string} filePath
39
+ * @param {string} correctedCode
40
+ */
41
+ export async function applyFix(filePath, correctedCode) {
42
+ try {
43
+ // For now, we assume the AI provides the corrected version of the code it analyzed.
44
+ // A more advanced version would use diffing/patching.
45
+ // If the AI provides a snippet, we'd need to find and replace.
46
+ // For this MVP, we'll just log that we are applying it.
47
+
48
+ // Let's try a simple approach: if the file exists, we write the code.
49
+ // WARNING: This is a simple overwrite for now.
50
+ fs.writeFileSync(filePath, correctedCode);
51
+ return true;
52
+ } catch (error) {
53
+ console.error(`Error applying fix to ${filePath}:`, error);
54
+ return false;
55
+ }
56
+ }
@@ -0,0 +1,51 @@
1
+ import pc from 'picocolors';
2
+
3
+ /**
4
+ * Parses the AI response into a structured format.
5
+ * @param {string} response
6
+ * @returns {Array<{file: string, issue: string, suggestion: string}>}
7
+ */
8
+ export function parseAIResponse(response) {
9
+ const issues = [];
10
+ const blocks = response.split(/File: /i).filter(block => block.trim() !== '');
11
+
12
+ for (const block of blocks) {
13
+ const lines = block.split('\n');
14
+ const file = lines[0]?.trim();
15
+ const issueMatch = block.match(/Issue: (.*)/i);
16
+ const suggestionMatch = block.match(/Suggestion: (.*)/i);
17
+ const codeMatch = block.match(/CorrectedCode:[\s\S]*?```[\w]*\n([\s\S]*?)```/i);
18
+
19
+ if (file && issueMatch && suggestionMatch) {
20
+ issues.push({
21
+ file,
22
+ issue: issueMatch[1].trim(),
23
+ suggestion: suggestionMatch[1].trim(),
24
+ correctedCode: codeMatch ? codeMatch[1].trim() : null,
25
+ });
26
+ }
27
+ }
28
+
29
+ return issues;
30
+ }
31
+
32
+ /**
33
+ * Displays the issues to the user in a formatted way.
34
+ * @param {Array<{file: string, issue: string, suggestion: string}>} issues
35
+ */
36
+ export function displayIssues(issues) {
37
+ if (issues.length === 0) {
38
+ console.log(pc.green('No issues found. Good job!'));
39
+ return;
40
+ }
41
+
42
+ console.log(pc.yellow(`Found ${issues.length} potential issues:\n`));
43
+
44
+ issues.forEach((issue, index) => {
45
+ console.log(pc.cyan(`--------------------------------------------------`));
46
+ console.log(`${pc.bold('File:')} ${issue.file}`);
47
+ console.log(`${pc.bold('Issue:')} ${issue.issue}`);
48
+ console.log(`${pc.bold('Suggestion:')} ${issue.suggestion}`);
49
+ });
50
+ console.log(pc.cyan(`--------------------------------------------------\n`));
51
+ }
package/package.json ADDED
@@ -0,0 +1,32 @@
1
+ {
2
+ "name": "aico-ai",
3
+ "version": "1.0.0",
4
+ "type": "module",
5
+ "main": "index.js",
6
+ "bin": {
7
+ "aico": "./index.js"
8
+ },
9
+ "scripts": {
10
+ "test": "echo \"Error: no test specified\" && exit 1"
11
+ },
12
+ "description": "AI-powered git gatekeeper for code reviews and commit message generation.",
13
+ "keywords": [
14
+ "ai",
15
+ "git",
16
+ "code-review",
17
+ "commit",
18
+ "groq",
19
+ "cli",
20
+ "developer-tools"
21
+ ],
22
+ "author": "Lucas Silva from CodeTech Software <projetos@codetechsoftware.com.br>",
23
+ "license": "ISC",
24
+ "dependencies": {
25
+ "dotenv": "^17.2.3",
26
+ "enquirer": "^2.4.1",
27
+ "groq-sdk": "^0.37.0",
28
+ "husky": "^9.1.7",
29
+ "picocolors": "^1.1.1",
30
+ "simple-git": "^3.30.0"
31
+ }
32
+ }