oft-agt 0.5.17 → 0.5.19

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
@@ -18,7 +18,7 @@ import { acceptSecret } from './secret.mjs';
18
18
  import { createUsageCollector } from './usage.mjs';
19
19
  import { instanceDirectory, resolveInstance, listInstances, rememberInstance } from './instance.mjs';
20
20
  import { AGENT_VERSION } from './version.mjs';
21
- import { installOfficialUpdate } from './update.mjs';
21
+ import { installOfficialUpdate, currentDirectory, newer } from './update.mjs';
22
22
  import { supervise } from './supervisor.mjs';
23
23
 
24
24
  const baseHome = process.env.OFTONE_AGENT_HOME || path.join(homedir(), '.oftone-agent');
@@ -233,6 +233,7 @@ function printHelp() {
233
233
  start | run Démarrer en arrière-plan, sans ressaisir le token
234
234
  stop Arrêter l’agent et son bot, conserver la configuration
235
235
  restart Redémarrer en arrière-plan
236
+ update | upgrade Mettre à jour l’instance et redémarrer sans perdre sa configuration
236
237
  status Afficher l’état local
237
238
  logs Lire les 100 dernières lignes du journal
238
239
  token [--stdin] Régénérer l’authentification agent ; --stdin lit 64 caractères hexadécimaux
@@ -243,9 +244,16 @@ Options : --bot=IDENTIFIANT_COURT (voir list) pour choisir un bot ; start --fore
243
244
  Après un redémarrage du serveur : start. La configuration est conservée sur disque.
244
245
  Documentation : https://doc-builder.oftone.fr/agent`);
245
246
  }
247
+ async function installSource(home, source) {
248
+ const install = path.join(home, 'installation');
249
+ await mkdir(install, { recursive: true, mode: 0o700 });
250
+ if (path.resolve(source) !== path.resolve(install)) for (const entry of ['cli.mjs', 'instance.mjs', 'version.mjs', 'release-key.mjs', 'update.mjs', 'supervisor.mjs', 'secret.mjs', 'usage.mjs', 'compiler.mjs', 'compile-plan.mjs', 'package.json', 'npm-shrinkwrap.json', 'runtime', 'templates']) await cp(path.join(source, entry), path.join(install, entry), { recursive: true });
251
+ await save(path.join(home, 'agent-release.json'), { directory: 'installation' });
252
+ }
253
+
246
254
  async function main() {
247
255
  const [requestedCommand, ...args] = process.argv.slice(2);
248
- const command = requestedCommand==='delete'?'remove':requestedCommand || 'login';
256
+ const command = requestedCommand==='delete'?'remove':requestedCommand==='update'?'upgrade':requestedCommand || 'login';
249
257
  const source = path.dirname(fileURLToPath(import.meta.url));
250
258
  if (['help', '--help', '-h'].includes(command)) { printHelp(); return; }
251
259
  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; }
