node-red-contrib-linux-copilot 1.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.
package/README.md ADDED
@@ -0,0 +1,15 @@
1
+ An advanced AI-powered assistant for Linux System Administrators and SREs. This node integrates Multi-LLM support to monitor, diagnose, and manage Linux systems directly from Node-RED.
2
+
3
+
4
+
5
+ ## Features
6
+ - **Multi-Engine Support**: Uses Gemini 2.0 Flash (Free) and OpenRouter Llama 3.3 (Free) with automatic failover.
7
+ - **Auto-Sequential Audit**: Can chain multiple commands (CPU -> RAM -> Disk) in a single request.
8
+ - **Smart Language Detection**: Automatically responds in the user's language.
9
+ - **Secure Execution**: Restricted to a safe list of diagnostic and management commands.
10
+ - **Context Aware**: Maintains a history of the last 10 interactions for complex troubleshooting.
11
+
12
+ ## Installation
13
+ Run the following command in your Node-RED user directory (typically `~/.node-red`):
14
+ ```bash
15
+ npm install node-red-contrib-linux-copilot
@@ -0,0 +1,51 @@
1
+ <script type="text/javascript">
2
+ RED.nodes.registerType('linux-copilot',{
3
+ category: 'advanced',
4
+ color: '#E2D96E',
5
+ defaults: {
6
+ name: {value:""},
7
+ prioDS: {value:"1"},
8
+ prioOR: {value:"2"},
9
+ prioGEM: {value:"3"}
10
+ },
11
+ credentials: {
12
+ deepseekKey: {type:"password"},
13
+ openrouterKey: {type:"password"},
14
+ geminiKey: {type:"password"}
15
+ },
16
+ inputs:1,
17
+ outputs:1,
18
+ icon: "font-awesome/fa-terminal",
19
+ label: function() {
20
+ return this.name || "linux-copilot";
21
+ }
22
+ });
23
+ </script>
24
+
25
+ <script type="text/html" data-template-name="linux-copilot">
26
+ <div class="form-row">
27
+ <label for="node-input-name"><i class="fa fa-tag"></i> Name</label>
28
+ <input type="text" id="node-input-name" placeholder="Name">
29
+ </div>
30
+ <hr>
31
+ <h4>Priorities (1 = First)</h4>
32
+ <div class="form-row">
33
+ <label for="node-input-prioDS">DeepSeek</label>
34
+ <input type="number" id="node-input-prioDS" style="width:50px">
35
+ <input type="password" id="node-input-deepseekKey" placeholder="API Key" style="width:200px">
36
+ </div>
37
+ <div class="form-row">
38
+ <label for="node-input-prioOR">OpenRouter</label>
39
+ <input type="number" id="node-input-prioOR" style="width:50px">
40
+ <input type="password" id="node-input-openrouterKey" placeholder="API Key" style="width:200px">
41
+ </div>
42
+ <div class="form-row">
43
+ <label for="node-input-prioGEM">Gemini Free</label>
44
+ <input type="number" id="node-input-prioGEM" style="width:50px">
45
+ <input type="password" id="node-input-geminiKey" placeholder="API Key" style="width:200px">
46
+ </div>
47
+ </script>
48
+
49
+ <script type="text/html" data-help-name="linux-copilot">
50
+ <p>A multi-LLM Linux SRE Copilot. Supports language detection and auto-sequential diagnostics.</p>
51
+ </script>
@@ -0,0 +1,118 @@
1
+ const axios = require('axios');
2
+ const { exec } = require('child_process');
3
+
4
+ module.exports = function(RED) {
5
+ function LinuxCopilotNode(config) {
6
+ RED.nodes.createNode(this, config);
7
+ const node = this;
8
+
9
+ // --- PROMPT AVEC DÉTECTION DE LANGUE ---
10
+ const omniPrompt = `Tu es un Expert Linux SRE.
11
+ RÈGLES CRUCIALES :
12
+ 1. LANGUE : Détecte la langue de l'utilisateur et réponds EXCLUSIVEMENT dans cette langue (Français, Anglais, Espagnol, etc.).
13
+ 2. ANALYSE : Si tu reçois un résultat de commande, analyse-le brièvement.
14
+ 3. ACTION : Propose et exécute l'étape suivante logicielle.
15
+ 4. FORMAT JSON : {"speech": "ton explication dans la langue détectée", "cmd": "commande linux ou none"}`;
16
+
17
+ const formatHistory = (history) => history.map(h => ({
18
+ role: h.role === "model" ? "assistant" : h.role,
19
+ content: String(h.content).substring(0, 800)
20
+ }));
21
+
22
+ const engines = {
23
+ gemini: async (history, key) => {
24
+ const res = await axios.post(`https://generativelanguage.googleapis.com/v1beta/models/gemini-2.0-flash-exp:generateContent?key=${key.trim()}`, {
25
+ contents: history.map(h => ({ role: h.role === "assistant" ? "model" : h.role, parts: [{ text: h.content }] })),
26
+ system_instruction: { parts: [{ text: omniPrompt }] },
27
+ generationConfig: { responseMimeType: "application/json" }
28
+ }, { timeout: 15000 });
29
+ return JSON.parse(res.data.candidates[0].content.parts[0].text);
30
+ },
31
+ openrouter: async (history, key) => {
32
+ const res = await axios.post('https://openrouter.ai/api/v1/chat/completions', {
33
+ model: "meta-llama/llama-3.3-70b-instruct:free",
34
+ messages: [{ role: 'system', content: omniPrompt }, ...formatHistory(history)]
35
+ }, {
36
+ headers: { 'Authorization': `Bearer ${key.trim()}`, 'HTTP-Referer': 'https://nodered.org', 'Content-Type': 'application/json' },
37
+ timeout: 25000
38
+ });
39
+ const match = res.data.choices[0].message.content.match(/\{[\s\S]*\}/);
40
+ return JSON.parse(match ? match[0] : res.data.choices[0].message.content);
41
+ },
42
+ deepseek: async (history, key) => {
43
+ const res = await axios.post('https://api.deepseek.com/chat/completions', {
44
+ model: "deepseek-chat",
45
+ messages: [{ role: 'system', content: omniPrompt }, ...formatHistory(history)],
46
+ response_format: { type: 'json_object' }
47
+ }, { headers: { 'Authorization': `Bearer ${key.trim()}` }, timeout: 20000 });
48
+ return JSON.parse(res.data.choices[0].message.content);
49
+ }
50
+ };
51
+
52
+ node.on('input', async function(msg) {
53
+ const chatId = msg.payload.chatId || config.chatId || "1457427557";
54
+ let userText = msg.payload.content || (typeof msg.payload === 'string' ? msg.payload : "");
55
+ let loopCount = msg.loopCount || 0;
56
+
57
+ if (loopCount > 4) return node.status({fill:"blue", text:"Fin de séquence"});
58
+
59
+ let history = node.context().get('history') || [];
60
+ if (userText.toLowerCase() === "reset") {
61
+ node.context().set('history', []);
62
+ return node.send({ payload: { chatId: chatId, type: "message", content: "♻️ Reset OK." } });
63
+ }
64
+
65
+ if (!userText) return;
66
+ history.push({ role: "user", content: userText });
67
+
68
+ let queue = [
69
+ { id: 'deepseek', p: parseInt(config.prioDS) || 1, k: node.credentials.deepseekKey, n: "DeepSeek" },
70
+ { id: 'openrouter', p: parseInt(config.prioOR) || 2, k: node.credentials.openrouterKey, n: "OpenRouter" },
71
+ { id: 'gemini', p: parseInt(config.prioGEM) || 3, k: node.credentials.geminiKey, n: "Gemini" }
72
+ ].filter(q => q.k).sort((a, b) => a.p - b.p);
73
+
74
+ let aiData = null;
75
+ let engineUsed = "";
76
+
77
+ for (let e of queue) {
78
+ try {
79
+ node.status({fill:"yellow", text: `Appel ${e.n}...`});
80
+ aiData = await engines[e.id](history, e.k);
81
+ if (aiData && (aiData.speech || aiData.cmd)) { engineUsed = e.n; break; }
82
+ } catch (err) { node.warn(`Échec ${e.n}`); }
83
+ }
84
+
85
+ if (!aiData) return;
86
+
87
+ if (aiData.speech) {
88
+ node.send({ payload: { chatId: chatId, type: "message", content: `🤖 <b>${engineUsed}</b> : ${aiData.speech}`, options: { parse_mode: "HTML" } } });
89
+ }
90
+
91
+ let cmd = (aiData.cmd || "").trim();
92
+ const safeWords = ['ls', 'df', 'free', 'uptime', 'top', 'ps', 'cat', 'grep', 'iostat', 'netstat', 'ss', 'ip', 'systemctl', 'journalctl'];
93
+
94
+ if (cmd && cmd !== "none" && safeWords.some(w => cmd.includes(w))) {
95
+ if (cmd.startsWith("top") && !cmd.includes("-b")) cmd = "top -b -n 1 | head -n 12";
96
+
97
+ exec(cmd, { timeout: 10000 }, (err, stdout, stderr) => {
98
+ let res = (stdout || stderr || "OK").substring(0, 800);
99
+ node.send({ payload: { chatId: chatId, type: "message", content: `📟 <b>Terminal (${cmd})</b> :\n<pre>${res}</pre>`, options: { parse_mode: "HTML" } } });
100
+
101
+ setTimeout(() => {
102
+ node.emit("input", {
103
+ payload: { chatId: chatId, content: `RÉSULTAT ${cmd} :\n${res}` },
104
+ loopCount: loopCount + 1
105
+ });
106
+ }, 1200);
107
+ });
108
+ }
109
+
110
+ history.push({ role: "assistant", content: aiData.speech || "Action" });
111
+ node.context().set('history', history.slice(-10));
112
+ node.status({fill:"green", text:`Langue auto : ${engineUsed}`});
113
+ });
114
+ }
115
+ RED.nodes.registerType('linux-copilot', LinuxCopilotNode, {
116
+ credentials: { geminiKey: {type:"password"}, openrouterKey: {type:"password"}, deepseekKey: {type:"password"} }
117
+ });
118
+ }
package/package.json ADDED
@@ -0,0 +1,58 @@
1
+ {
2
+
3
+ "name": "node-red-contrib-linux-copilot",
4
+
5
+ "version": "1.0.1",
6
+
7
+ "description": "A versatile SRE & SysAdmin AI FREE Copilot for Linux, supporting Gemini and OpenRouter (Llama 3.3).",
8
+
9
+ "main": "linux-copilot.js",
10
+
11
+ "scripts": {
12
+
13
+ "test": "echo \"Error: no test specified\" && exit 1"
14
+
15
+ },
16
+
17
+ "keywords": [
18
+
19
+ "node-red",
20
+
21
+ "linux",
22
+
23
+ "sre",
24
+
25
+ "ai",
26
+
27
+ "copilot",
28
+
29
+ "gemini",
30
+
31
+ "openrouter",
32
+
33
+ "sysadmin"
34
+
35
+ ],
36
+
37
+ "author": "surprise_dev",
38
+
39
+ "license": "MIT",
40
+
41
+ "node-red": {
42
+
43
+ "nodes": {
44
+
45
+ "linux-copilot": "linux-copilot.js"
46
+
47
+ }
48
+
49
+ },
50
+
51
+ "dependencies": {
52
+
53
+ "axios": "^1.6.0"
54
+
55
+ }
56
+
57
+ }
58
+