atlasia-ghost 0.0.1

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.
Files changed (4) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +2 -0
  3. package/ghost.js +361 -0
  4. package/package.json +35 -0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Adel Lamallam
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,2 @@
1
+ # ghost
2
+ Utilitaire de verification, validation, et de proposition de commits basé sur l'IA
package/ghost.js ADDED
@@ -0,0 +1,361 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * 👻 Ghost CLI - Assistant Git Intelligent (Node.js Edition)
5
+ * Zero-Dependency: Utilise uniquement les modules natifs Node.js
6
+ * Compatible: Windows, Mac, Linux
7
+ */
8
+
9
+ const fs = require('fs');
10
+ const path = require('path');
11
+ const https = require('https');
12
+ const { execSync } = require('child_process');
13
+ const readline = require('readline');
14
+ const os = require('os');
15
+
16
+ // ==============================================================================
17
+ // ⚙️ CONFIGURATION & CONSTANTES
18
+ // ==============================================================================
19
+ const CONFIG_FILE = path.join(os.homedir(), '.ghost');
20
+ const SAFE_EXTENSIONS = new Set(['.md', '.txt', '.csv', '.html', '.css', '.scss', '.lock', '.xml', '.json']);
21
+ const SAFE_FILES = new Set(['mvnw', 'gradlew', 'package-lock.json', 'yarn.lock', 'pom.xml']);
22
+
23
+ // Modèles disponibles sur Groq (Basé sur votre plan gratuit)
24
+ // llama-3.3-70b-versatile : Intelligent, Idéal pour la sécurité (Limit: 1k RPD, 12k TPM)
25
+ // llama-3.1-8b-instant : Rapide, Idéal si quota dépassé (Limit: 14.4k RPD, 6k TPM)
26
+ const DEFAULT_MODEL = "llama-3.3-70b-versatile";
27
+
28
+ // Couleurs ANSI pour un terminal plus beau
29
+ const Colors = {
30
+ HEADER: '\x1b[95m',
31
+ BLUE: '\x1b[94m',
32
+ CYAN: '\x1b[96m',
33
+ GREEN: '\x1b[92m',
34
+ WARNING: '\x1b[93m',
35
+ FAIL: '\x1b[91m',
36
+ ENDC: '\x1b[0m',
37
+ BOLD: '\x1b[1m',
38
+ DIM: '\x1b[2m'
39
+ };
40
+
41
+ // ==============================================================================
42
+ // 🔧 GESTIONNAIRE DE CONFIGURATION
43
+ // ==============================================================================
44
+ class ConfigManager {
45
+ constructor() {
46
+ this.config = {};
47
+ this.load();
48
+ }
49
+
50
+ load() {
51
+ if (fs.existsSync(CONFIG_FILE)) {
52
+ try {
53
+ this.config = JSON.parse(fs.readFileSync(CONFIG_FILE, 'utf8'));
54
+ } catch (e) {
55
+ this.config = {};
56
+ }
57
+ }
58
+ }
59
+
60
+ save() {
61
+ fs.writeFileSync(CONFIG_FILE, JSON.stringify(this.config, null, 4));
62
+ console.log(`${Colors.DIM}Configuration sauvegardée dans ${CONFIG_FILE}${Colors.ENDC}`);
63
+ }
64
+
65
+ async getApiKey() {
66
+ let key = process.env.GROQ_API_KEY || this.config.groq_api_key;
67
+
68
+ 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 Groq (Gratuite).${Colors.ENDC}`);
71
+ console.log(`${Colors.BLUE}👉 Obtenir une clé : https://console.groq.com${Colors.ENDC}\n`);
72
+
73
+ key = await promptUser(`${Colors.BOLD}Collez votre clé Groq (gsk_...) : ${Colors.ENDC}`);
74
+
75
+ if (key && key.trim().startsWith('gsk_')) {
76
+ this.config.groq_api_key = key.trim();
77
+ this.save();
78
+ } else {
79
+ console.log(`${Colors.FAIL}❌ Clé invalide ou manquante. Abandon.${Colors.ENDC}`);
80
+ process.exit(1);
81
+ }
82
+ }
83
+ return key;
84
+ }
85
+
86
+ getModel() {
87
+ if (!this.config.model) {
88
+ this.config.model = DEFAULT_MODEL;
89
+ this.save();
90
+ }
91
+ return this.config.model;
92
+ }
93
+ }
94
+
95
+ // ==============================================================================
96
+ // 🧠 MOTEUR IA (Client HTTPS Natif)
97
+ // ==============================================================================
98
+ class AIEngine {
99
+ constructor(apiKey, model) {
100
+ this.apiKey = apiKey;
101
+ this.hostname = "api.groq.com";
102
+ this.path = "/openai/v1/chat/completions";
103
+ this.model = model || DEFAULT_MODEL;
104
+ }
105
+
106
+ async call(systemPrompt, userPrompt, temperature = 0.3, jsonMode = false) {
107
+ const payload = {
108
+ model: this.model,
109
+ messages: [
110
+ { role: "system", content: systemPrompt },
111
+ { role: "user", content: userPrompt }
112
+ ],
113
+ temperature: temperature
114
+ };
115
+
116
+ if (jsonMode) {
117
+ payload.response_format = { type: "json_object" };
118
+ }
119
+
120
+ const options = {
121
+ hostname: this.hostname,
122
+ path: this.path,
123
+ method: 'POST',
124
+ headers: {
125
+ 'Authorization': `Bearer ${this.apiKey}`,
126
+ 'Content-Type': 'application/json',
127
+ 'User-Agent': 'Mozilla/5.0 (Node.js/GhostCLI)'
128
+ }
129
+ };
130
+
131
+ return new Promise((resolve, reject) => {
132
+ const req = https.request(options, (res) => {
133
+ let data = '';
134
+ res.on('data', (chunk) => data += chunk);
135
+ res.on('end', () => {
136
+ if (res.statusCode >= 400) {
137
+ try {
138
+ const errBody = JSON.parse(data);
139
+ reject(new Error(`API Error ${res.statusCode}: ${errBody.error?.message || data}`));
140
+ } catch (e) {
141
+ reject(new Error(`API Error ${res.statusCode}: ${data}`));
142
+ }
143
+ } else {
144
+ try {
145
+ const result = JSON.parse(data);
146
+ resolve(result.choices[0].message.content);
147
+ } catch (e) {
148
+ reject(e);
149
+ }
150
+ }
151
+ });
152
+ });
153
+
154
+ req.on('error', (e) => reject(e));
155
+ req.write(JSON.stringify(payload));
156
+ req.end();
157
+ });
158
+ }
159
+ }
160
+
161
+ // ==============================================================================
162
+ // 🛡️ SCANNER DE SECURITE
163
+ // ==============================================================================
164
+ function calculateShannonEntropy(data) {
165
+ if (!data) return 0;
166
+ const frequencies = {};
167
+ for (let char of data) {
168
+ frequencies[char] = (frequencies[char] || 0) + 1;
169
+ }
170
+
171
+ let entropy = 0;
172
+ const len = data.length;
173
+ for (let char in frequencies) {
174
+ const p = frequencies[char] / len;
175
+ entropy -= p * Math.log2(p);
176
+ }
177
+ return entropy;
178
+ }
179
+
180
+ function scanForSecrets(content) {
181
+ if (!content) return [];
182
+ const suspicious = [];
183
+ // Regex : Cherche ce qui est entre quotes ou après un signe égal
184
+ const regex = /(['"])(.*?)(\1)|=\s*([^\s]+)/g;
185
+ let match;
186
+
187
+ while ((match = regex.exec(content)) !== null) {
188
+ const candidate = match[2] || match[4];
189
+
190
+ // Filtres heuristiques de base
191
+ if (!candidate || candidate.length < 12 || candidate.includes(' ')) continue;
192
+
193
+ // Analyse mathématique (Entropie > 4.8 est souvent un secret)
194
+ if (calculateShannonEntropy(candidate) > 4.8) {
195
+ suspicious.push(candidate.substring(0, 15) + "...");
196
+ }
197
+ }
198
+ return suspicious;
199
+ }
200
+
201
+ // ==============================================================================
202
+ // 📂 GIT INTERFACE
203
+ // ==============================================================================
204
+ function gitExec(args, suppressError = false) {
205
+ try {
206
+ return execSync(`git ${args.join(' ')}`, { encoding: 'utf8', stdio: ['pipe', 'pipe', 'pipe'] }).trim();
207
+ } catch (e) {
208
+ if (!suppressError && e.stderr) {
209
+ // On ignore les erreurs mineures
210
+ }
211
+ return "";
212
+ }
213
+ }
214
+
215
+ function checkGitRepo() {
216
+ try {
217
+ execSync('git rev-parse --is-inside-work-tree', { stdio: 'ignore' });
218
+ return true;
219
+ } catch (e) {
220
+ return false;
221
+ }
222
+ }
223
+
224
+ function getStagedDiff() {
225
+ const filesOutput = gitExec(['diff', '--cached', '--name-only']);
226
+ if (!filesOutput) return { text: "", map: {}, files: [] };
227
+
228
+ const files = filesOutput.split('\n');
229
+ let fullDiff = "";
230
+ const fileMap = {};
231
+ const validFiles = [];
232
+
233
+ for (let f of files) {
234
+ f = f.trim().replace(/^"|"$/g, '');
235
+ if (!f || SAFE_FILES.has(path.basename(f)) || SAFE_EXTENSIONS.has(path.extname(f))) continue;
236
+
237
+ const content = gitExec(['diff', '--cached', `"${f}"`]);
238
+ if (content) {
239
+ fullDiff += `\n--- ${f} ---\n${content}\n`;
240
+ fileMap[f] = content;
241
+ validFiles.push(f);
242
+ }
243
+ }
244
+
245
+ return { text: fullDiff, map: fileMap, files: validFiles };
246
+ }
247
+
248
+ function promptUser(question) {
249
+ const rl = readline.createInterface({
250
+ input: process.stdin,
251
+ output: process.stdout
252
+ });
253
+ return new Promise(resolve => {
254
+ rl.question(question, (answer) => {
255
+ rl.close();
256
+ resolve(answer);
257
+ });
258
+ });
259
+ }
260
+
261
+ // ==============================================================================
262
+ // 🚀 MAIN LOOP
263
+ // ==============================================================================
264
+ async function main() {
265
+ console.clear();
266
+ console.log(`\n${Colors.BOLD}${Colors.CYAN} 👻 GHOST CLI ${Colors.ENDC}${Colors.DIM} v1.0.0${Colors.ENDC}`);
267
+ console.log(`${Colors.DIM} ─────────────────────────────────────${Colors.ENDC}\n`);
268
+
269
+ // 1. Vérification Git
270
+ if (!checkGitRepo()) {
271
+ console.log(`${Colors.FAIL}❌ Erreur : Ce dossier n'est pas un dépôt Git.${Colors.ENDC}`);
272
+ console.log(`💡 Solution : Lancez ${Colors.BOLD}git init${Colors.ENDC} d'abord.`);
273
+ process.exit(1);
274
+ }
275
+
276
+ const config = new ConfigManager();
277
+ const apiKey = await config.getApiKey();
278
+ const model = config.getModel(); // Récupère le modèle configuré
279
+ const ai = new AIEngine(apiKey, model);
280
+
281
+ // 2. Récupération du Diff
282
+ const { text: fullDiffText, map: diffMap, files: fileList } = getStagedDiff();
283
+
284
+ if (!fullDiffText) {
285
+ console.log(`${Colors.WARNING}⚠️ Rien à commiter.${Colors.ENDC}`);
286
+ console.log(`💡 Astuce : Utilisez ${Colors.BOLD}git add <fichier>${Colors.ENDC} pour préparer vos changements.\n`);
287
+ process.exit(0);
288
+ }
289
+
290
+ // Affichage des fichiers détectés
291
+ console.log(`${Colors.BOLD}📂 Fichiers détectés (${fileList.length}) :${Colors.ENDC}`);
292
+ fileList.forEach(f => console.log(` ${Colors.DIM}• ${f}${Colors.ENDC}`));
293
+ console.log(""); // Saut de ligne
294
+
295
+ // 3. Audit de Sécurité
296
+ process.stdout.write(`${Colors.BLUE}🛡️ [1/2] Audit de Sécurité... ${Colors.ENDC}`);
297
+
298
+ const potentialLeaks = {};
299
+ for (const [fname, content] of Object.entries(diffMap)) {
300
+ const suspects = scanForSecrets(content);
301
+ if (suspects.length > 0) potentialLeaks[fname] = suspects;
302
+ }
303
+
304
+ if (Object.keys(potentialLeaks).length > 0) {
305
+ console.log(`\n${Colors.WARNING}⚠️ Entropie élevée détectée ! Analyse approfondie par l'IA...${Colors.ENDC}`);
306
+ const valPrompt = `Analyse ces secrets potentiels : ${JSON.stringify(potentialLeaks)}. Réponds JSON {'is_breach': bool, 'reason': str}. Vrais secrets (API Keys) seulement.`;
307
+
308
+ try {
309
+ const res = await ai.call("Tu es un expert sécurité.", valPrompt, 0.3, true);
310
+ const audit = JSON.parse(res);
311
+
312
+ if (audit.is_breach) {
313
+ console.log(`\n${Colors.FAIL}❌ [BLOCAGE SÉCURITÉ] Secret détecté !${Colors.ENDC}`);
314
+ console.log(`${Colors.FAIL} Raison : ${audit.reason}${Colors.ENDC}\n`);
315
+ process.exit(1);
316
+ } else {
317
+ console.log(`${Colors.GREEN}✅ Faux positifs confirmés (Sûr).${Colors.ENDC}`);
318
+ }
319
+ } catch (e) {
320
+ console.log(`${Colors.FAIL}Erreur audit IA: ${e.message}${Colors.ENDC}`);
321
+ }
322
+ } else {
323
+ console.log(`${Colors.GREEN}OK (Code sain)${Colors.ENDC}`);
324
+ }
325
+
326
+ // 4. Génération
327
+ const tokensEstimates = Math.ceil(fullDiffText.length / 4);
328
+ console.log(`${Colors.BLUE}⚡ [2/2] Génération du message... ${Colors.DIM}(~${tokensEstimates} tokens)${Colors.ENDC}`);
329
+ console.log(`${Colors.DIM} Modèle utilisé : ${model}${Colors.ENDC}`);
330
+
331
+ 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.";
332
+
333
+ try {
334
+ let commitMsg = await ai.call(sysPrompt, `Diff :\n${fullDiffText.substring(0, 12000)}`);
335
+ commitMsg = commitMsg.trim().replace(/^['"`]|['"`]$/g, ''); // Nettoyage final
336
+
337
+ console.log(`\n${Colors.CYAN}──────────────────────────────────────────────────${Colors.ENDC}`);
338
+ console.log(`${Colors.BOLD}${commitMsg}${Colors.ENDC}`);
339
+ console.log(`${Colors.CYAN}──────────────────────────────────────────────────${Colors.ENDC}\n`);
340
+
341
+ const action = await promptUser(`${Colors.BOLD}[Enter]${Colors.ENDC} Valider | ${Colors.BOLD}[n]${Colors.ENDC} Annuler : `);
342
+
343
+ if (action.toLowerCase() === 'n') {
344
+ console.log(`\n${Colors.WARNING}🚫 Opération annulée.${Colors.ENDC}\n`);
345
+ } else {
346
+ try {
347
+ execSync(`git commit -m "${commitMsg}"`, { stdio: 'inherit' });
348
+ console.log(`\n${Colors.GREEN}✅ Commit effectué avec succès !${Colors.ENDC}\n`);
349
+ } catch (e) {
350
+ // L'erreur est déjà affichée par git via stdio: inherit
351
+ }
352
+ }
353
+ } catch (e) {
354
+ console.log(`\n${Colors.FAIL}❌ Erreur fatale : ${e.message}${Colors.ENDC}\n`);
355
+ process.exit(1);
356
+ }
357
+ }
358
+
359
+ if (require.main === module) {
360
+ main().catch(console.error);
361
+ }
package/package.json ADDED
@@ -0,0 +1,35 @@
1
+ {
2
+ "name": "atlasia-ghost",
3
+ "version": "0.0.1",
4
+ "description": "Utilitaire de verification, validation, et de proposition de commits basé sur l'IA",
5
+ "main": "ghost.js",
6
+ "bin": {
7
+ "ghost": "./ghost.js"
8
+ },
9
+ "scripts": {
10
+ "test": "echo \"ghost raider\"",
11
+ "start": "node ghost.js"
12
+ },
13
+ "repository": {
14
+ "type": "git",
15
+ "url": "git+https://github.com/lamallamadel/ghost.git"
16
+ },
17
+ "keywords": [
18
+ "git",
19
+ "commit",
20
+ "ghost",
21
+ "automate",
22
+ "ai",
23
+ "cli",
24
+ "devsecops"
25
+ ],
26
+ "author": "Adel Lamallam",
27
+ "license": "MIT",
28
+ "bugs": {
29
+ "url": "https://github.com/lamallamadel/ghost/issues"
30
+ },
31
+ "homepage": "https://github.com/lamallamadel/ghost#readme",
32
+ "engines": {
33
+ "node": ">=14.0.0"
34
+ }
35
+ }