@arka-labs/nemesis 1.2.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.
Files changed (100) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +668 -0
  3. package/lib/core/agent-launcher.js +193 -0
  4. package/lib/core/audit.js +210 -0
  5. package/lib/core/connexions.js +80 -0
  6. package/lib/core/flowmap/api.js +111 -0
  7. package/lib/core/flowmap/cli-helpers.js +80 -0
  8. package/lib/core/flowmap/machine.js +281 -0
  9. package/lib/core/flowmap/persistence.js +83 -0
  10. package/lib/core/generators.js +183 -0
  11. package/lib/core/inbox.js +275 -0
  12. package/lib/core/logger.js +20 -0
  13. package/lib/core/mission.js +109 -0
  14. package/lib/core/notewriter/config.js +36 -0
  15. package/lib/core/notewriter/cr.js +237 -0
  16. package/lib/core/notewriter/log.js +112 -0
  17. package/lib/core/notewriter/notes.js +168 -0
  18. package/lib/core/notewriter/paths.js +45 -0
  19. package/lib/core/notewriter/reader.js +121 -0
  20. package/lib/core/notewriter/registry.js +80 -0
  21. package/lib/core/odm.js +191 -0
  22. package/lib/core/profile-picker.js +323 -0
  23. package/lib/core/project.js +287 -0
  24. package/lib/core/registry.js +129 -0
  25. package/lib/core/secrets.js +137 -0
  26. package/lib/core/services.js +45 -0
  27. package/lib/core/team.js +287 -0
  28. package/lib/core/templates.js +80 -0
  29. package/lib/kairos/agent-runner.js +261 -0
  30. package/lib/kairos/claude-invoker.js +90 -0
  31. package/lib/kairos/context-injector.js +331 -0
  32. package/lib/kairos/context-loader.js +108 -0
  33. package/lib/kairos/context-writer.js +45 -0
  34. package/lib/kairos/dispatcher-router.js +173 -0
  35. package/lib/kairos/dispatcher.js +139 -0
  36. package/lib/kairos/event-bus.js +287 -0
  37. package/lib/kairos/event-router.js +131 -0
  38. package/lib/kairos/flowmap-bridge.js +120 -0
  39. package/lib/kairos/hook-handlers.js +351 -0
  40. package/lib/kairos/hook-installer.js +207 -0
  41. package/lib/kairos/hook-prompts.js +54 -0
  42. package/lib/kairos/leader-rules.js +94 -0
  43. package/lib/kairos/pid-checker.js +108 -0
  44. package/lib/kairos/situation-detector.js +123 -0
  45. package/lib/sync/fallback-engine.js +97 -0
  46. package/lib/sync/hcm-client.js +170 -0
  47. package/lib/sync/health.js +47 -0
  48. package/lib/sync/llm-client.js +387 -0
  49. package/lib/sync/nemesis-client.js +379 -0
  50. package/lib/sync/service-session.js +74 -0
  51. package/lib/sync/sync-engine.js +178 -0
  52. package/lib/ui/box.js +104 -0
  53. package/lib/ui/brand.js +42 -0
  54. package/lib/ui/colors.js +57 -0
  55. package/lib/ui/dashboard.js +580 -0
  56. package/lib/ui/error-hints.js +49 -0
  57. package/lib/ui/format.js +61 -0
  58. package/lib/ui/menu.js +306 -0
  59. package/lib/ui/note-card.js +198 -0
  60. package/lib/ui/note-colors.js +26 -0
  61. package/lib/ui/note-detail.js +297 -0
  62. package/lib/ui/note-filters.js +252 -0
  63. package/lib/ui/note-views.js +283 -0
  64. package/lib/ui/prompt.js +81 -0
  65. package/lib/ui/spinner.js +139 -0
  66. package/lib/ui/streambox.js +46 -0
  67. package/lib/ui/table.js +42 -0
  68. package/lib/ui/tree.js +33 -0
  69. package/package.json +53 -0
  70. package/src/cli.js +457 -0
  71. package/src/commands/_helpers.js +119 -0
  72. package/src/commands/audit.js +187 -0
  73. package/src/commands/auth.js +316 -0
  74. package/src/commands/doctor.js +243 -0
  75. package/src/commands/hcm.js +147 -0
  76. package/src/commands/inbox.js +333 -0
  77. package/src/commands/init.js +160 -0
  78. package/src/commands/kairos.js +216 -0
  79. package/src/commands/kars.js +134 -0
  80. package/src/commands/mission.js +275 -0
  81. package/src/commands/notes.js +316 -0
  82. package/src/commands/notewriter.js +296 -0
  83. package/src/commands/odm.js +329 -0
  84. package/src/commands/orch.js +68 -0
  85. package/src/commands/project.js +123 -0
  86. package/src/commands/run.js +123 -0
  87. package/src/commands/services.js +705 -0
  88. package/src/commands/status.js +231 -0
  89. package/src/commands/team.js +572 -0
  90. package/src/config.js +84 -0
  91. package/src/index.js +5 -0
  92. package/templates/project-context.json +10 -0
  93. package/templates/template_CONTRIB-NAME.json +22 -0
  94. package/templates/template_CR-ODM-NAME-000.exemple.json +32 -0
  95. package/templates/template_DEC-NAME-000.json +18 -0
  96. package/templates/template_INTV-NAME-000.json +15 -0
  97. package/templates/template_MISSION_CONTRACT.json +46 -0
  98. package/templates/template_ODM-NAME-000.json +89 -0
  99. package/templates/template_REGISTRY-PROJECT.json +26 -0
  100. package/templates/template_TXN-NAME-000.json +24 -0
