@fkom13/mcp-sftp-orchestrator 6.0.0 → 11.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/queue.js CHANGED
@@ -5,8 +5,11 @@ import config from './config.js';
5
5
 
6
6
  const QUEUE_FILE = path.join(config.dataDir, 'queue.json');
7
7
  const QUEUE_BACKUP = path.join(config.dataDir, 'queue.backup.json');
8
- const SAVE_INTERVAL = 5000; // Sauvegarder toutes les 5 secondes
9
- const MAX_QUEUE_SIZE = 1000; // Limite de tâches en mémoire
8
+ const SAVE_INTERVAL = 5000;
9
+ const MAX_QUEUE_SIZE = 1000;
10
+
11
+ // ✅ Mode silencieux par défaut (logs désactivés sauf si MCP_DEBUG=true)
12
+ const SILENT_MODE = process.env.MCP_DEBUG !== 'true';
10
13
 
11
14
  const jobQueue = {};
12
15
  const logHistory = [];
@@ -20,31 +23,27 @@ async function loadQueue() {
20
23
  try {
21
24
  const data = await fs.readFile(QUEUE_FILE, 'utf-8');
22
25
  const savedQueue = JSON.parse(data);
23
-
24
- // Restaurer les tâches non terminées
26
+
25
27
  for (const [id, job] of Object.entries(savedQueue)) {
26
- // Convertir les dates string en objets Date
27
28
  if (job.createdAt) job.createdAt = new Date(job.createdAt);
28
29
  if (job.updatedAt) job.updatedAt = new Date(job.updatedAt);
29
30
  if (job.reminderAt) job.reminderAt = new Date(job.reminderAt);
30
-
31
- // Marquer les tâches running comme crashed
31
+
32
32
  if (job.status === 'running') {
33
33
  job.status = 'crashed';
34
34
  job.crashedAt = new Date();
35
35
  job.canRetry = true;
36
36
  log('warn', `Tâche ${id} marquée comme crashed (reprise après redémarrage)`);
37
37
  }
38
-
38
+
39
39
  jobQueue[id] = job;
40
40
  }
41
-
41
+
42
42
  log('info', `${Object.keys(jobQueue).length} tâches restaurées depuis la sauvegarde`);
43
43
  } catch (err) {
44
44
  if (err.code !== 'ENOENT') {
45
45
  log('error', `Erreur lors du chargement de la queue: ${err.message}`);
46
-
47
- // Essayer de charger la sauvegarde de secours
46
+
48
47
  try {
49
48
  const backupData = await fs.readFile(QUEUE_BACKUP, 'utf-8');
50
49
  const backupQueue = JSON.parse(backupData);
@@ -59,58 +58,62 @@ async function loadQueue() {
59
58
 
60
59
  let isSaving = false;
61
60
 
62
- // Sauvegarder la queue
61
+ let saveLock = null;
62
+
63
63
  async function saveQueue() {
64
- if (!isDirty || isSaving) return;
65
-
66
- isSaving = true;
67
- try {
68
- // Créer une sauvegarde de l'ancienne queue
64
+ if (!isDirty) return;
65
+
66
+ // Attendre le verrou précédent
67
+ if (saveLock) await saveLock;
68
+
69
+ saveLock = (async () => {
70
+ isSaving = true;
69
71
  try {
70
- await fs.copyFile(QUEUE_FILE, QUEUE_BACKUP);
71
- } catch (e) {
72
- // Ignorer si le fichier n'existe pas encore
73
- }
74
-
75
- // Filtrer les tâches terminées anciennes (garder 24h)
76
- const now = Date.now();
77
- const filteredQueue = {};
78
-
79
- for (const [id, job] of Object.entries(jobQueue)) {
80
- const age = now - new Date(job.createdAt).getTime();
81
- const isRecent = age < 86400000; // 24 heures
82
- const isActive = ['pending', 'running', 'crashed'].includes(job.status);
83
-
84
- if (isActive || isRecent) {
85
- filteredQueue[id] = job;
72
+ try {
73
+ await fs.copyFile(QUEUE_FILE, QUEUE_BACKUP);
74
+ } catch (e) {
75
+ // Ignorer si le fichier n'existe pas
86
76
  }
77
+
78
+ const now = Date.now();
79
+ const filteredQueue = {};
80
+
81
+ for (const [id, job] of Object.entries(jobQueue)) {
82
+ const age = now - new Date(job.createdAt).getTime();
83
+ const isRecent = age < 86400000;
84
+ const isActive = ['pending', 'running', 'crashed'].includes(job.status);
85
+
86
+ if (isActive || isRecent) {
87
+ filteredQueue[id] = job;
88
+ }
89
+ }
90
+
91
+ await fs.writeFile(
92
+ QUEUE_FILE,
93
+ JSON.stringify(filteredQueue, null, 2)
94
+ );
95
+
96
+ isDirty = false;
97
+ log('debug', `Queue sauvegardée (${Object.keys(filteredQueue).length} tâches)`);
98
+ } catch (err) {
99
+ log('error', `Erreur lors de la sauvegarde de la queue: ${err.message}`);
100
+ } finally {
101
+ isSaving = false;
102
+ saveLock = null;
87
103
  }
88
-
89
- // Sauvegarder la queue filtrée
90
- await fs.writeFile(
91
- QUEUE_FILE,
92
- JSON.stringify(filteredQueue, null, 2)
93
- );
94
-
95
- isDirty = false;
96
- log('debug', `Queue sauvegardée (${Object.keys(filteredQueue).length} tâches)`);
97
- } catch (err) {
98
- log('error', `Erreur lors de la sauvegarde de la queue: ${err.message}`);
99
- } finally {
100
- isSaving = false;
101
- }
104
+ })();
105
+
106
+ return saveLock;
102
107
  }
103
108
 
104
- // Démarrer la sauvegarde automatique
105
109
  function startAutoSave() {
106
110
  if (saveTimer) clearInterval(saveTimer);
107
-
111
+
108
112
  saveTimer = setInterval(() => {
109
113
  saveQueue();
110
114
  }, SAVE_INTERVAL);
111
115
  }
112
116
 
113
- // Arrêter la sauvegarde automatique
114
117
  function stopAutoSave() {
115
118
  if (saveTimer) {
116
119
  clearInterval(saveTimer);
@@ -124,31 +127,30 @@ function log(level, message) {
124
127
  if (logHistory.length > MAX_LOGS) {
125
128
  logHistory.shift();
126
129
  }
127
-
128
- // Afficher les logs importants
129
- if (['error', 'warn', 'info'].includes(level)) {
130
+
131
+ // N'afficher les logs que si MCP_DEBUG=true
132
+ if (!SILENT_MODE && ['error', 'warn', 'info'].includes(level)) {
130
133
  const prefix = {
131
134
  error: '[❌ ERROR]',
132
135
  warn: '[⚠️ WARN]',
133
136
  info: '[ℹ️ INFO]',
134
137
  debug: '[🔧 DEBUG]'
135
138
  }[level] || `[${level.toUpperCase()}]`;
136
-
137
- console.log(`${prefix} ${new Date().toISOString().split('T')[1].split('.')[0]} - ${message}`);
139
+
140
+ // TOUJOURS utiliser stderr (pas stdout)
141
+ console.error(`${prefix} ${new Date().toISOString().split('T')[1].split('.')[0]} - ${message}`);
138
142
  }
139
143
  }
140
144
 
141
145
  function addJob(details) {
142
- // Vérifier la taille de la queue
143
146
  if (Object.keys(jobQueue).length >= MAX_QUEUE_SIZE) {
144
- // Nettoyer les vieilles tâches terminées
145
147
  cleanOldJobs();
146
-
148
+
147
149
  if (Object.keys(jobQueue).length >= MAX_QUEUE_SIZE) {
148
150
  throw new Error(`Queue pleine (${MAX_QUEUE_SIZE} tâches max)`);
149
151
  }
150
152
  }
151
-
153
+
152
154
  const id = uuidv4().split('-')[0];
153
155
  const job = {
154
156
  id,
@@ -174,8 +176,7 @@ function updateJobStatus(id, status, data = {}) {
174
176
  const oldStatus = jobQueue[id].status;
175
177
  jobQueue[id].status = status;
176
178
  jobQueue[id].updatedAt = new Date();
177
-
178
- // Copier toutes les données supplémentaires
179
+
179
180
  Object.assign(jobQueue[id], data);
180
181
 
181
182
  if (data.error) {
@@ -186,7 +187,7 @@ function updateJobStatus(id, status, data = {}) {
186
187
  jobQueue[id].completedAt = new Date();
187
188
  const duration = jobQueue[id].completedAt - jobQueue[id].createdAt;
188
189
  jobQueue[id].duration = duration;
189
- log('info', `Tâche ${id} terminée en ${(duration/1000).toFixed(2)}s`);
190
+ log('info', `Tâche ${id} terminée en ${(duration / 1000).toFixed(2)}s`);
190
191
  } else {
191
192
  log('info', `Tâche ${id}: ${oldStatus} -> ${status}`);
192
193
  }
@@ -207,7 +208,7 @@ function getLogs(filter = {}) {
207
208
  if (!filter || Object.keys(filter).length === 0) {
208
209
  return logHistory;
209
210
  }
210
-
211
+
211
212
  return logHistory.filter(log => {
212
213
  if (filter.level && log.level !== filter.level) return false;
213
214
  if (filter.since && new Date(log.timestamp) < new Date(filter.since)) return false;
@@ -216,47 +217,57 @@ function getLogs(filter = {}) {
216
217
  });
217
218
  }
218
219
 
219
- // Nettoyer les vieilles tâches terminées
220
220
  function cleanOldJobs() {
221
221
  const now = Date.now();
222
222
  const toDelete = [];
223
-
223
+ const MAX_AGE = 86400000;
224
+
224
225
  for (const [id, job] of Object.entries(jobQueue)) {
225
- const age = now - new Date(job.createdAt).getTime();
226
- const isOld = age > 86400000; // Plus de 24h
226
+ const createdAt = job.createdAt ? new Date(job.createdAt).getTime() : now;
227
+ if (isNaN(createdAt)) {
228
+ log('warn', `Tâche ${id} a une date de création invalide, conservation`);
229
+ continue;
230
+ }
231
+
232
+ const age = now - createdAt;
233
+
234
+ if (age < 0) {
235
+ log('warn', `Tâche ${id} a une date dans le futur, conservation`);
236
+ continue;
237
+ }
238
+
239
+ const isOld = age > MAX_AGE;
227
240
  const isCompleted = ['completed', 'failed'].includes(job.status);
228
-
241
+
229
242
  if (isOld && isCompleted) {
230
243
  toDelete.push(id);
231
244
  }
232
245
  }
233
-
246
+
234
247
  for (const id of toDelete) {
235
248
  delete jobQueue[id];
236
249
  }
237
-
250
+
238
251
  if (toDelete.length > 0) {
239
252
  log('info', `${toDelete.length} vieilles tâches supprimées de la queue`);
240
253
  isDirty = true;
241
254
  }
242
255
  }
243
256
 
244
- // Réessayer une tâche échouée ou crashed
245
257
  async function retryJob(id) {
246
258
  const job = jobQueue[id];
247
259
  if (!job) {
248
260
  throw new Error(`Tâche ${id} introuvable`);
249
261
  }
250
-
262
+
251
263
  if (!['failed', 'crashed'].includes(job.status)) {
252
264
  throw new Error(`La tâche ${id} ne peut pas être réessayée (statut: ${job.status})`);
253
265
  }
254
-
266
+
255
267
  if (job.retryCount >= job.maxRetries) {
256
268
  throw new Error(`La tâche ${id} a atteint le nombre max de tentatives (${job.maxRetries})`);
257
269
  }
258
-
259
- // Créer une nouvelle tâche basée sur l'ancienne
270
+
260
271
  const newJob = {
261
272
  ...job,
262
273
  id: uuidv4().split('-')[0],
@@ -268,29 +279,27 @@ async function retryJob(id) {
268
279
  error: null,
269
280
  output: null
270
281
  };
271
-
282
+
272
283
  delete newJob.failedAt;
273
284
  delete newJob.crashedAt;
274
285
  delete newJob.completedAt;
275
-
286
+
276
287
  jobQueue[newJob.id] = newJob;
277
288
  isDirty = true;
278
-
289
+
279
290
  log('info', `Tâche ${id} réessayée -> nouvelle tâche ${newJob.id} (tentative ${newJob.retryCount}/${newJob.maxRetries})`);
280
-
291
+
281
292
  return newJob;
282
293
  }
283
294
 
284
- // Obtenir les tâches crashées qui peuvent être reprises
285
295
  function getCrashedJobs() {
286
- return Object.values(jobQueue).filter(job =>
287
- job.status === 'crashed' &&
296
+ return Object.values(jobQueue).filter(job =>
297
+ job.status === 'crashed' &&
288
298
  job.canRetry &&
289
299
  job.retryCount < job.maxRetries
290
300
  );
291
301
  }
292
302
 
293
- // Statistiques de la queue
294
303
  function getStats() {
295
304
  const stats = {
296
305
  total: Object.keys(jobQueue).length,
@@ -299,79 +308,56 @@ function getStats() {
299
308
  avgDuration: 0,
300
309
  successRate: 0
301
310
  };
302
-
311
+
303
312
  let totalDuration = 0;
304
313
  let completedCount = 0;
305
-
314
+
306
315
  for (const job of Object.values(jobQueue)) {
307
- // Par statut
308
316
  stats.byStatus[job.status] = (stats.byStatus[job.status] || 0) + 1;
309
-
310
- // Par type
311
317
  stats.byType[job.type] = (stats.byType[job.type] || 0) + 1;
312
-
313
- // Durée moyenne
318
+
314
319
  if (job.duration) {
315
320
  totalDuration += job.duration;
316
321
  completedCount++;
317
322
  }
318
323
  }
319
-
324
+
320
325
  if (completedCount > 0) {
321
326
  stats.avgDuration = Math.round(totalDuration / completedCount);
322
327
  }
323
-
328
+
324
329
  const totalFinished = (stats.byStatus.completed || 0) + (stats.byStatus.failed || 0);
325
330
  if (totalFinished > 0) {
326
331
  stats.successRate = Math.round((stats.byStatus.completed || 0) / totalFinished * 100);
327
332
  }
328
-
333
+
329
334
  return stats;
330
335
  }
331
336
 
332
- // Initialiser le module
333
337
  async function init() {
334
338
  await loadQueue();
335
339
  startAutoSave();
336
-
337
- // Nettoyer périodiquement
338
- setInterval(cleanOldJobs, 3600000); // Toutes les heures
340
+ setInterval(cleanOldJobs, 3600000);
339
341
  }
340
342
 
341
- // Arrêter proprement
342
343
  async function shutdown() {
343
344
  log('info', 'Arrêt du gestionnaire de queue...');
344
345
  stopAutoSave();
345
346
  await saveQueue();
346
347
  }
347
348
 
348
- // Gérer l'arrêt du processus
349
- process.on('SIGINT', async () => {
350
- await shutdown();
351
- process.exit(0);
352
- });
353
-
354
- process.on('SIGTERM', async () => {
355
- await shutdown();
356
- process.exit(0);
357
- });
358
-
359
- // Démarrer automatiquement
360
- init().catch(err => {
361
- console.error('Erreur lors de l\'initialisation de la queue:', err);
362
- });
363
-
364
- export default {
365
- addJob,
366
- updateJobStatus,
367
- getJob,
368
- getQueue,
369
- getLogs,
349
+ export default {
350
+ addJob,
351
+ updateJobStatus,
352
+ getJob,
353
+ getQueue,
354
+ getLogs,
370
355
  log,
371
356
  retryJob,
372
357
  getCrashedJobs,
373
358
  getStats,
374
359
  cleanOldJobs,
375
360
  saveQueue,
376
- shutdown
361
+ shutdown,
362
+ init
377
363
  };