atlasia-ghost 0.1.0 → 0.3.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 +16 -0
- package/ghost.js +98 -32
- package/package.json +8 -3
- package/test.js +0 -36
package/README.md
CHANGED
|
@@ -39,10 +39,26 @@ ghost
|
|
|
39
39
|
| Option | Description |
|
|
40
40
|
| :--- | :--- |
|
|
41
41
|
| `--model <name>` | Utiliser un modèle spécifique (ex: `llama-3.1-8b-instant`) |
|
|
42
|
+
| `--provider <name>` | Choisir le fournisseur (`groq` [défaut], `openai`) |
|
|
42
43
|
| `--no-security` | Désactiver l'audit de sécurité (scan de secrets) |
|
|
43
44
|
| `--dry-run` | Générer le message sans effectuer le commit |
|
|
44
45
|
| `--help`, `-h` | Afficher l'aide |
|
|
45
46
|
|
|
47
|
+
## 📝 Personnalisation
|
|
48
|
+
|
|
49
|
+
### Sélection de fichiers
|
|
50
|
+
Ghost propose désormais une sélection interactive si plusieurs fichiers sont modifiés. Vous pouvez spécifier les fichiers à analyser (ex: `1,3,5`) ou tout analyser (`all`).
|
|
51
|
+
|
|
52
|
+
### Configuration locale (`.ghostrc`)
|
|
53
|
+
Vous pouvez personnaliser le comportement de Ghost par projet en créant un fichier `.ghostrc` à la racine :
|
|
54
|
+
|
|
55
|
+
```json
|
|
56
|
+
{
|
|
57
|
+
"prompt": "Ton prompt système personnalisé pour l'IA ici",
|
|
58
|
+
"provider": "openai"
|
|
59
|
+
}
|
|
60
|
+
```
|
|
61
|
+
|
|
46
62
|
## 🛡️ Sécurité
|
|
47
63
|
|
|
48
64
|
Ghost effectue un double audit :
|
package/ghost.js
CHANGED
|
@@ -17,6 +17,7 @@ const os = require('os');
|
|
|
17
17
|
// ⚙️ CONFIGURATION & CONSTANTES
|
|
18
18
|
// ==============================================================================
|
|
19
19
|
const CONFIG_FILE = path.join(os.homedir(), '.ghost');
|
|
20
|
+
const HISTORY_FILE = path.join(os.homedir(), '.ghost_history');
|
|
20
21
|
const SAFE_EXTENSIONS = new Set(['.md', '.txt', '.csv', '.html', '.css', '.scss', '.lock', '.xml', '.json']);
|
|
21
22
|
const SAFE_FILES = new Set(['mvnw', 'gradlew', 'package-lock.json', 'yarn.lock', 'pom.xml']);
|
|
22
23
|
|
|
@@ -62,21 +63,29 @@ class ConfigManager {
|
|
|
62
63
|
console.log(`${Colors.DIM}Configuration sauvegardée dans ${CONFIG_FILE}${Colors.ENDC}`);
|
|
63
64
|
}
|
|
64
65
|
|
|
65
|
-
async getApiKey() {
|
|
66
|
-
|
|
66
|
+
async getApiKey(provider = 'groq') {
|
|
67
|
+
const keyMap = {
|
|
68
|
+
groq: { env: 'GROQ_API_KEY', config: 'groq_api_key', label: 'Groq', url: 'https://console.groq.com', prefix: 'gsk_' },
|
|
69
|
+
openai: { env: 'OPENAI_API_KEY', config: 'openai_api_key', label: 'OpenAI', url: 'https://platform.openai.com', prefix: 'sk-' },
|
|
70
|
+
anthropic: { env: 'ANTHROPIC_API_KEY', config: 'anthropic_api_key', label: 'Anthropic', url: 'https://console.anthropic.com', prefix: 'sk-ant' },
|
|
71
|
+
gemini: { env: 'GEMINI_API_KEY', config: 'gemini_api_key', label: 'Gemini', url: 'https://aistudio.google.com', prefix: '' }
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
const info = keyMap[provider] || keyMap.groq;
|
|
75
|
+
let key = process.env[info.env] || this.config[info.config];
|
|
67
76
|
|
|
68
77
|
if (!key) {
|
|
69
|
-
console.log(`\n${Colors.WARNING}⚠️ Configuration manquante${Colors.ENDC}`);
|
|
70
|
-
console.log(`${Colors.DIM}Pour utiliser Ghost, vous avez besoin d'une clé API
|
|
71
|
-
console.log(`${Colors.BLUE}👉 Obtenir une clé :
|
|
78
|
+
console.log(`\n${Colors.WARNING}⚠️ Configuration manquante pour ${info.label}${Colors.ENDC}`);
|
|
79
|
+
console.log(`${Colors.DIM}Pour utiliser Ghost avec ${info.label}, vous avez besoin d'une clé API.${Colors.ENDC}`);
|
|
80
|
+
console.log(`${Colors.BLUE}👉 Obtenir une clé : ${info.url}${Colors.ENDC}\n`);
|
|
72
81
|
|
|
73
|
-
key = await promptUser(`${Colors.BOLD}Collez votre clé
|
|
82
|
+
key = await promptUser(`${Colors.BOLD}Collez votre clé ${info.label} : ${Colors.ENDC}`);
|
|
74
83
|
|
|
75
|
-
if (key && key.trim()
|
|
76
|
-
this.config.
|
|
84
|
+
if (key && key.trim()) {
|
|
85
|
+
this.config[info.config] = key.trim();
|
|
77
86
|
this.save();
|
|
78
87
|
} else {
|
|
79
|
-
console.log(`${Colors.FAIL}❌ Clé
|
|
88
|
+
console.log(`${Colors.FAIL}❌ Clé manquante. Abandon.${Colors.ENDC}`);
|
|
80
89
|
process.exit(1);
|
|
81
90
|
}
|
|
82
91
|
}
|
|
@@ -96,14 +105,27 @@ class ConfigManager {
|
|
|
96
105
|
// 🧠 MOTEUR IA (Client HTTPS Natif)
|
|
97
106
|
// ==============================================================================
|
|
98
107
|
class AIEngine {
|
|
99
|
-
constructor(apiKey, model) {
|
|
108
|
+
constructor(apiKey, model, provider = 'groq') {
|
|
100
109
|
this.apiKey = apiKey;
|
|
101
|
-
this.hostname = "api.groq.com";
|
|
102
|
-
this.path = "/openai/v1/chat/completions";
|
|
103
110
|
this.model = model || DEFAULT_MODEL;
|
|
111
|
+
this.provider = provider;
|
|
112
|
+
|
|
113
|
+
// Configuration des providers
|
|
114
|
+
this.providers = {
|
|
115
|
+
groq: {
|
|
116
|
+
hostname: "api.groq.com",
|
|
117
|
+
path: "/openai/v1/chat/completions"
|
|
118
|
+
},
|
|
119
|
+
openai: {
|
|
120
|
+
hostname: "api.openai.com",
|
|
121
|
+
path: "/v1/chat/completions"
|
|
122
|
+
}
|
|
123
|
+
};
|
|
104
124
|
}
|
|
105
125
|
|
|
106
126
|
async call(systemPrompt, userPrompt, temperature = 0.3, jsonMode = false) {
|
|
127
|
+
const config = this.providers[this.provider] || this.providers.groq;
|
|
128
|
+
|
|
107
129
|
const payload = {
|
|
108
130
|
model: this.model,
|
|
109
131
|
messages: [
|
|
@@ -118,8 +140,8 @@ class AIEngine {
|
|
|
118
140
|
}
|
|
119
141
|
|
|
120
142
|
const options = {
|
|
121
|
-
hostname:
|
|
122
|
-
path:
|
|
143
|
+
hostname: config.hostname,
|
|
144
|
+
path: config.path,
|
|
123
145
|
method: 'POST',
|
|
124
146
|
headers: {
|
|
125
147
|
'Authorization': `Bearer ${this.apiKey}`,
|
|
@@ -136,9 +158,9 @@ class AIEngine {
|
|
|
136
158
|
if (res.statusCode >= 400) {
|
|
137
159
|
try {
|
|
138
160
|
const errBody = JSON.parse(data);
|
|
139
|
-
reject(new Error(`API Error ${res.statusCode}: ${errBody.error?.message || data}`));
|
|
161
|
+
reject(new Error(`API Error ${res.statusCode} (${this.provider}): ${errBody.error?.message || data}`));
|
|
140
162
|
} catch (e) {
|
|
141
|
-
reject(new Error(`API Error ${res.statusCode}: ${data}`));
|
|
163
|
+
reject(new Error(`API Error ${res.statusCode} (${this.provider}): ${data}`));
|
|
142
164
|
}
|
|
143
165
|
} else {
|
|
144
166
|
try {
|
|
@@ -165,6 +187,7 @@ function parseArgs() {
|
|
|
165
187
|
const args = process.argv.slice(2);
|
|
166
188
|
const flags = {
|
|
167
189
|
model: null,
|
|
190
|
+
provider: null,
|
|
168
191
|
noSecurity: false,
|
|
169
192
|
dryRun: false,
|
|
170
193
|
help: false
|
|
@@ -174,6 +197,9 @@ function parseArgs() {
|
|
|
174
197
|
if (args[i] === '--model' && args[i + 1]) {
|
|
175
198
|
flags.model = args[i + 1];
|
|
176
199
|
i++;
|
|
200
|
+
} else if (args[i] === '--provider' && args[i + 1]) {
|
|
201
|
+
flags.provider = args[i + 1];
|
|
202
|
+
i++;
|
|
177
203
|
} else if (args[i] === '--no-security') {
|
|
178
204
|
flags.noSecurity = true;
|
|
179
205
|
} else if (args[i] === '--dry-run') {
|
|
@@ -187,17 +213,22 @@ function parseArgs() {
|
|
|
187
213
|
|
|
188
214
|
function showHelp() {
|
|
189
215
|
console.log(`
|
|
190
|
-
${Colors.BOLD}${Colors.CYAN}GHOST CLI v0.
|
|
191
|
-
Assistant Git Intelligent basé sur l'IA (Groq)
|
|
216
|
+
${Colors.BOLD}${Colors.CYAN}GHOST CLI v0.2.0${Colors.ENDC}
|
|
217
|
+
Assistant Git Intelligent basé sur l'IA (Groq/OpenAI)
|
|
192
218
|
|
|
193
219
|
${Colors.BOLD}USAGE:${Colors.ENDC}
|
|
194
220
|
ghost [options]
|
|
195
221
|
|
|
196
222
|
${Colors.BOLD}OPTIONS:${Colors.ENDC}
|
|
197
|
-
--model <name>
|
|
198
|
-
--
|
|
199
|
-
--
|
|
200
|
-
--
|
|
223
|
+
--model <name> Utiliser un modèle spécifique (ex: llama-3.1-8b-instant)
|
|
224
|
+
--provider <name> Choisir le fournisseur (groq [défaut], openai)
|
|
225
|
+
--no-security Désactiver l'audit de sécurité
|
|
226
|
+
--dry-run Générer le message sans effectuer le commit
|
|
227
|
+
--help, -h Afficher cette aide
|
|
228
|
+
|
|
229
|
+
${Colors.BOLD}CONFIGURATION LOCALE (.ghostrc):${Colors.ENDC}
|
|
230
|
+
Créez un fichier ${Colors.CYAN}.ghostrc${Colors.ENDC} JSON dans votre projet pour personnaliser le prompt :
|
|
231
|
+
{ "prompt": "Ton prompt personnalisé ici" }
|
|
201
232
|
`);
|
|
202
233
|
}
|
|
203
234
|
|
|
@@ -338,9 +369,22 @@ async function main() {
|
|
|
338
369
|
}
|
|
339
370
|
|
|
340
371
|
console.clear();
|
|
341
|
-
console.log(`\n${Colors.BOLD}${Colors.CYAN} 👻 GHOST CLI ${Colors.ENDC}${Colors.DIM} v0.
|
|
372
|
+
console.log(`\n${Colors.BOLD}${Colors.CYAN} 👻 GHOST CLI ${Colors.ENDC}${Colors.DIM} v0.2.0${Colors.ENDC}`);
|
|
342
373
|
console.log(`${Colors.DIM} ─────────────────────────────────────${Colors.ENDC}\n`);
|
|
343
374
|
|
|
375
|
+
// 0. Chargement de la configuration locale .ghostrc
|
|
376
|
+
const localConfigPath = path.join(process.cwd(), '.ghostrc');
|
|
377
|
+
let customPrompt = null;
|
|
378
|
+
if (fs.existsSync(localConfigPath)) {
|
|
379
|
+
try {
|
|
380
|
+
const localConfig = JSON.parse(fs.readFileSync(localConfigPath, 'utf8'));
|
|
381
|
+
if (localConfig.prompt) customPrompt = localConfig.prompt;
|
|
382
|
+
console.log(`${Colors.DIM}📝 Configuration locale .ghostrc chargée${Colors.ENDC}`);
|
|
383
|
+
} catch (e) {
|
|
384
|
+
console.log(`${Colors.WARNING}⚠️ Erreur lecture .ghostrc: ${e.message}${Colors.ENDC}`);
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
|
|
344
388
|
// 1. Vérification Git
|
|
345
389
|
if (!checkGitRepo()) {
|
|
346
390
|
console.log(`${Colors.FAIL}❌ Erreur : Ce dossier n'est pas un dépôt Git.${Colors.ENDC}`);
|
|
@@ -350,8 +394,9 @@ async function main() {
|
|
|
350
394
|
|
|
351
395
|
const config = new ConfigManager();
|
|
352
396
|
const apiKey = await config.getApiKey();
|
|
353
|
-
const model = flags.model || config.getModel();
|
|
354
|
-
const
|
|
397
|
+
const model = flags.model || config.getModel();
|
|
398
|
+
const provider = flags.provider || config.config.provider || 'groq';
|
|
399
|
+
const ai = new AIEngine(apiKey, model, provider);
|
|
355
400
|
|
|
356
401
|
// 2. Récupération du Diff
|
|
357
402
|
const { text: fullDiffText, map: diffMap, files: fileList } = getStagedDiff();
|
|
@@ -364,15 +409,36 @@ async function main() {
|
|
|
364
409
|
|
|
365
410
|
// Affichage des fichiers détectés
|
|
366
411
|
console.log(`${Colors.BOLD}📂 Fichiers détectés (${fileList.length}) :${Colors.ENDC}`);
|
|
367
|
-
fileList.forEach(f => console.log(` ${Colors.DIM}
|
|
368
|
-
console.log("");
|
|
412
|
+
fileList.forEach((f, idx) => console.log(` ${Colors.DIM}${idx + 1}. ${f}${Colors.ENDC}`));
|
|
413
|
+
console.log("");
|
|
414
|
+
|
|
415
|
+
let selectedFiles = fileList;
|
|
416
|
+
let finalDiffText = fullDiffText;
|
|
417
|
+
let finalDiffMap = diffMap;
|
|
418
|
+
|
|
419
|
+
if (fileList.length > 1) {
|
|
420
|
+
const selection = await promptUser(`${Colors.BOLD}Sélectionnez les fichiers (ex: 1,3,5 ou 'all' [par défaut]) : ${Colors.ENDC}`);
|
|
421
|
+
if (selection && selection.toLowerCase() !== 'all' && selection.trim() !== '') {
|
|
422
|
+
const indices = selection.split(',').map(s => parseInt(s.trim()) - 1).filter(idx => idx >= 0 && idx < fileList.length);
|
|
423
|
+
if (indices.length > 0) {
|
|
424
|
+
selectedFiles = indices.map(idx => fileList[idx]);
|
|
425
|
+
finalDiffMap = {};
|
|
426
|
+
finalDiffText = "";
|
|
427
|
+
selectedFiles.forEach(f => {
|
|
428
|
+
finalDiffMap[f] = diffMap[f];
|
|
429
|
+
finalDiffText += `\n--- ${f} ---\n${diffMap[f]}\n`;
|
|
430
|
+
});
|
|
431
|
+
console.log(`${Colors.GREEN}✅ ${selectedFiles.length} fichier(s) sélectionné(s).${Colors.ENDC}\n`);
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
}
|
|
369
435
|
|
|
370
436
|
// 3. Audit de Sécurité
|
|
371
437
|
if (!flags.noSecurity) {
|
|
372
438
|
process.stdout.write(`${Colors.BLUE}🛡️ [1/2] Audit de Sécurité... ${Colors.ENDC}`);
|
|
373
439
|
|
|
374
440
|
const potentialLeaks = {};
|
|
375
|
-
for (const [fname, content] of Object.entries(
|
|
441
|
+
for (const [fname, content] of Object.entries(finalDiffMap)) {
|
|
376
442
|
const suspects = scanForSecrets(content);
|
|
377
443
|
if (suspects.length > 0) potentialLeaks[fname] = suspects;
|
|
378
444
|
}
|
|
@@ -403,14 +469,14 @@ async function main() {
|
|
|
403
469
|
}
|
|
404
470
|
|
|
405
471
|
// 4. Génération
|
|
406
|
-
const tokensEstimates = Math.ceil(
|
|
472
|
+
const tokensEstimates = Math.ceil(finalDiffText.length / 4);
|
|
407
473
|
console.log(`${Colors.BLUE}⚡ [2/2] Génération du message... ${Colors.DIM}(~${tokensEstimates} tokens)${Colors.ENDC}`);
|
|
408
|
-
console.log(`${Colors.DIM} Modèle utilisé : ${model}${Colors.ENDC}`);
|
|
474
|
+
console.log(`${Colors.DIM} Modèle utilisé : ${model} (${provider})${Colors.ENDC}`);
|
|
409
475
|
|
|
410
|
-
const sysPrompt = "Tu es un assistant Git expert. Génère UNIQUEMENT un message de commit suivant la convention 'Conventional Commits' (ex: feat: add login). Sois concis, descriptif et professionnel. N'utilise pas de markdown (pas de backticks), pas de guillemets autour du message.";
|
|
476
|
+
const sysPrompt = customPrompt || "Tu es un assistant Git expert. Génère UNIQUEMENT un message de commit suivant la convention 'Conventional Commits' (ex: feat: add login). Sois concis, descriptif et professionnel. N'utilise pas de markdown (pas de backticks), pas de guillemets autour du message.";
|
|
411
477
|
|
|
412
478
|
try {
|
|
413
|
-
let commitMsg = await ai.call(sysPrompt, `Diff :\n${
|
|
479
|
+
let commitMsg = await ai.call(sysPrompt, `Diff :\n${finalDiffText.substring(0, 12000)}`);
|
|
414
480
|
commitMsg = commitMsg.trim().replace(/^['"`]|['"`]$/g, ''); // Nettoyage final
|
|
415
481
|
|
|
416
482
|
console.log(`\n${Colors.CYAN}──────────────────────────────────────────────────${Colors.ENDC}`);
|
package/package.json
CHANGED
|
@@ -1,10 +1,15 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "atlasia-ghost",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"description": "Utilitaire de verification, validation, et de proposition de commits basé sur l'IA",
|
|
5
5
|
"main": "ghost.js",
|
|
6
|
+
"files": [
|
|
7
|
+
"ghost.js",
|
|
8
|
+
"LICENSE",
|
|
9
|
+
"README.md"
|
|
10
|
+
],
|
|
6
11
|
"bin": {
|
|
7
|
-
"ghost": "
|
|
12
|
+
"ghost": "ghost.js"
|
|
8
13
|
},
|
|
9
14
|
"scripts": {
|
|
10
15
|
"test": "node test.js",
|
|
@@ -21,7 +26,7 @@
|
|
|
21
26
|
"automate",
|
|
22
27
|
"ai",
|
|
23
28
|
"cli",
|
|
24
|
-
"
|
|
29
|
+
"npm"
|
|
25
30
|
],
|
|
26
31
|
"author": "Adel Lamallam",
|
|
27
32
|
"license": "MIT",
|
package/test.js
DELETED
|
@@ -1,36 +0,0 @@
|
|
|
1
|
-
const assert = require('assert');
|
|
2
|
-
const { execSync } = require('child_process');
|
|
3
|
-
const path = require('path');
|
|
4
|
-
|
|
5
|
-
// Mock Colors if needed or import from ghost.js if it were exported
|
|
6
|
-
const Colors = {
|
|
7
|
-
CYAN: '', BOLD: '', ENDC: '', DIM: '', GREEN: '', FAIL: ''
|
|
8
|
-
};
|
|
9
|
-
|
|
10
|
-
console.log('🧪 Running Ghost CLI Tests...\n');
|
|
11
|
-
|
|
12
|
-
try {
|
|
13
|
-
// Test 1: Version check
|
|
14
|
-
const packageJson = require('./package.json');
|
|
15
|
-
assert.strictEqual(packageJson.version, '0.1.0', 'package.json version should be 0.1.0');
|
|
16
|
-
console.log('✅ Test 1: package.json version is correct');
|
|
17
|
-
|
|
18
|
-
// Test 2: Help flag
|
|
19
|
-
const helpOutput = execSync('node ghost.js --help', { encoding: 'utf8' });
|
|
20
|
-
assert.ok(helpOutput.includes('GHOST CLI v0.1.0'), 'Help output should contain version');
|
|
21
|
-
assert.ok(helpOutput.includes('--model'), 'Help output should contain --model flag');
|
|
22
|
-
assert.ok(helpOutput.includes('--no-security'), 'Help output should contain --no-security flag');
|
|
23
|
-
assert.ok(helpOutput.includes('--dry-run'), 'Help output should contain --dry-run flag');
|
|
24
|
-
console.log('✅ Test 2: Help flag output is correct');
|
|
25
|
-
|
|
26
|
-
// Test 3: Internal logic (Entropy)
|
|
27
|
-
// We can't easily import internal functions without exporting them
|
|
28
|
-
// But we can test if the file is valid JS
|
|
29
|
-
execSync('node --check ghost.js');
|
|
30
|
-
console.log('✅ Test 3: ghost.js syntax is valid');
|
|
31
|
-
|
|
32
|
-
console.log('\n🎉 All tests passed successfully!');
|
|
33
|
-
} catch (error) {
|
|
34
|
-
console.error(`\n❌ Test failed: ${error.message}`);
|
|
35
|
-
process.exit(1);
|
|
36
|
-
}
|