package/src/cli.js ADDED
@@ -0,0 +1,457 @@
1
+ import { loadConfig } from './config.js';
2
+ import { renderBrandHeader, renderBrandBanner } from '../lib/ui/brand.js';
3
+ import { titledBox } from '../lib/ui/box.js';
4
+ import { style } from '../lib/ui/colors.js';
5
+ import { interactiveMenu } from '../lib/ui/menu.js';
6
+ import { clearScreen, waitForKey, BACK_ITEM, HOME_ITEM } from './commands/_helpers.js';
7
+ import { detectProject, storeHcmStatus } from '../lib/core/project.js';
8
+ import { readRegistry, getLanes, getPM } from '../lib/core/registry.js';
9
+ import { createHcmClient } from '../lib/sync/hcm-client.js';
10
+
11
+ const COMMANDS = {
12
+ init: () => import('./commands/init.js'),
13
+ team: () => import('./commands/team.js'),
14
+ project: () => import('./commands/project.js'),
15
+ status: () => import('./commands/status.js'),
16
+ doctor: () => import('./commands/doctor.js'),
17
+ mission: () => import('./commands/mission.js'),
18
+ odm: () => import('./commands/odm.js'),
19
+ hcm: () => import('./commands/hcm.js'),
20
+ audit: () => import('./commands/audit.js'),
21
+ services: () => import('./commands/services.js'),
22
+ kars: () => import('./commands/kars.js'),
23
+ notes: () => import('./commands/notes.js'),
24
+ notewriter: () => import('./commands/notewriter.js'),
25
+ kairos: () => import('./commands/kairos.js'),
26
+ auth: () => import('./commands/auth.js'),
27
+ run: () => import('./commands/run.js'),
28
+ orch: () => import('./commands/orch.js'),
29
+ inbox: () => import('./commands/inbox.js'),
30
+ };
31
+
32
+ const VERSION = '1.0.1';
33
+
34
+ // ---------------------------------------------------------------------------
35
+ // Menu principal — 2 zones : acces rapide agents + categories
36
+ // ---------------------------------------------------------------------------
37
+
38
+ export function buildMainMenu(config) {
39
+ const items = [];
40
+
41
+ // --- Zone Acces rapide (si projet detecte) ---
42
+ const project = detectProject(config.cwd);
43
+ if (project) {
44
+ const registry = readRegistry(project.hcm_dir);
45
+ if (registry) {
46
+ const lanes = getLanes(registry);
47
+ const pm = getPM(registry);
48
+ // Agents directs du PM
49
+ const directReports = lanes.filter(l =>
50
+ l.reports_to === pm?.name || l.reports_to === pm?.id || l.reports_to === 'PM'
51
+ );
52
+ // Si aucun reports_to defini, afficher tous les agents (max 5)
53
+ const agents = directReports.length > 0 ? directReports : lanes;
54
+
55
+ for (const agent of agents.slice(0, 5)) {
56
+ const { checkAgentAlive } = require_team();
57
+ const alive = checkAgentAlive(agent.pid);
58
+ const dot = alive ? style.green('\u25CF') : agent.session_id ? style.yellow('\u25CF') : style.dim('\u25CB');
59
+ const statusText = alive ? 'actif' : agent.session_id ? 'en veille' : '';
60
+ items.push({
61
+ label: `${agent.name} ${dot} ${style.dim(statusText)}`,
62
+ value: `__agent__:${agent.name}`,
63
+ description: agent.role || '',
64
+ });
65
+ }
66
+ if (lanes.length > 0) {
67
+ items.push({ label: style.dim('Voir tous \u2192'), value: '__all_agents__', description: '' });
68
+ }
69
+ // Separateur visuel
70
+ items.push({ label: style.dim('\u2500'.repeat(35)), value: '__sep__', description: '' });
71
+ }
72
+ }
73
+
74
+ // --- Zone Categories ---
75
+ items.push(
76
+ { label: 'Projet', value: '__cat_projet__', description: 'Status, init, diagnostic' },
77
+ { label: 'Equipe', value: '__cat_equipe__', description: 'Agents, profils' },
78
+ { label: 'Travail', value: '__cat_travail__', description: 'Missions, OdMs, audits' },
79
+ { label: 'Memoire', value: '__cat_memoire__', description: 'Notes, CR, contexte agent' },
80
+ { label: 'Infra', value: '__cat_infra__', description: 'Connexions LLM, HCM' },
81
+ );
82
+
83
+ return items;
84
+ }
85
+
86
+ // Delegates to pid-checker for centralized PID logic
87
+ import { isPidAlive } from '../lib/kairos/pid-checker.js';
88
+ function require_team() {
89
+ return { checkAgentAlive: isPidAlive };
90
+ }
91
+
92
+ // ---------------------------------------------------------------------------
93
+ // Sous-menus categories
94
+ // ---------------------------------------------------------------------------
95
+
96
+ const CAT_PROJET = [
97
+ { label: 'status', value: 'status', description: 'Etat du projet + alertes' },
98
+ { label: 'init', value: 'init', description: 'Scaffold structure projet' },
99
+ { label: 'doctor', value: 'doctor', description: 'Diagnostic complet' },
100
+ BACK_ITEM, HOME_ITEM,
101
+ ];
102
+
103
+ const CAT_EQUIPE = [
104
+ { label: 'Organigramme', value: 'team:list', description: 'Vue hierarchique de l\'equipe' },
105
+ { label: 'Agents', value: 'team:panneau', description: 'Espace de gestion des agents' },
106
+ { label: 'Ajouter', value: 'team:add', description: 'Ajouter un agent' },
107
+ { label: 'Profils', value: 'kars', description: 'Parcourir les profils disponibles' },
108
+ BACK_ITEM, HOME_ITEM,
109
+ ];
110
+
111
+ const CAT_TRAVAIL = [
112
+ { label: 'Orchestration', value: 'orch', description: 'Dashboard dispatches & agents' },
113
+ { label: 'Inbox', value: 'inbox', description: 'Documents humains → MC' },
114
+ { label: 'Missions', value: 'mission', description: 'Contrats de fonctionnalite' },
115
+ { label: 'OdMs', value: 'odm', description: 'Ordres de mission' },
116
+ { label: 'Audit', value: 'audit', description: 'Audit technique' },
117
+ BACK_ITEM, HOME_ITEM,
118
+ ];
119
+
120
+ const CAT_MEMOIRE = [
121
+ { label: 'Notes', value: 'notes', description: 'Consulter notes & CR des agents' },
122
+ { label: 'Contexte', value: 'notewriter', description: 'Gestion du contexte agent' },
123
+ BACK_ITEM, HOME_ITEM,
124
+ ];
125
+
126
+ const CAT_INFRA = [
127
+ { label: 'Services', value: 'services', description: 'Connexions LLM' },
128
+ { label: 'HCM', value: 'hcm', description: 'Synchronisation & recherche' },
129
+ { label: 'Auth', value: 'auth', description: 'Connexion HCM' },
130
+ { label: 'Kairos', value: 'kairos', description: 'Hooks & contexte agent' },
131
+ BACK_ITEM, HOME_ITEM,
132
+ ];
133
+
134
+ export const CATEGORY_MAP = {
135
+ '__cat_projet__': { items: CAT_PROJET, title: 'nemesis \u203A Projet' },
136
+ '__cat_equipe__': { items: CAT_EQUIPE, title: 'nemesis \u203A Equipe' },
137
+ '__cat_travail__': { items: CAT_TRAVAIL, title: 'nemesis \u203A Travail' },
138
+ '__cat_memoire__': { items: CAT_MEMOIRE, title: 'nemesis \u203A Memoire' },
139
+ '__cat_infra__': { items: CAT_INFRA, title: 'nemesis \u203A Infra' },
140
+ };
141
+
142
+ // Export sub-menus for tests
143
+ export { CAT_PROJET, CAT_EQUIPE, CAT_TRAVAIL, CAT_MEMOIRE, CAT_INFRA };
144
+
145
+ // Export gate functions for tests
146
+ export { SKIP_HCM_COMMANDS, READ_ONLY_COMMANDS, warnIfHcmDown, blockIfHcmDown };
147
+
148
+ // ---------------------------------------------------------------------------
149
+ // Ecran rapide agent
150
+ // ---------------------------------------------------------------------------
151
+
152
+ async function handleQuickAgent(agentName, flags, config) {
153
+ const { teamInspect } = await import('../lib/core/team.js');
154
+ const result = teamInspect(agentName, config.cwd);
155
+ const agent = result.agent;
156
+
157
+ const alive = isPidAlive(agent.pid);
158
+ const dot = alive ? style.green('\u25CF') : agent.session_id ? style.yellow('\u25CF') : style.dim('\u25CB');
159
+ const statusText = alive ? 'actif' : agent.session_id ? 'en veille' : 'pas de session';
160
+
161
+ const choice = await interactiveMenu([
162
+ { label: 'Rejoindre la session', value: 'join', description: '' },
163
+ { label: 'Envoyer un message', value: 'inject', description: '' },
164
+ { label: 'Panneau complet', value: 'panneau', description: '' },
165
+ BACK_ITEM,
166
+ ], { title: `${agentName} ${dot} ${statusText}` });
167
+
168
+ if (!choice || choice === '__back__') return '__back__';
169
+ if (choice === '__home__') return '__home__';
170
+
171
+ const teamMod = await import('./commands/team.js');
172
+ switch (choice) {
173
+ case 'join': return teamMod.actionJoin(agent, [], flags, config);
174
+ case 'inject': return teamMod.actionInject(agent);
175
+ case 'panneau': return teamMod.handler({ args: ['panneau', agentName], flags, config });
176
+ }
177
+ }
178
+
179
+ // ---------------------------------------------------------------------------
180
+ // printHelp — mode direct (inchange)
181
+ // ---------------------------------------------------------------------------
182
+
183
+ function printHelp() {
184
+ const header = renderBrandHeader({ version: VERSION });
185
+ for (const line of header) console.log(line);
186
+
187
+ const cmdLines = [
188
+ `${style.bold('init')} ${style.gray('Scaffold structure projet')}`,
189
+ `${style.bold('team')} ${style.gray('Gestion equipe agents (add, list, panneau, remove)')}`,
190
+ `${style.bold('project')} ${style.gray('Vue projet (status, init)')}`,
191
+ `${style.bold('mission')} ${style.gray('Mission Contracts (init, list, inspect)')}`,
192
+ `${style.bold('odm')} ${style.gray('Ordres de Mission (init, list, inspect)')}`,
193
+ `${style.bold('status')} ${style.gray('Raccourci project status + alertes')}`,
194
+ `${style.bold('hcm')} ${style.gray('Synchronisation HCM (sync, status, search)')}`,
195
+ `${style.bold('kars')} ${style.gray('Recherche profils Kairos (search)')}`,
196
+ `${style.bold('audit')} ${style.gray('Audit technique (run, list, reports)')}`,
197
+ `${style.bold('services')} ${style.gray('Connexions LLM (connexions add/list/test/remove)')}`,
198
+ `${style.bold('notes')} ${style.gray('Notes & CR — consultation memoire')}`,
199
+ `${style.bold('notewriter')} ${style.gray('Memoire agent — notes, CR, replay')}`,
200
+ `${style.bold('kairos')} ${style.gray('Hooks & contexte agent')}`,
201
+ `${style.bold('auth')} ${style.gray('Connexion HCM (login, status, logout)')}`,
202
+ `${style.bold('inbox')} ${style.gray('Pipeline documents humains (add, list, process)')}`,
203
+ `${style.bold('orch')} ${style.gray('Orchestrateur daemon (dispatch, routing, supervision)')}`,
204
+ `${style.bold('run')} ${style.gray('Reprendre session agent (run agent:<NAME>)')}`,
205
+ `${style.bold('doctor')} ${style.gray('Diagnostic complet')}`,
206
+ ];
207
+
208
+ const flagLines = [
209
+ `${style.bold('--format <fmt>')} ${style.gray('Format de sortie: json, text, yaml (default: text)')}`,
210
+ `${style.bold('--no-color')} ${style.gray('Desactiver les couleurs ANSI')}`,
211
+ `${style.bold('--verbose')} ${style.gray('Sortie detaillee')}`,
212
+ `${style.bold('-h, --help')} ${style.gray('Aide')}`,
213
+ `${style.bold('-V, --version')} ${style.gray('Version')}`,
214
+ ];
215
+
216
+ console.log(titledBox('Commands', cmdLines, { border: style.arkaRed }));
217
+ console.log('');
218
+ console.log(titledBox('Flags', flagLines, { border: style.arkaRed }));
219
+ console.log('');
220
+ }
221
+
222
+ // ---------------------------------------------------------------------------
223
+ // parseGlobalFlags
224
+ // ---------------------------------------------------------------------------
225
+
226
+ function parseGlobalFlags(args) {
227
+ const flags = {
228
+ format: 'text',
229
+ noColor: false,
230
+ verbose: false,
231
+ help: false,
232
+ version: false,
233
+ };
234
+ const remaining = [];
235
+
236
+ for (let i = 0; i < args.length; i++) {
237
+ const arg = args[i];
238
+ if (arg === '--format' && i + 1 < args.length) {
239
+ flags.format = args[++i];
240
+ } else if (arg === '--no-color') {
241
+ flags.noColor = true;
242
+ } else if (arg === '--verbose') {
243
+ flags.verbose = true;
244
+ } else if (arg === '-h' || arg === '--help') {
245
+ flags.help = true;
246
+ } else if (arg === '-V' || arg === '--version') {
247
+ flags.version = true;
248
+ } else {
249
+ remaining.push(arg);
250
+ }
251
+ }
252
+
253
+ return { flags, remaining };
254
+ }
255
+
256
+ // ---------------------------------------------------------------------------
257
+ // HCM gate
258
+ // ---------------------------------------------------------------------------
259
+
260
+ async function checkHcm(config) {
261
+ const project = detectProject(config.cwd);
262
+ const client = createHcmClient({ timeout: 3000 });
263
+ const ping = await client.ping();
264
+
265
+ if (project) {
266
+ storeHcmStatus(project.hcm_dir, project.id, {
267
+ connected: ping.ok,
268
+ url: client.baseUrl,
269
+ latency: ping.latency,
270
+ checked_at: new Date().toISOString(),
271
+ ...(ping.error ? { error: ping.error } : {}),
272
+ });
273
+ }
274
+
275
+ return ping;
276
+ }
277
+
278
+ function blockIfHcmDown(ping) {
279
+ if (ping.ok) return;
280
+
281
+ console.log('');
282
+ console.log(` ${style.red('\u2717')} ${style.bold('HCM non connecte')}`);
283
+ console.log('');
284
+ console.log(` La CLI necessite une connexion au HCM pour fonctionner.`);
285
+ if (ping.error) {
286
+ console.log(` ${style.dim('\u2192')} ${style.dim(ping.error)}`);
287
+ }
288
+ console.log('');
289
+ console.log(` ${style.bold('Connectez-vous avec :')} nemesis auth login`);
290
+ console.log(` ${style.bold('Dashboard HCM :')} ${style.nemesisAccent('https://hcm.arkalabs.app')}`);
291
+ console.log('');
292
+ process.exit(1);
293
+ }
294
+
295
+ const SKIP_HCM_COMMANDS = new Set(['auth', 'init', 'notewriter', 'notes', 'kairos', 'orch', 'inbox']);
296
+
297
+ const READ_ONLY_COMMANDS = new Set(['status', 'project', 'doctor', 'audit', 'team', 'odm', 'mission']);
298
+
299
+ function warnIfHcmDown(ping) {
300
+ if (ping.ok) return;
301
+ console.log('');
302
+ console.log(` ${style.yellow('\u26A0')} ${style.bold('HCM non connecte')} \u2014 mode lecture seule`);
303
+ console.log(` ${style.dim('\u2192')} Reconnectez-vous : ${style.bold('nemesis auth login')}`);
304
+ console.log('');
305
+ }
306
+
307
+ // ---------------------------------------------------------------------------
308
+ // Boucle principale
309
+ // ---------------------------------------------------------------------------
310
+
311
+ export async function run(argv) {
312
+ const { flags, remaining } = parseGlobalFlags(argv);
313
+
314
+ if (flags.noColor) {
315
+ process.env.NO_COLOR = '1';
316
+ }
317
+
318
+ if (flags.version) {
319
+ console.log(`nemesis v${VERSION}`);
320
+ process.exit(0);
321
+ }
322
+
323
+ const commandName = remaining[0];
324
+ const commandArgs = remaining.slice(1);
325
+
326
+ // --help flag without command -> static help
327
+ if (flags.help && !commandName) {
328
+ printHelp();
329
+ process.exit(0);
330
+ }
331
+
332
+ // No command -> interactive menu loop (screen-based)
333
+ if (!commandName) {
334
+ const config = await loadConfig();
335
+
336
+ // HCM gate (interactive = warn only)
337
+ const ping = await checkHcm(config);
338
+ warnIfHcmDown(ping);
339
+
340
+ while (true) {
341
+ clearScreen();
342
+ const header = renderBrandHeader({ version: VERSION });
343
+ for (const line of header) console.log(line);
344
+
345
+ const menuItems = buildMainMenu(config);
346
+ const selected = await interactiveMenu(menuItems, {
347
+ title: 'Que souhaitez-vous faire ?',
348
+ });
349
+
350
+ if (!selected) {
351
+ clearScreen();
352
+ process.exit(0);
353
+ }
354
+
355
+ if (selected === '__sep__' || selected === '__coming_soon__') continue;
356
+
357
+ // Acces rapide agent
358
+ if (selected.startsWith('__agent__:')) {
359
+ clearScreen();
360
+ const agentName = selected.replace('__agent__:', '');
361
+ process.stdout.write(renderBrandBanner({ version: VERSION, section: agentName }));
362
+ try {
363
+ const result = await handleQuickAgent(agentName, flags, config);
364
+ if (result !== '__back__' && result !== '__home__') await waitForKey();
365
+ } catch (err) {
366
+ if (flags.verbose) console.error(err);
367
+ else console.error(` ${style.red('Erreur')}: ${err.message}\n`);
368
+ await waitForKey();
369
+ }
370
+ continue;
371
+ }
372
+
373
+ // Voir tous -> Equipe panneau
374
+ if (selected === '__all_agents__') {
375
+ clearScreen();
376
+ process.stdout.write(renderBrandBanner({ version: VERSION, section: 'Equipe' }));
377
+ const mod = await COMMANDS.team();
378
+ const result = await mod.handler({ args: ['panneau'], flags, config });
379
+ if (result !== '__back__' && result !== '__home__') await waitForKey();
380
+ continue;
381
+ }
382
+
383
+ // Categorie -> sous-menu
384
+ const cat = CATEGORY_MAP[selected];
385
+ if (cat) {
386
+ let stayInCategory = true;
387
+ while (stayInCategory) {
388
+ clearScreen();
389
+ process.stdout.write(renderBrandBanner({ version: VERSION, section: cat.title.split(' \u203A ')[1] }));
390
+
391
+ const sub = await interactiveMenu(cat.items, { title: cat.title });
392
+ if (!sub || sub === '__back__') { break; }
393
+ if (sub === '__home__') { break; }
394
+ if (sub === '__coming_soon__') continue;
395
+
396
+ // Dispatch commande
397
+ clearScreen();
398
+ let cmdName = sub;
399
+ let cmdArgs = [];
400
+ if (sub.includes(':')) {
401
+ const [cmd, ...rest] = sub.split(':');
402
+ cmdName = cmd;
403
+ cmdArgs = rest;
404
+ }
405
+
406
+ process.stdout.write(renderBrandBanner({ version: VERSION, section: cmdName }));
407
+ const loader = COMMANDS[cmdName];
408
+ if (!loader) continue;
409
+
410
+ try {
411
+ const mod = await loader();
412
+ const result = await mod.handler({ args: cmdArgs, flags, config });
413
+ if (result === '__home__') { stayInCategory = false; break; }
414
+ if (result !== '__back__') await waitForKey();
415
+ } catch (err) {
416
+ if (flags.verbose) console.error(err);
417
+ else console.error(` ${style.red('Erreur')}: ${err.message}\n`);
418
+ await waitForKey();
419
+ }
420
+ }
421
+ continue;
422
+ }
423
+ }
424
+ }
425
+
426
+ // Direct command path
427
+ const loader = COMMANDS[commandName];
428
+ if (!loader) {
429
+ console.error(`Commande inconnue : ${commandName}`);
430
+ console.error(`Tapez "nemesis --help" pour voir les commandes disponibles.`);
431
+ process.exit(1);
432
+ }
433
+
434
+ const config = await loadConfig();
435
+
436
+ // HCM gate: skip → no ping, read-only → warn, others → block
437
+ if (!SKIP_HCM_COMMANDS.has(commandName)) {
438
+ const ping = await checkHcm(config);
439
+ if (READ_ONLY_COMMANDS.has(commandName)) {
440
+ warnIfHcmDown(ping);
441
+ } else {
442
+ blockIfHcmDown(ping);
443
+ }
444
+ }
445
+
446
+ try {
447
+ const mod = await loader();
448
+ await mod.handler({ args: commandArgs, flags, config });
449
+ } catch (err) {
450
+ if (flags.verbose) {
451
+ console.error(err);
452
+ } else {
453
+ console.error(`Erreur: ${err.message}`);
454
+ }
455
+ process.exit(1);
456
+ }
457
+ }
@@ -0,0 +1,119 @@
1
+ import { createInterface } from 'node:readline';
2
+ import { detectProject } from '../../lib/core/project.js';
3
+ import { askConfirm } from '../../lib/ui/prompt.js';
4
+ import { interactiveMenu } from '../../lib/ui/menu.js';
5
+ import { style } from '../../lib/ui/colors.js';
6
+
7
+ const BACK_ITEM = { label: style.dim('\u2190 Retour'), value: '__back__', description: '' };
8
+ const HOME_ITEM = { label: style.dim('\u2302 Accueil'), value: '__home__', description: '' };
9
+
10
+ /**
11
+ * Detect project or propose interactive init. Returns project or null.
12
+ */
13
+ export async function ensureProject(config, flags) {
14
+ let project = detectProject(config.cwd);
15
+ if (project) return project;
16
+
17
+ console.log(`\n ${style.yellow('\u26A0')} Aucun projet detecte.`);
18
+ const doInit = await askConfirm('Initialiser un projet maintenant ?', true);
19
+ if (doInit) {
20
+ const initMod = await import('./init.js');
21
+ await initMod.handler({ args: [], flags, config });
22
+ project = detectProject(config.cwd);
23
+ }
24
+ if (!project) {
25
+ console.log(` ${style.dim('Utilisez')} nemesis init ${style.dim('pour creer un projet.')}\n`);
26
+ }
27
+ return project || null;
28
+ }
29
+
30
+ /**
31
+ * Pick an item from a list via interactive menu.
32
+ * labelFn is used for display — description is NOT auto-populated from item.title
33
+ * to avoid duplication when labelFn already includes it.
34
+ * Returns selected value or null if cancelled.
35
+ */
36
+ export async function pickFromList(items, opts = {}) {
37
+ const {
38
+ title = 'Choisir',
39
+ labelFn = (item) => item.id || item.label,
40
+ maxVisible = 0,
41
+ } = opts;
42
+
43
+ if (items.length === 0) return null;
44
+
45
+ const menuItems = items.map(item => ({
46
+ label: labelFn(item),
47
+ value: item.id || item.value,
48
+ }));
49
+
50
+ return interactiveMenu(menuItems, { title, maxVisible });
51
+ }
52
+
53
+ /**
54
+ * Auto-generate the next sequential ID.
55
+ * @param {string} prefix - e.g. "MCT-NEMESIS" or "ODM-NEMESIS"
56
+ * @param {string[]} existingIds - list of existing IDs
57
+ * @returns {string} next ID like "MCT-NEMESIS-003"
58
+ */
59
+ export function nextId(prefix, existingIds) {
60
+ let max = 0;
61
+ for (const id of existingIds) {
62
+ const match = id.match(/-(\d+)$/);
63
+ if (match) {
64
+ const num = parseInt(match[1], 10);
65
+ if (num > max) max = num;
66
+ }
67
+ }
68
+ return `${prefix}-${String(max + 1).padStart(3, '0')}`;
69
+ }
70
+
71
+ /**
72
+ * Clear the terminal screen and move cursor to top-left.
73
+ */
74
+ export function clearScreen() {
75
+ process.stdout.write('\x1b[2J\x1b[H');
76
+ }
77
+
78
+ /**
79
+ * Wait for the user to press Enter before returning.
80
+ * Used after command output so user can read it before screen clears.
81
+ */
82
+ export async function waitForKey() {
83
+ if (!process.stdin.isTTY) return;
84
+ console.log(`\n ${style.dim('Appuyez sur Entree pour revenir au menu...')}`);
85
+ const rl = createInterface({ input: process.stdin, output: process.stdout });
86
+ return new Promise(resolve => {
87
+ rl.once('line', () => { rl.close(); resolve(); });
88
+ });
89
+ }
90
+
91
+ /**
92
+ * Extract a flag value from args array: --flag <value>
93
+ */
94
+ export function getFlag(args, flag) {
95
+ const idx = args.indexOf(flag);
96
+ return idx !== -1 && idx + 1 < args.length ? args[idx + 1] : null;
97
+ }
98
+
99
+ /**
100
+ * Sanitize agent name: strip existing Agent_ prefix, replace spaces/special chars, re-prefix.
101
+ * "Agent stratege" → "Agent_Stratege", "Agent_Agent_Foo" → "Agent_Foo"
102
+ */
103
+ export function sanitizeName(raw) {
104
+ let name = raw.trim();
105
+ // Strip existing Agent_ or Agent prefix (with underscore or space)
106
+ name = name.replace(/^Agent[_ ]?/i, '');
107
+ // Replace spaces and special chars with underscores, collapse multiples
108
+ name = name.replace(/[^a-zA-Z0-9_-]/g, '_').replace(/_+/g, '_').replace(/^[_-]+|[_-]+$/g, '');
109
+ if (!name) return 'Agent_Unknown';
110
+ // Capitalize first letter
111
+ name = name.charAt(0).toUpperCase() + name.slice(1);
112
+ return `Agent_${name}`;
113
+ }
114
+
115
+ /**
116
+ * Back menu item — add as last option in submenus.
117
+ * Home menu item — returns to main CLI menu from any depth.
118
+ */
119
+ export { BACK_ITEM, HOME_ITEM };