@@ -302,22 +310,31 @@ async function main() {
302
310
  if (command === 'upgrade') {
303
311
  await stopBackground(home);
304
312
  const source = path.dirname(fileURLToPath(import.meta.url));
305
- const install = path.join(home, 'installation');
306
- await mkdir(install, { recursive: true, mode: 0o700 });
307
- if (path.resolve(source) !== path.resolve(install)) for (const entry of ['cli.mjs', 'instance.mjs', 'version.mjs', 'release-key.mjs', 'update.mjs', 'supervisor.mjs', 'secret.mjs', 'usage.mjs', 'compiler.mjs', 'compile-plan.mjs', 'package.json', 'npm-shrinkwrap.json', 'runtime', 'templates']) await cp(path.join(source, entry), path.join(install, entry), { recursive: true });
308
- await save(path.join(home, 'agent-release.json'), { directory: 'installation' });
313
+ await installSource(home, source);
309
314
  console.log('Agent mis à jour.');
310
315
  if (args.includes('--foreground')) { process.env.OFTONE_FOREGROUND = '1'; return supervise(home, source); }
311
316
  await startBackground(home, source); console.log('Agent démarré en arrière-plan.'); return;
312
317
  }
313
318
  if (['run', 'start', 'restart'].includes(command)) {
314
- if (command === 'restart') await stopBackground(home);
319
+ const installed = await currentDirectory(home, source);
320
+ const installedVersion = JSON.parse(await readFile(path.join(installed, 'package.json'), 'utf8')).version;
321
+ const upgrading = newer(AGENT_VERSION, installedVersion);
322
+ if (command === 'restart' || upgrading) await stopBackground(home);
323
+ if (upgrading) { await installSource(home, source); console.log('Agent mis à jour vers ' + AGENT_VERSION + '.'); }
315
324
  await completeCredentialRotation(home, JSON.parse(await readFile(configPath, 'utf8')));
316
325
  if (args.includes('--foreground')) { process.env.OFTONE_FOREGROUND = '1'; return supervise(home, source); }
317
326
  await startBackground(home, source); console.log('Agent actif en arrière-plan. Aucun token à ressaisir.'); return;
318
327
  }
319
328
  if (command === 'stop') { await stopBackground(home); console.log('Agent arrêté. Sa configuration est conservée.'); return; }
320
- if (command === 'status') { const state = await localStatus(home); console.log(state.legacy ? 'Agent actif au premier plan (ancienne installation).' : state.running ? 'Agent actif en arrière-plan.' : state.starting ? 'Agent en cours de démarrage.' : 'Agent arrêté.'); return; }
329
+ if (command === 'status') {
330
+ const state = await localStatus(home);
331
+ console.log(state.legacy ? 'Agent actif au premier plan (ancienne installation).' : state.running ? 'Agent actif en arrière-plan.' : state.starting ? 'Agent en cours de démarrage.' : 'Agent arrêté.');
332
+ const installed = await currentDirectory(home, source);
333
+ const version = JSON.parse(await readFile(path.join(installed, 'package.json'), 'utf8')).version;
334
+ console.log('Instance installée : ' + version + ' · CLI : ' + AGENT_VERSION);
335
+ if (newer(AGENT_VERSION, version)) console.log('Exécutez update pour appliquer la nouvelle version à cette instance.');
336
+ return;
337
+ }
321
338
  if (command === 'logs') { process.stdout.write(await readLogs(home)); return; }
322
339
  if (command === 'token') {
323
340
  const state = await localStatus(home); await stopBackground(home);
package/compile-plan.mjs CHANGED
@@ -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);
@@ -203,7 +207,7 @@ export function compile(input) {
203
207
 
204
208
  const normal = normalizeMessageConfig(config);
205
209
 
206
- if (!['reply', 'channel', 'dm'].includes(normal.sendMode)) fail(`/${name}/${current} : type d’envoi inconnu.`);
210
+ if (!['reply', 'channel', 'dm', 'edit'].includes(normal.sendMode)) fail(`/${name}/${current} : type d’envoi inconnu.`);
207
211
 
208
212
  if (config.sendMode && config.replyMode && !['command', 'block', 'message'].includes(config.replyMode)) fail(`/${name}/${current} : mode de réponse inconnu.`);
209
213
 
@@ -217,12 +221,13 @@ export function compile(input) {
217
221
 
218
222
  if (error) fail(`/${name}/${current} : ${error}`);
219
223
 
220
- if (normal.sendMode === 'reply' && normal.replyMode === 'block' && !emittedMessages.has(normal.targetBlockId)) fail(`/${name}/${current} : le message cible doit être envoyé avant la réponse.`);
224
+ if ((normal.sendMode === 'edit' || normal.sendMode === 'reply' && normal.replyMode === 'block') && !emittedMessages.has(normal.targetBlockId)) fail(`/${name}/${current} : le message cible doit être envoyé avant la réponse.`);
221
225
 
222
226
  // Only accepted arguments are emitted; tokens and unrelated JSON properties are discarded.
223
227
 
224
228
  args = Object.fromEntries(['text', 'embeds', 'sendMode', 'replyMode', 'targetBlockId', 'messageId', 'channelMode', 'channelId', 'createdChannelBlockId', 'dmMode', 'userId', 'ephemeral'].map(key => [key, normal[key]]));
225
229
 
230
+ if (normal.sendMode === 'edit') args.keepButtons = normal.keepButtons;
226
231
  args.embeds = normal.embeds.map(embed => ({
227
232
 
228
233
  ...Object.fromEntries(['title', 'description', 'url', 'color', 'timestamp'].filter(key => embed[key] !== undefined).map(key => [key, embed[key]])),
@@ -246,8 +251,8 @@ export function compile(input) {
246
251
  if(children.length) args.buttons=children.map(child=>({id:child.id,config:validateButton(child.config)}));
247
252
  for(const child of children){
248
253
  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});
254
+ const childCalls=compilePath(child.id,emittedMessages,0,depth+1,path);
255
+ if (!buttonBranches.some(branch => branch.id === child.id)) buttonBranches.push({id:child.id,parentId:current,config,calls:childCalls});
251
256
  }
252
257
 
253
258
  } 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.17",
3
+ "version": "0.5.19",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "oft-agt",
9
- "version": "0.5.17",
9
+ "version": "0.5.19",
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.17",
3
+ "version": "0.5.19",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "engines": {
@@ -60,7 +60,7 @@ async function executeAdmitted(command, interaction, client, options) {
60
60
 
61
61
  for (const call of calls) {
62
62
 
63
- if (++count > 500) throw new Error('Plan d’exécution trop volumineux.');
63
+ if (++count > 10000) throw new Error('Plan d’exécution trop volumineux.');
64
64
 
65
65
  if (call.fn === 'condition') {
66
66
 
@@ -135,7 +135,7 @@ async function executeAdmitted(command, interaction, client, options) {
135
135
  context.signal.throwIfAborted();
136
136
 
137
137
  const args=await (call.fn==='apiRequest'?interpolateApiArguments:interpolateVariables)(call.args,context.variableOptions);
138
- const prepared=call.fn==='sendMessage'&&args.buttons?.length?(options.buttonRouter||buttonRouter).prepare(command,args.buttons,context,optionValues):null;
138
+ const prepared=call.fn==='sendMessage'&&!(args.sendMode==='edit'&&args.keepButtons!==false)&&args.buttons?.length?(options.buttonRouter||buttonRouter).prepare(command,args.buttons,context,optionValues):null;
139
139
  const invoke=async()=>{try{return await registry[call.fn](context,{...args,...(prepared?{components:prepared.components}:{})});}catch(error){prepared?.cancel();throw error;}};
140
140
 
141
141
  let result;try{result=call.fn==='wait'?await invoke():await options.scheduler.run(invoke,{signal:context.signal});}catch(error){prepared?.cancel();throw error;}
@@ -162,7 +162,7 @@ async function executeAdmitted(command, interaction, client, options) {
162
162
  context.signal.throwIfAborted();
163
163
 
164
164
  const args=interpolateError(await (call.fn==='apiRequest'?interpolateApiArguments:interpolateVariables)(call.args,{...context.variableOptions,base:{...context.variableOptions.base,...optionValues,...variables}}),variables);
165
- const prepared=call.fn==='sendMessage'&&args.buttons?.length?(options.buttonRouter||buttonRouter).prepare(command,args.buttons,context,{...optionValues,...variables}):null;
165
+ const prepared=call.fn==='sendMessage'&&!(args.sendMode==='edit'&&args.keepButtons!==false)&&args.buttons?.length?(options.buttonRouter||buttonRouter).prepare(command,args.buttons,context,{...optionValues,...variables}):null;
166
166
  const invoke=async()=>{try{return await registry[call.fn](context,{...args,...(prepared?{components:prepared.components}:{})});}catch(error){prepared?.cancel();throw error;}};
167
167
 
168
168
  let result;try{result=call.fn==='wait'?await invoke():await options.scheduler.run(invoke,{signal:context.signal});}catch(error){prepared?.cancel();throw error;}
@@ -63,6 +63,14 @@ async function sendMessage(context, parameters) {
63
63
 
64
64
  const { interaction, client, messages } = context;
65
65
 
66
+ if (config.sendMode === 'edit') {
67
+ const message = messages.get(config.targetBlockId);
68
+ if (!message?.edit || !client.user?.id || message.author?.id !== client.user.id) throw runtimeError('MESSAGE_NOT_FOUND');
69
+ const editedPayload = { ...payload, content: config.text };
70
+ if (config.keepButtons) delete editedPayload.components;
71
+ else editedPayload.components = parameters.components || [];
72
+ return message.edit(editedPayload);
73
+ }
66
74
  if (interaction.eventTrigger && config.ephemeral) throw runtimeError('MESSAGE_INVALID');
67
75
  if (interaction.eventTrigger && config.sendMode === 'reply' && config.replyMode === 'command') {
68
76
  if (interaction.eventTrigger !== 'messageCreate' || !interaction.eventMessage?.reply) throw runtimeError('MESSAGE_NOT_FOUND');
@@ -29,6 +29,7 @@ export function normalizeMessageConfig(config = {}) {
29
29
  userId: config.userId || '',
30
30
 
31
31
  ephemeral: Boolean(config.ephemeral),
32
+ keepButtons: config.keepButtons !== false,
32
33
 
33
34
  };
34
35
 
@@ -96,7 +97,7 @@ export function messageConfigError(raw, nodes, allowVariables = true) {
96
97
 
97
98
  if (total > 6000) return 'L’ensemble des embeds est limité à 6 000 caractères.';
98
99
 
99
- if (config.sendMode === 'reply' && config.replyMode === 'block' && (!config.targetBlockId || (nodes && !nodes.some(node => (node.id === config.targetBlockId && (node.type === 'sendMessage' || node.data?.kind === 'message')))))) return 'Choisissez un bloc de message existant.';
100
+ if ((config.sendMode === 'edit' || config.sendMode === 'reply' && config.replyMode === 'block') && (!config.targetBlockId || (nodes && !nodes.some(node => (node.id === config.targetBlockId && (node.type === 'sendMessage' || node.data?.kind === 'message')))))) return 'Choisissez un bloc de message existant.';
100
101
 
101
102
  if (config.sendMode === 'channel' && config.channelMode === 'created' && (!config.createdChannelBlockId || (nodes && !nodes.some(n=>n.id===config.createdChannelBlockId && (n.type==='createChannel'||n.data?.kind==='createChannel'))))) return 'Choisissez un bloc de création de salon.';
102
103
 
@@ -1 +1 @@
1
- export const RUNTIME_VERSION = '0.5.17';
1
+ export const RUNTIME_VERSION = '0.5.19';
package/update.mjs CHANGED
@@ -1,6 +1,6 @@
1
1
  import { createHash, verify } from 'node:crypto';
2
2
  import { gunzipSync } from 'node:zlib';
3
- import { readFile, writeFile, mkdir, rename, access, symlink } from 'node:fs/promises';
3
+ import { readFile, writeFile, mkdir, rename, access, symlink, realpath } from 'node:fs/promises';
4
4
  import { spawn } from 'node:child_process';
5
5
  import path from 'node:path';
6
6
  import { RELEASE_PUBLIC_KEY } from './release-key.mjs';
@@ -80,6 +80,16 @@ function run(args, cwd) {
80
80
  child.once('exit', code => { clearTimeout(timer); code === 0 ? resolve() : reject(new Error('UPDATE_CHECK')); });
81
81
  });
82
82
  }
83
+ // Package version changes do not change the installed dependency tree.
84
+ export function sameDependencies(a, b) {
85
+ const normalize = bytes => { const lock = JSON.parse(bytes.toString()); delete lock.version; if (lock.packages?.['']) delete lock.packages[''].version; return lock; };
86
+ return JSON.stringify(normalize(a)) === JSON.stringify(normalize(b));
87
+ }
88
+ export async function findNpmCli() {
89
+ const candidates = [process.env.npm_execpath, path.join(path.dirname(process.execPath), 'node_modules/npm/bin/npm-cli.js'), ...String(process.env.PATH || '').split(path.delimiter).map(dir => path.join(dir, 'npm'))].filter(Boolean);
90
+ for (const candidate of candidates) { try { const resolved = await realpath(candidate); if (path.basename(resolved) === 'npm-cli.js') return resolved; } catch {} }
91
+ throw new Error('UPDATE_DEPENDENCIES');
92
+ }
83
93
  export async function installUpdate(release, { home, currentVersion, source, report = () => {}, fetchRelease = fetch, publicKey = RELEASE_PUBLIC_KEY, prepareDependencies, check } = {}) {
84
94
  const url = validateRelease(release, currentVersion, publicKey);
85
95
  report('downloading');
@@ -99,12 +109,10 @@ export async function installUpdate(release, { home, currentVersion, source, rep
99
109
  if (prepareDependencies) await prepareDependencies(destination);
100
110
  else {
101
111
  const oldLock = await readFile(path.join(source, 'npm-shrinkwrap.json'));
102
- if (oldLock.equals(entries.get('npm-shrinkwrap.json'))) {
112
+ if (sameDependencies(oldLock, entries.get('npm-shrinkwrap.json'))) {
103
113
  try { await access(path.join(destination, 'node_modules')); } catch { await symlink(path.join(source, 'node_modules'), path.join(destination, 'node_modules'), process.platform === 'win32' ? 'junction' : 'dir'); }
104
114
  } else {
105
- const npm = process.env.npm_execpath || path.join(path.dirname(process.execPath), 'node_modules/npm/bin/npm-cli.js');
106
- if (path.basename(npm) !== 'npm-cli.js') throw new Error('UPDATE_DEPENDENCIES');
107
- await access(npm);
115
+ const npm = await findNpmCli();
108
116
  await run([npm, 'ci', '--omit=dev', '--ignore-scripts', '--no-audit', '--no-fund', '--registry=https://registry.npmjs.org'], destination);
109
117
  }
110
118
  }
package/version.mjs CHANGED
@@ -1 +1 @@
1
- export const AGENT_VERSION = '0.5.17';
1
+ export const AGENT_VERSION = '0.5.19';