oft-agt 0.5.16 → 0.5.18

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/cli.mjs CHANGED
@@ -51,6 +51,7 @@ async function credentials(config) {
51
51
  }
52
52
  async function stopBot() {
53
53
  const current = child;
54
+ child = null;
54
55
  if (!current || current.exitCode !== null || current.signalCode) return;
55
56
  await new Promise(resolve => { const timer = setTimeout(() => current.kill('SIGKILL'), 5000); current.once('exit', () => { clearTimeout(timer); resolve(); }); current.kill('SIGTERM'); });
56
57
  }
@@ -89,6 +90,7 @@ async function deploy(config, message, notify) {
89
90
  await run(process.execPath, ['bot.mjs', '--deploy'], { cwd: target, env, stdio: 'ignore' });
90
91
  const previous = child;
91
92
  const previousId = active;
93
+ child = null;
92
94
  if (previous && previous.exitCode === null && !previous.signalCode) {
93
95
  await new Promise(resolve => { const timeout = setTimeout(() => { previous.kill('SIGKILL'); }, 5000); previous.once('exit', () => { clearTimeout(timeout); resolve(); }); previous.kill('SIGTERM'); });
94
96
  }
@@ -109,7 +111,8 @@ async function deploy(config, message, notify) {
109
111
  }
110
112
  throw error;
111
113
  }
112
- child.once('exit', () => { if (active === message.id) notify('failed'); });
114
+ const deployedChild=child;
115
+ deployedChild.once('exit', () => { if (child === deployedChild && active === message.id) notify('runtime-failed'); });
113
116
  active = message.id;
114
117
  await save(path.join(home, 'active.json'), { id: message.id });
115
118
  notify('running');
@@ -203,7 +206,7 @@ async function start(config) {
203
206
  if (message.type === 'deploy') {
204
207
  console.log(`[${new Date().toISOString()}] Nouveau déploiement reçu.`);
205
208
  if (await exists(path.join(home, 'paused.json'))) await unlink(path.join(home, 'paused.json'));
206
- try { await deploy(config, message, async state => { send({ type: 'deployment-status', id: message.id, state, runtimeVersion:state==='running'?await runtimeVersion():null }); console.log(({ received: 'Déploiement validé.', installing: 'Préparation du bot.', running: 'Déploiement terminé. Bot démarré.', failed: 'Échec du déploiement.' })[state] || 'État du déploiement modifié.'); }); }
209
+ try { await deploy(config, message, async state => { if(state==='runtime-failed'){send({type:'runtime-status',state:'failed'});return;} send({ type: 'deployment-status', id: message.id, state, runtimeVersion:state==='running'?await runtimeVersion():null }); console.log(({ received: 'Déploiement validé.', installing: 'Préparation du bot.', running: 'Déploiement terminé. Bot démarré.', failed: 'Échec du déploiement.' })[state] || 'État du déploiement modifié.'); }); }
207
210
  catch { send({ type: 'deployment-status', id: message.id, state: 'failed' }); console.error('Déploiement refusé ou échoué.'); }
208
211
  }
209
212
  }).catch(() => console.error('Message refusé.'));
@@ -233,7 +236,7 @@ function printHelp() {
233
236
  status Afficher l’état local
234
237
  logs Lire les 100 dernières lignes du journal
235
238
  token [--stdin] Régénérer l’authentification agent ; --stdin lit 64 caractères hexadécimaux
236
- remove --yes Révoquer la liaison et supprimer les données locales de cette instance
239
+ remove | delete --yes Supprimer une instance arrêtée de la liste et révoquer sa liaison
237
240
  list Afficher les instances installées
238
241
  help Afficher cette aide
239
242
  Options : --bot=IDENTIFIANT_COURT (voir list) pour choisir un bot ; start --foreground pour diagnostiquer.
@@ -242,7 +245,7 @@ Documentation : https://doc-builder.oftone.fr/agent`);
242
245
  }
243
246
  async function main() {
244
247
  const [requestedCommand, ...args] = process.argv.slice(2);
245
- const command = requestedCommand || 'login';
248
+ const command = requestedCommand==='delete'?'remove':requestedCommand || 'login';
246
249
  const source = path.dirname(fileURLToPath(import.meta.url));
247
250
  if (['help', '--help', '-h'].includes(command)) { printHelp(); return; }
248
251
  if (command === 'list') { for (const instance of await listInstances(baseHome)) console.log(`${instance.agentId.slice(0, 8)} · ${(await localStatus(instance.home)).running ? 'actif' : 'arrêté'}`); return; }
@@ -327,7 +330,8 @@ async function main() {
327
330
  }
328
331
  if (command === 'remove') {
329
332
  if (!args.includes('--yes')) { console.log('Supprime l’agent sélectionné et ses données locales, y compris son token Discord. Relancez avec remove --yes pour confirmer.'); return; }
330
- await stopBackground(home); const config = JSON.parse(await readFile(configPath, 'utf8'));
333
+ const state=await localStatus(home);if(state.running||state.starting||state.legacy)throw Error('REMOVE_RUNNING');
334
+ const config = JSON.parse(await readFile(configPath, 'utf8'));
331
335
  let revoked = false;
332
336
  try { const response = await fetch(`${config.relay}/v1/agents/${config.object}/remove`, { method: 'POST', headers: { Authorization: `Bearer ${config.credential}` }, signal: AbortSignal.timeout(15000) }); revoked = response.ok; } catch {}
333
337
  await removeLocalInstance(home, baseHome, config);
@@ -335,5 +339,5 @@ async function main() {
335
339
  }
336
340
  throw new Error('COMMAND_UNKNOWN');
337
341
  }
338
- main().catch(error => { const messages = { LOGIN_BUSY: 'Une connexion Discord est déjà en cours sur cette machine.', LOGIN_EXPIRED: 'La demande a expiré. Relancez npx oft-agt login.', LOGIN_START_FAILED: 'Impossible de démarrer la connexion. Réessayez dans quelques minutes.', LOGIN_FAILED: 'Connexion refusée. Relancez npx oft-agt login.', AUTH_ROTATION_BUSY: 'Une modification de l’authentification est déjà en cours pour cette instance. Réessayez après sa fin.', COMMAND_UNKNOWN: 'Commande inconnue. Consultez oftone-agent help.', LEGACY_FOREGROUND: 'Un ancien agent fonctionne au premier plan. Arrêtez-le avec Ctrl+C une seule fois, puis utilisez start.', DAEMON_START_FAILED: 'L’agent n’a pas démarré. Consultez logs.', DAEMON_START_TIMEOUT: 'Démarrage non confirmé. Consultez status et logs avant de réessayer.', DAEMON_STOP_TIMEOUT: 'L’arrêt n’est pas encore confirmé. Consultez status et logs.', AUTH_ROTATION_FAILED: 'Changement d’authentification non confirmé. Réessayez token ou start pour reprendre sans perdre la liaison.', AUTH_ROTATION_INVALID: 'Token d’authentification invalide : 64 caractères hexadécimaux attendus.', PAIRING_INVALID: 'Commande de liaison invalide. Copiez à nouveau la commande complète depuis le dashboard.', PAIRING_REJECTED: 'Commande de liaison expirée, déjà utilisée ou révoquée. Générez-en une nouvelle depuis le bot concerné.', INSTANCE_INVALID: 'Identifiant d’instance invalide. Copiez la commande depuis les paramètres du bot concerné.', INSTANCE_NOT_FOUND: 'Cette liaison n’est pas installée sur cette machine. Utilisez la commande locale de cette instance ou configurez une nouvelle liaison.', INSTANCE_REQUIRED: 'Précisez l’instance avec la commande affichée dans les paramètres du bot ou la commande locale affichée à l’installation.', INSTANCE_EXISTS: 'Cette instance existe déjà. Utilisez sa commande locale de démarrage.', AGENT_ACTIVE: 'Cet agent est déjà actif. Arrêtez uniquement le processus de cette instance avant de le relancer.' }; console.error(messages[error.code || error.message] || 'Échec de l’agent. Vérifiez la liaison et la configuration locale. Aucun secret affiché.'); process.exitCode = 1; });
342
+ main().catch(error => { const messages = { REMOVE_RUNNING:'Arrêtez ce bot avec stop avant de le supprimer de la liste.', LOGIN_BUSY: 'Une connexion Discord est déjà en cours sur cette machine.', LOGIN_EXPIRED: 'La demande a expiré. Relancez npx oft-agt login.', LOGIN_START_FAILED: 'Impossible de démarrer la connexion. Réessayez dans quelques minutes.', LOGIN_FAILED: 'Connexion refusée. Relancez npx oft-agt login.', AUTH_ROTATION_BUSY: 'Une modification de l’authentification est déjà en cours pour cette instance. Réessayez après sa fin.', COMMAND_UNKNOWN: 'Commande inconnue. Consultez oftone-agent help.', LEGACY_FOREGROUND: 'Un ancien agent fonctionne au premier plan. Arrêtez-le avec Ctrl+C une seule fois, puis utilisez start.', DAEMON_START_FAILED: 'L’agent n’a pas démarré. Consultez logs.', DAEMON_START_TIMEOUT: 'Démarrage non confirmé. Consultez status et logs avant de réessayer.', DAEMON_STOP_TIMEOUT: 'L’arrêt n’est pas encore confirmé. Consultez status et logs.', AUTH_ROTATION_FAILED: 'Changement d’authentification non confirmé. Réessayez token ou start pour reprendre sans perdre la liaison.', AUTH_ROTATION_INVALID: 'Token d’authentification invalide : 64 caractères hexadécimaux attendus.', PAIRING_INVALID: 'Commande de liaison invalide. Copiez à nouveau la commande complète depuis le dashboard.', PAIRING_REJECTED: 'Commande de liaison expirée, déjà utilisée ou révoquée. Générez-en une nouvelle depuis le bot concerné.', INSTANCE_INVALID: 'Identifiant d’instance invalide. Copiez la commande depuis les paramètres du bot concerné.', INSTANCE_NOT_FOUND: 'Cette liaison n’est pas installée sur cette machine. Utilisez la commande locale de cette instance ou configurez une nouvelle liaison.', INSTANCE_REQUIRED: 'Précisez l’instance avec la commande affichée dans les paramètres du bot ou la commande locale affichée à l’installation.', INSTANCE_EXISTS: 'Cette instance existe déjà. Utilisez sa commande locale de démarrage.', AGENT_ACTIVE: 'Cet agent est déjà actif. Arrêtez uniquement le processus de cette instance avant de le relancer.' }; console.error(messages[error.code || error.message] || 'Échec de l’agent. Vérifiez la liaison et la configuration locale. Aucun secret affiché.'); process.exitCode = 1; });
339
343
 
package/compile-plan.mjs CHANGED
@@ -46,9 +46,9 @@ export function compile(input) {
46
46
  if (!plain(command)) fail(`Commande ${commandIndex + 1} : configuration absente.`);
47
47
 
48
48
  validateEventGraph(command);
49
- const name = text(command.name ?? entry.name, 'Nom de commande', 32, true);
49
+ const name = text(command.name ?? entry.name, 'Nom', command.trigger?.kind==='event'?100:32, true);
50
50
 
51
- if (!/^[a-z0-9_-]{1,32}$/.test(name)) fail(`/${name} : utilisez des minuscules, chiffres, tirets ou underscores.`);
51
+ if (command.trigger?.kind!=='event' && !/^[a-z0-9_-]{1,32}$/.test(name)) fail(`/${name} : utilisez des minuscules, chiffres, tirets ou underscores.`);
52
52
 
53
53
  if (names.has(name)) fail(`Commande dupliquée : /${name}.`);
54
54
 
@@ -134,7 +134,7 @@ export function compile(input) {
134
134
 
135
135
  if (next.has(edge.source)) fail(`/${name}/${edge.source} : plusieurs sorties ne sont pas encore prises en charge. Reliez les actions en séquence.`);
136
136
 
137
- if (incoming.has(edge.target)) fail(`/${name}/${edge.target} : plusieurs entrées ne sont pas prises en charge.`);
137
+
138
138
 
139
139
  next.set(edge.source, edge.target);
140
140
 
@@ -153,7 +153,9 @@ export function compile(input) {
153
153
  }
154
154
  const visited = new Set(options.map(option => option.id));
155
155
 
156
- function compilePath(rootId, inheritedMessages = new Set(), inheritedWait = 0, depth = 0) {
156
+ let expanded = 0;
157
+ function compilePath(rootId, inheritedMessages = new Set(), inheritedWait = 0, depth = 0, ancestry = new Set()) {
158
+ const path = new Set(ancestry);
157
159
 
158
160
  if (depth > 32) fail(`/${name} : conditions imbriquées trop profondément (32 maximum).`);
159
161
 
@@ -167,7 +169,9 @@ export function compile(input) {
167
169
 
168
170
  while (current) {
169
171
 
170
- if (visited.has(current)) fail(`/${name} : boucle détectée.`);
172
+ if (path.has(current)) fail(`/${name} : boucle détectée.`);
173
+ path.add(current);
174
+ if (++expanded > 10000) fail(`/${name} : parcours trop complexe (10 000 étapes maximum).`);
171
175
 
172
176
  visited.add(current);
173
177
 
@@ -185,7 +189,7 @@ export function compile(input) {
185
189
 
186
190
  const rules = conditionConfigs.get(current).rules;
187
191
 
188
- calls.push({ id: current, fn: 'condition', args: { kind: block.type, rules }, branches: rules.map(rule => ({ id: branches.get(rule.id), ruleId: rule.id, calls: compilePath(branches.get(rule.id), emittedMessages, waitBudget, depth + 1) })), otherwise: { id: branches.get('else'), calls: compilePath(branches.get('else'), emittedMessages, waitBudget, depth + 1) } });
192
+ calls.push({ id: current, fn: 'condition', args: { kind: block.type, rules }, branches: rules.map(rule => ({ id: branches.get(rule.id), ruleId: rule.id, calls: compilePath(branches.get(rule.id), emittedMessages, waitBudget, depth + 1, path) })), otherwise: { id: branches.get('else'), calls: compilePath(branches.get('else'), emittedMessages, waitBudget, depth + 1, path) } });
189
193
 
190
194
  } else if (isVariableBlock(block.type)) {
191
195
  fn = block.type; args = validateVariableBlock(block.type, config, variableDefinitions);
@@ -246,8 +250,8 @@ export function compile(input) {
246
250
  if(children.length) args.buttons=children.map(child=>({id:child.id,config:validateButton(child.config)}));
247
251
  for(const child of children){
248
252
  const config=validateButton(child.config);
249
- const childCalls=compilePath(child.id,emittedMessages,0,depth+1);
250
- buttonBranches.push({id:child.id,parentId:current,config,calls:childCalls});
253
+ const childCalls=compilePath(child.id,emittedMessages,0,depth+1,path);
254
+ if (!buttonBranches.some(branch => branch.id === child.id)) buttonBranches.push({id:child.id,parentId:current,config,calls:childCalls});
251
255
  }
252
256
 
253
257
  } else if (block.type === 'apiRequest') { fn='apiRequest'; args=validateApiRequest(config); if([...blocks.values()].some(b=>b.id!==block.id&&b.type==='apiRequest'&&b.config?.name===args.name))fail('Chaque requête API doit avoir un nom unique.');
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "oft-agt",
3
- "version": "0.5.16",
3
+ "version": "0.5.18",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "oft-agt",
9
- "version": "0.5.16",
9
+ "version": "0.5.18",
10
10
  "license": "UNLICENSED",
11
11
  "dependencies": {
12
12
  "discord.js": "^14.14.0",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "oft-agt",
3
- "version": "0.5.16",
3
+ "version": "0.5.18",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "engines": {
@@ -1 +1 @@
1
- export const RUNTIME_VERSION = '0.5.16';
1
+ export const RUNTIME_VERSION = '0.5.18';
package/usage.mjs CHANGED
@@ -12,7 +12,7 @@ export async function createUsageCollector(home, now = () => Date.now()) {
12
12
  let writing = Promise.resolve();
13
13
  return {
14
14
  record(event) {
15
- if (event?.type !== 'usage' || !/^[a-z0-9_-]{1,32}$/.test(event.command || '') || typeof event.success !== 'boolean' || !Number.isFinite(event.durationMs) || event.durationMs < 0 || event.durationMs > 900000) return;
15
+ if (event?.type !== 'usage' || (typeof event.command!=='string'||!event.command.trim()||event.command.length>100||/[\u0000-\u001f\u007f]/.test(event.command)) || typeof event.success !== 'boolean' || !Number.isFinite(event.durationMs) || event.durationMs < 0 || event.durationMs > 900000) return;
16
16
  const current = Math.floor(now() / HOUR) * HOUR;
17
17
  const hour = event.hour ?? current;
18
18
  if (!Number.isSafeInteger(hour) || hour % HOUR || hour > current || hour < current - 719 * HOUR) return;
package/version.mjs CHANGED
@@ -1 +1 @@
1
- export const AGENT_VERSION = '0.5.16';
1
+ export const AGENT_VERSION = '0.5.18';