@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
@@ -0,0 +1,705 @@
1
+ import { interactiveMenu } from '../../lib/ui/menu.js';
2
+ import { askText, askConfirm } from '../../lib/ui/prompt.js';
3
+ import { createSpinner } from '../../lib/ui/spinner.js';
4
+ import { renderTable } from '../../lib/ui/table.js';
5
+ import { titledBox } from '../../lib/ui/box.js';
6
+ import { style } from '../../lib/ui/colors.js';
7
+ import { writeFileSync, readFileSync, unlinkSync, existsSync } from 'node:fs';
8
+ import { join } from 'node:path';
9
+ import { BACK_ITEM, HOME_ITEM, pickFromList, ensureProject } from './_helpers.js';
10
+ import {
11
+ PROVIDERS, addConnexion, listConnexions,
12
+ removeConnexion, updateConnexionStatus,
13
+ } from '../../lib/core/connexions.js';
14
+ import { testConnection, liveTestConnection } from '../../lib/sync/llm-client.js';
15
+ import { resolveNotesDir, resolveCrDir, ensureDir } from '../../lib/core/notewriter/paths.js';
16
+ import {
17
+ SERVICE_IDS, SERVICE_LABELS, SERVICE_DEFAULTS,
18
+ readServices, writeServices, assignService,
19
+ } from '../../lib/core/services.js';
20
+ import {
21
+ ALL_HOOKS, getHookStatus, installHooks, removeHooks,
22
+ activateSingleHook, deactivateSingleHook,
23
+ } from '../../lib/kairos/hook-installer.js';
24
+
25
+ const HELP = `
26
+ nemesis services — Gestion des connexions LLM et services
27
+
28
+ Usage: nemesis services [subcommand]
29
+
30
+ Subcommands:
31
+ connexions Gerer les connexions LLM
32
+ connexions add Ajouter une connexion
33
+ connexions list Lister les connexions
34
+ connexions test Tester une connexion (health check)
35
+ connexions live Tester en simulation reelle (appel LLM)
36
+ connexions remove Supprimer une connexion
37
+ hooks Gerer les hooks Claude Code (menu interactif)
38
+ hooks status Etat des hooks
39
+ hooks setup Activer tous les hooks
40
+ hooks reset Desactiver tous les hooks
41
+ config Affecter connexions aux services (ODM-CLI-012)
42
+ status Vue d'ensemble des services (ODM-CLI-012)
43
+ -h, --help Aide
44
+ `;
45
+
46
+ export async function handler({ args, flags, config }) {
47
+ if (flags.help || args.includes('--help') || args.includes('-h')) {
48
+ console.log(HELP);
49
+ return;
50
+ }
51
+
52
+ let sub = args[0];
53
+ let subArgs = args.slice(1);
54
+
55
+ if (!sub) {
56
+ sub = await interactiveMenu([
57
+ { label: 'connexions', value: 'connexions', description: 'Gerer les connexions LLM' },
58
+ { label: 'hooks', value: 'hooks', description: 'Gerer les hooks Claude Code' },
59
+ { label: 'config', value: 'config', description: 'Affecter aux services' },
60
+ { label: 'status', value: 'status', description: 'Etat des services' },
61
+ BACK_ITEM,
62
+ HOME_ITEM,
63
+ ], { title: 'nemesis services' });
64
+ if (!sub || sub === '__back__') return '__back__';
65
+ if (sub === '__home__') return '__home__';
66
+ subArgs = [];
67
+ }
68
+
69
+ switch (sub) {
70
+ case 'connexions': return handleConnexions(subArgs, flags, config);
71
+ case 'hooks': return handleHooks(subArgs, flags, config);
72
+ case 'config': return handleConfig(subArgs, flags, config);
73
+ case 'status': return handleStatus(subArgs, flags, config);
74
+ default:
75
+ console.log(` Sous-commande inconnue : ${sub}`);
76
+ console.log(HELP);
77
+ }
78
+ }
79
+
80
+ async function handleConnexions(args, flags, config) {
81
+ let sub = args[0];
82
+ let subArgs = args.slice(1);
83
+
84
+ if (!sub) {
85
+ sub = await interactiveMenu([
86
+ { label: 'add', value: 'add', description: 'Ajouter une connexion' },
87
+ { label: 'list', value: 'list', description: 'Lister les connexions' },
88
+ { label: 'test', value: 'test', description: 'Tester une connexion (health check)' },
89
+ { label: 'live', value: 'live', description: 'Test reel — appel LLM' },
90
+ { label: 'remove', value: 'remove', description: 'Supprimer une connexion' },
91
+ BACK_ITEM,
92
+ HOME_ITEM,
93
+ ], { title: 'Connexions LLM' });
94
+ if (!sub || sub === '__back__') return '__back__';
95
+ if (sub === '__home__') return '__home__';
96
+ subArgs = [];
97
+ }
98
+
99
+ switch (sub) {
100
+ case 'add': return handleConnexionsAdd(subArgs, flags, config);
101
+ case 'list': return handleConnexionsList(flags, config);
102
+ case 'test': return handleConnexionsTest(subArgs, flags, config);
103
+ case 'live': return handleConnexionsLive(subArgs, flags, config);
104
+ case 'remove': return handleConnexionsRemove(subArgs, flags, config);
105
+ }
106
+ }
107
+
108
+ // ── connexions add ──────────────────────────────────────────────────
109
+
110
+ async function handleConnexionsAdd(_args, _flags, _config) {
111
+ console.log('');
112
+
113
+ // 1. Nom
114
+ const name = await askText('Nom de la connexion');
115
+ if (!name) return;
116
+
117
+ // 2. Provider
118
+ const providerItems = PROVIDERS.map(p => ({
119
+ label: p.label, value: p.id, description: '',
120
+ }));
121
+ const provider = await interactiveMenu(providerItems, { title: 'Provider' });
122
+ if (!provider) return;
123
+ const providerDef = PROVIDERS.find(p => p.id === provider);
124
+
125
+ // 3. Type d'authentification
126
+ let authType;
127
+ if (providerDef.authTypes.length > 1) {
128
+ const authItems = providerDef.authTypes.map(t => ({
129
+ label: t === 'api_key' ? 'API Key' : t === 'token' ? 'Token' : 'Aucune',
130
+ value: t, description: '',
131
+ }));
132
+ authType = await interactiveMenu(authItems, { title: 'Type d\'authentification' });
133
+ if (!authType) return;
134
+ } else {
135
+ authType = providerDef.authTypes[0];
136
+ }
137
+
138
+ // 4. Modele
139
+ const model = await askText('Modele (laisser vide pour defaut)', '');
140
+
141
+ // 5. Credential
142
+ let credential = null;
143
+ if (authType !== 'none') {
144
+ credential = await askText(authType === 'api_key' ? 'Cle API' : 'Token');
145
+ if (!credential) return;
146
+ }
147
+
148
+ // 6. Endpoint
149
+ let endpoint = null;
150
+ if (provider === 'custom') {
151
+ endpoint = await askText('Endpoint URL');
152
+ if (!endpoint) return;
153
+ } else if (provider === 'ollama') {
154
+ endpoint = await askText('URL Ollama', 'http://localhost:11434');
155
+ }
156
+
157
+ // 7. CLI args
158
+ let cliArgs = null;
159
+ if (provider === 'cli_tools') {
160
+ cliArgs = await askText('Arguments CLI (optionnel)', '');
161
+ }
162
+
163
+ // 8. Creer + test auto
164
+ const spinner = createSpinner('Test de connexion...');
165
+ spinner.start();
166
+ const connexion = addConnexion({
167
+ name, provider, authType, model: model || null,
168
+ credential, endpoint, cliArgs: cliArgs || null,
169
+ });
170
+ const testResult = await testConnection(connexion);
171
+ updateConnexionStatus(connexion.id, testResult.ok ? 'connected' : 'error', testResult.latency);
172
+ if (testResult.ok) {
173
+ spinner.stop(`Connexion etablie \u2014 latence ${testResult.latency}ms`);
174
+ } else {
175
+ spinner.fail(`Echec connexion : ${testResult.error}`);
176
+ }
177
+
178
+ // 9. Confirmation
179
+ console.log(` ${style.green('\u2713')} Connexion "${name}" creee`);
180
+ console.log(` ID : ${style.dim(connexion.id)}`);
181
+ console.log('');
182
+ }
183
+
184
+ // ── connexions list ─────────────────────────────────────────────────
185
+
186
+ async function handleConnexionsList(_flags, _config) {
187
+ const connexions = listConnexions();
188
+ if (connexions.length === 0) {
189
+ console.log(`\n ${style.dim('Aucune connexion configuree.')}`);
190
+ console.log(` ${style.dim('Utilisez')} nemesis services connexions add\n`);
191
+ return;
192
+ }
193
+ const rows = connexions.map(c => ({
194
+ nom: c.name,
195
+ provider: c.provider,
196
+ statut: c.status === 'connected'
197
+ ? `${style.green('\u25CF')} actif`
198
+ : c.status === 'error'
199
+ ? `${style.red('\u25CF')} erreur`
200
+ : `${style.dim('\u25CB')} non teste`,
201
+ latence: c.latency ? `${c.latency}ms` : '\u2014',
202
+ }));
203
+ console.log('');
204
+ console.log(titledBox('Connexions LLM', [
205
+ renderTable(
206
+ [{ key: 'nom', label: 'Nom' }, { key: 'provider', label: 'Provider' },
207
+ { key: 'statut', label: 'Statut' }, { key: 'latence', label: 'Latence' }],
208
+ rows,
209
+ ),
210
+ ], { border: style.nemesisAccent }));
211
+ console.log('');
212
+ }
213
+
214
+ // ── connexions test ─────────────────────────────────────────────────
215
+
216
+ async function handleConnexionsTest(args, _flags, _config) {
217
+ const connexions = listConnexions();
218
+ if (connexions.length === 0) {
219
+ console.log(`\n ${style.dim('Aucune connexion configuree.')}\n`);
220
+ return;
221
+ }
222
+
223
+ let cx = args[0] ? connexions.find(c => c.id === args[0] || c.name === args[0]) : null;
224
+ if (!cx) {
225
+ const selected = await pickFromList(
226
+ connexions.map(c => ({ id: c.id, title: c.provider, ...c })),
227
+ {
228
+ title: 'Tester quelle connexion ?',
229
+ labelFn: (c) => `${c.name} (${c.provider})`,
230
+ },
231
+ );
232
+ if (!selected) return;
233
+ cx = connexions.find(c => c.id === selected);
234
+ }
235
+ if (!cx) return;
236
+
237
+ const spinner = createSpinner(`Test de connexion "${cx.name}"...`);
238
+ spinner.start();
239
+ const result = await testConnection(cx);
240
+ updateConnexionStatus(cx.id, result.ok ? 'connected' : 'error', result.latency);
241
+ if (result.ok) {
242
+ spinner.stop('Test reussi');
243
+ console.log(` ${style.green('\u2713')} Auth valide`);
244
+ if (cx.model) console.log(` ${style.green('\u2713')} Modele ${cx.model} accessible`);
245
+ console.log(` ${style.green('\u2713')} Latence : ${result.latency}ms`);
246
+ } else {
247
+ spinner.fail('Test echoue');
248
+ console.log(` ${style.red('\u2717')} ${result.error}`);
249
+ }
250
+ console.log('');
251
+ }
252
+
253
+ // ── connexions live (test reel LLM) ─────────────────────────────────
254
+
255
+ async function handleConnexionsLive(args, flags, config) {
256
+ const connexions = listConnexions();
257
+ if (connexions.length === 0) {
258
+ console.log(`\n ${style.dim('Aucune connexion configuree.')}\n`);
259
+ return;
260
+ }
261
+
262
+ let cx = args[0] ? connexions.find(c => c.id === args[0] || c.name === args[0]) : null;
263
+ if (!cx) {
264
+ const selected = await pickFromList(
265
+ connexions.map(c => ({ id: c.id, title: c.provider, ...c })),
266
+ {
267
+ title: 'Test reel — quelle connexion ?',
268
+ labelFn: (c) => `${c.name} (${c.provider}${c.model ? ' \u2014 ' + c.model : ''})`,
269
+ },
270
+ );
271
+ if (!selected) return;
272
+ cx = connexions.find(c => c.id === selected);
273
+ }
274
+ if (!cx) return;
275
+
276
+ // --- Step 1: Appel LLM reel ---
277
+ const spinner = createSpinner(`Appel LLM reel vers "${cx.name}" (${cx.provider})...`);
278
+ spinner.start();
279
+ const result = await liveTestConnection(cx);
280
+ updateConnexionStatus(cx.id, result.ok ? 'connected' : 'error', result.latency);
281
+
282
+ if (!result.ok) {
283
+ spinner.fail('Echec appel LLM');
284
+ console.log('');
285
+ console.log(titledBox('Test reel — Echec', [
286
+ `Connexion : ${cx.name}`,
287
+ `Provider : ${cx.provider}`,
288
+ `Modele : ${cx.model || '(defaut)'}`,
289
+ `Latence : ${result.latency}ms`,
290
+ '',
291
+ `${style.red('\u2717')} ${result.error}`,
292
+ ], { border: style.red }));
293
+ console.log('');
294
+ return;
295
+ }
296
+ spinner.stop('Appel LLM reussi');
297
+
298
+ // --- Step 2: Validation ecriture notes/cr ---
299
+ const project = await ensureProject(config, flags).catch(() => null);
300
+ const testAgent = '_live-test_';
301
+ let writeOk = false;
302
+ let noteWriteOk = false;
303
+ let crWriteOk = false;
304
+ let cleanupOk = false;
305
+
306
+ if (project) {
307
+ const root = project.root || config.cwd;
308
+ const notesDir = resolveNotesDir(root, testAgent);
309
+ const crDir = resolveCrDir(root, testAgent);
310
+ const testNoteFile = join(notesDir, '_live-test-note.json');
311
+ const testCrFile = join(crDir, '_live-test-cr.json');
312
+ const testPayload = JSON.stringify({ _test: true, ts: new Date().toISOString() });
313
+
314
+ try {
315
+ // Ecriture note test
316
+ ensureDir(notesDir);
317
+ writeFileSync(testNoteFile, testPayload, 'utf-8');
318
+ const readBack = readFileSync(testNoteFile, 'utf-8');
319
+ noteWriteOk = readBack === testPayload;
320
+ unlinkSync(testNoteFile);
321
+
322
+ // Ecriture CR test
323
+ ensureDir(crDir);
324
+ writeFileSync(testCrFile, testPayload, 'utf-8');
325
+ const readBackCr = readFileSync(testCrFile, 'utf-8');
326
+ crWriteOk = readBackCr === testPayload;
327
+ unlinkSync(testCrFile);
328
+
329
+ // Nettoyage dossier agent test
330
+ const { resolveAgentDir } = await import('../../lib/core/notewriter/paths.js');
331
+ const agentDir = resolveAgentDir(root, testAgent);
332
+ const { rmSync } = await import('node:fs');
333
+ rmSync(agentDir, { recursive: true, force: true });
334
+
335
+ writeOk = noteWriteOk && crWriteOk;
336
+ cleanupOk = !existsSync(agentDir);
337
+ } catch {
338
+ writeOk = false;
339
+ }
340
+ }
341
+
342
+ // --- Affichage resultat complet ---
343
+ const lines = [
344
+ `Connexion : ${cx.name}`,
345
+ `Provider : ${cx.provider}`,
346
+ `Modele : ${result.model || cx.model || '(defaut)'}`,
347
+ `Latence : ${result.latency}ms`,
348
+ '',
349
+ style.bold('1. Reponse LLM'),
350
+ ` ${result.response || '(vide)'}`,
351
+ ` ${style.green('\u2713')} Generation texte OK`,
352
+ '',
353
+ style.bold('2. Ecriture fichiers'),
354
+ ];
355
+
356
+ if (!project) {
357
+ lines.push(` ${style.dim('\u25CB')} Aucun projet detecte — test ecriture ignore`);
358
+ } else {
359
+ lines.push(noteWriteOk
360
+ ? ` ${style.green('\u2713')} Ecriture note : creation + lecture + suppression OK`
361
+ : ` ${style.red('\u2717')} Ecriture note : echec`);
362
+ lines.push(crWriteOk
363
+ ? ` ${style.green('\u2713')} Ecriture CR : creation + lecture + suppression OK`
364
+ : ` ${style.red('\u2717')} Ecriture CR : echec`);
365
+ lines.push(cleanupOk
366
+ ? ` ${style.green('\u2713')} Nettoyage dossier test OK`
367
+ : ` ${style.dim('\u25CB')} Nettoyage partiel`);
368
+ }
369
+
370
+ lines.push('');
371
+ const allOk = result.ok && (!project || writeOk);
372
+ lines.push(allOk
373
+ ? `${style.green('\u2713')} Validation complete — connexion operationnelle`
374
+ : `${style.red('\u26A0')} Validation partielle — verifiez les erreurs ci-dessus`);
375
+
376
+ console.log('');
377
+ console.log(titledBox('Test reel — Resultat', lines, { border: allOk ? style.green : style.arkaRed }));
378
+ console.log('');
379
+ }
380
+
381
+ // ── connexions remove ───────────────────────────────────────────────
382
+
383
+ async function handleConnexionsRemove(args, flags, config) {
384
+ const connexions = listConnexions();
385
+ if (connexions.length === 0) {
386
+ console.log(`\n ${style.dim('Aucune connexion configuree.')}\n`);
387
+ return;
388
+ }
389
+
390
+ let cx = args[0] ? connexions.find(c => c.id === args[0] || c.name === args[0]) : null;
391
+ if (!cx) {
392
+ const selected = await pickFromList(
393
+ connexions.map(c => ({ id: c.id, title: c.provider, ...c })),
394
+ {
395
+ title: 'Supprimer quelle connexion ?',
396
+ labelFn: (c) => `${c.name} (${c.provider})`,
397
+ },
398
+ );
399
+ if (!selected) return;
400
+ cx = connexions.find(c => c.id === selected);
401
+ }
402
+ if (!cx) return;
403
+
404
+ const confirmed = await askConfirm(`Supprimer "${cx.name}" ?`, false);
405
+ if (!confirmed) { console.log(' Annule.\n'); return; }
406
+
407
+ removeConnexion(cx.id);
408
+
409
+ // Nettoyage references dans services.json
410
+ const project = await ensureProject(config, {}).catch(() => null);
411
+ if (project) {
412
+ const svcData = readServices(project.root || config.cwd);
413
+ let changed = false;
414
+ for (const [, svc] of Object.entries(svcData.services)) {
415
+ if (svc.connexion_id === cx.id) { svc.connexion_id = null; changed = true; }
416
+ if (svc.fallback) {
417
+ const before = svc.fallback.length;
418
+ svc.fallback = svc.fallback.filter(f => f.connexion_id !== cx.id);
419
+ if (svc.fallback.length !== before) changed = true;
420
+ }
421
+ }
422
+ if (changed) writeServices(project.root || config.cwd, svcData);
423
+ }
424
+
425
+ console.log(` ${style.green('\u2713')} Connexion "${cx.name}" supprimee`);
426
+ console.log('');
427
+ }
428
+
429
+ // ── hooks ────────────────────────────────────────────────────────────
430
+
431
+ async function handleHooks(args, flags, config) {
432
+ let sub = args[0];
433
+
434
+ if (!sub) {
435
+ sub = await interactiveMenu([
436
+ { label: 'status', value: 'status', description: 'Etat des hooks' },
437
+ { label: 'toggle', value: 'toggle', description: 'Activer/desactiver par hook' },
438
+ { label: 'setup', value: 'setup', description: 'Tout activer' },
439
+ { label: 'reset', value: 'reset', description: 'Tout desactiver' },
440
+ BACK_ITEM,
441
+ HOME_ITEM,
442
+ ], { title: 'Hooks Claude Code' });
443
+ if (!sub || sub === '__back__') return '__back__';
444
+ if (sub === '__home__') return '__home__';
445
+ }
446
+
447
+ switch (sub) {
448
+ case 'status': return handleHooksStatus(config);
449
+ case 'toggle': return handleHooksToggle(config);
450
+ case 'setup': return handleHooksSetup(config);
451
+ case 'reset': return handleHooksReset(config);
452
+ }
453
+ }
454
+
455
+ async function handleHooksStatus(config) {
456
+ const status = getHookStatus(config.cwd);
457
+ const hookNames = Object.keys(ALL_HOOKS);
458
+ const activeCount = hookNames.filter(h => status[h].active).length;
459
+
460
+ const lines = [];
461
+ for (const event of hookNames) {
462
+ const info = status[event];
463
+ const icon = info.active ? style.green('\u25CF') : style.dim('\u25CB');
464
+ const state = info.active ? 'actif' : 'inactif';
465
+ lines.push(` ${icon} ${event.padEnd(20)} ${state.padEnd(8)} ${style.dim(info.command)}`);
466
+ }
467
+ lines.push('');
468
+ lines.push(activeCount === hookNames.length
469
+ ? style.green(`${activeCount}/${hookNames.length} hooks actifs`)
470
+ : `${activeCount}/${hookNames.length} hooks actifs`);
471
+
472
+ console.log('');
473
+ console.log(titledBox('Hooks Claude Code', lines, { border: style.nemesisAccent }));
474
+ console.log('');
475
+ }
476
+
477
+ async function handleHooksToggle(config) {
478
+ const hookNames = Object.keys(ALL_HOOKS);
479
+
480
+ while (true) {
481
+ const status = getHookStatus(config.cwd);
482
+ const items = hookNames.map(event => {
483
+ const info = status[event];
484
+ const icon = info.active ? style.green('\u25CF') : style.dim('\u25CB');
485
+ const state = info.active ? 'actif' : 'inactif';
486
+ return {
487
+ label: `${icon} ${event}`,
488
+ value: event,
489
+ description: `${state} — ${info.command}`,
490
+ };
491
+ });
492
+ items.push(
493
+ { label: 'Tout activer', value: '__setup__', description: '' },
494
+ { label: 'Tout desactiver', value: '__reset__', description: '' },
495
+ BACK_ITEM,
496
+ HOME_ITEM,
497
+ );
498
+
499
+ const choice = await interactiveMenu(items, { title: 'Toggle hooks' });
500
+ if (!choice || choice === '__back__') return;
501
+ if (choice === '__home__') return '__home__';
502
+
503
+ if (choice === '__setup__') {
504
+ await handleHooksSetup(config);
505
+ continue;
506
+ }
507
+ if (choice === '__reset__') {
508
+ await handleHooksReset(config);
509
+ continue;
510
+ }
511
+
512
+ // Toggle single hook
513
+ const info = status[choice];
514
+ const result = info.active
515
+ ? deactivateSingleHook(config.cwd, choice)
516
+ : activateSingleHook(config.cwd, choice);
517
+
518
+ if (result.ok) {
519
+ console.log(` ${style.green('\u2713')} ${choice} ${info.active ? 'desactive' : 'active'}`);
520
+ } else {
521
+ console.log(` ${style.red('\u2717')} Echec : ${result.error}`);
522
+ }
523
+ }
524
+ }
525
+
526
+ async function handleHooksSetup(config) {
527
+ const statusBefore = getHookStatus(config.cwd);
528
+ const alreadyActive = Object.values(statusBefore).filter(h => h.active).length;
529
+
530
+ installHooks(config.cwd);
531
+
532
+ const statusAfter = getHookStatus(config.cwd);
533
+ const nowActive = Object.values(statusAfter).filter(h => h.active).length;
534
+ const activated = nowActive - alreadyActive;
535
+
536
+ for (const [event, info] of Object.entries(statusAfter)) {
537
+ console.log(` ${style.green('\u2713')} ${event} — ${info.active ? 'actif' : 'inactif'}`);
538
+ }
539
+ console.log('');
540
+ console.log(` ${style.bold('Bilan :')} ${activated} actives, ${alreadyActive} deja actifs`);
541
+ console.log('');
542
+ }
543
+
544
+ async function handleHooksReset(config) {
545
+ const statusBefore = getHookStatus(config.cwd);
546
+ const wasActive = Object.values(statusBefore).filter(h => h.active).length;
547
+ const wasInactive = Object.values(statusBefore).filter(h => !h.active).length;
548
+
549
+ removeHooks(config.cwd);
550
+
551
+ for (const [event] of Object.entries(ALL_HOOKS)) {
552
+ console.log(` ${style.dim('\u25CB')} ${event} — inactif`);
553
+ }
554
+ console.log('');
555
+ console.log(` ${style.bold('Bilan :')} ${wasActive} desactives, ${wasInactive} deja inactifs`);
556
+ console.log('');
557
+ }
558
+
559
+ // ── config ──────────────────────────────────────────────────────────
560
+
561
+ async function handleConfig(args, flags, config) {
562
+ const project = await ensureProject(config, flags);
563
+ if (!project) return;
564
+
565
+ let serviceId = args[0];
566
+ if (!serviceId) {
567
+ const items = SERVICE_IDS.map(id => ({
568
+ label: id, value: id, description: SERVICE_LABELS[id],
569
+ }));
570
+ items.push(BACK_ITEM, HOME_ITEM);
571
+ serviceId = await interactiveMenu(items, { title: 'Configurer quel service ?' });
572
+ if (!serviceId || serviceId === '__back__') return '__back__';
573
+ if (serviceId === '__home__') return '__home__';
574
+ }
575
+ if (!SERVICE_IDS.includes(serviceId)) {
576
+ console.log(` Service inconnu : ${serviceId}`);
577
+ return;
578
+ }
579
+ return configureService(serviceId, project, flags, config);
580
+ }
581
+
582
+ async function configureService(serviceId, project, flags, config) {
583
+ const connexions = listConnexions();
584
+ if (connexions.length === 0) {
585
+ console.log(`\n ${style.dim('Aucune connexion. Creez-en une d\'abord :')}`);
586
+ console.log(` nemesis services connexions add\n`);
587
+ return;
588
+ }
589
+ console.log(`\n Service : ${style.bold(SERVICE_LABELS[serviceId])}\n`);
590
+
591
+ // Connexion primaire
592
+ const primaryItems = connexions.map(c => ({
593
+ label: `${c.name} (${c.provider}${c.model ? ' \u2014 ' + c.model : ''})`,
594
+ value: c.id, description: '',
595
+ }));
596
+ const primaryId = await interactiveMenu(primaryItems, { title: 'Connexion primaire' });
597
+ if (!primaryId) return;
598
+
599
+ // Model override
600
+ const modelOverride = await askText('Modele override (laisser vide pour celui de la connexion)', '');
601
+
602
+ // Strategie fallback
603
+ const fbCountItems = [
604
+ { label: 'Aucun fallback', value: '0', description: '' },
605
+ { label: '1 fallback', value: '1', description: '' },
606
+ { label: '2 fallbacks', value: '2', description: '' },
607
+ ];
608
+ const fbCount = await interactiveMenu(fbCountItems, { title: 'Strategie de fallback' });
609
+ if (!fbCount) return;
610
+
611
+ const fallback = [];
612
+ const remaining = connexions.filter(c => c.id !== primaryId);
613
+ for (let i = 0; i < parseInt(fbCount); i++) {
614
+ const avail = remaining.filter(c => !fallback.some(f => f.connexion_id === c.id));
615
+ if (avail.length === 0) break;
616
+ const fbItems = avail.map(c => ({
617
+ label: `${c.name} (${c.provider}${c.model ? ' \u2014 ' + c.model : ''})`,
618
+ value: c.id, description: '',
619
+ }));
620
+ const fbId = await interactiveMenu(fbItems, { title: `Fallback ${i + 1}` });
621
+ if (!fbId) break;
622
+ const defaults = SERVICE_DEFAULTS[serviceId] || { fallback_timeout: 3 };
623
+ fallback.push({ connexion_id: fbId, timeout: defaults.fallback_timeout });
624
+ }
625
+
626
+ // Persister
627
+ assignService(project.root || config.cwd, serviceId, {
628
+ connexionId: primaryId, modelOverride: modelOverride || null, fallback,
629
+ });
630
+
631
+ // Confirmation
632
+ const primaryName = connexions.find(c => c.id === primaryId)?.name || primaryId;
633
+ console.log(`\n ${style.green('\u2713')} Service ${serviceId} configure`);
634
+ console.log(` Primaire \u2192 ${primaryName}`);
635
+ for (let i = 0; i < fallback.length; i++) {
636
+ const fbName = connexions.find(c => c.id === fallback[i].connexion_id)?.name || '?';
637
+ console.log(` Fallback ${i + 1} \u2192 ${fbName}`);
638
+ }
639
+ console.log('');
640
+ }
641
+
642
+ // ── status ──────────────────────────────────────────────────────────
643
+
644
+ async function handleStatus(args, flags, config) {
645
+ const project = await ensureProject(config, flags);
646
+ if (!project) return;
647
+
648
+ const svcData = readServices(project.root || config.cwd);
649
+ const connexions = listConnexions();
650
+
651
+ if (Object.keys(svcData.services).length === 0) {
652
+ console.log(`\n ${style.dim('Aucun service configure.')}`);
653
+ console.log(` ${style.dim('Utilisez')} nemesis services config\n`);
654
+ return;
655
+ }
656
+
657
+ const lines = [];
658
+ for (const sid of SERVICE_IDS) {
659
+ const svc = svcData.services[sid];
660
+ if (!svc) {
661
+ lines.push(`${style.bold(SERVICE_LABELS[sid])}`);
662
+ lines.push(` ${style.dim('\u25CB')} non configure`);
663
+ lines.push('');
664
+ continue;
665
+ }
666
+
667
+ lines.push(`${style.bold(SERVICE_LABELS[sid])}`);
668
+
669
+ // Primaire
670
+ const primary = connexions.find(c => c.id === svc.connexion_id);
671
+ if (primary) {
672
+ const statusIcon = primary.status === 'connected'
673
+ ? style.green('\u25CF')
674
+ : primary.status === 'error'
675
+ ? style.red('\u25CF')
676
+ : style.dim('\u25CB');
677
+ const tested = primary.last_tested
678
+ ? new Date(primary.last_tested).toLocaleDateString('fr-FR') + ' ' +
679
+ new Date(primary.last_tested).toLocaleTimeString('fr-FR', { hour: '2-digit', minute: '2-digit' })
680
+ : '';
681
+ lines.push(` Primaire ${primary.name} ${statusIcon} ${primary.status} ${style.dim(tested)}`);
682
+ } else {
683
+ lines.push(` Primaire ${style.red('connexion manquante')}`);
684
+ }
685
+
686
+ // Fallbacks
687
+ for (let i = 0; i < (svc.fallback || []).length; i++) {
688
+ const fb = svc.fallback[i];
689
+ const fbCx = connexions.find(c => c.id === fb.connexion_id);
690
+ if (fbCx) {
691
+ const fbIcon = fbCx.status === 'connected'
692
+ ? style.green('\u25CF')
693
+ : fbCx.status === 'error'
694
+ ? style.red('\u25CF')
695
+ : style.dim('\u25CB');
696
+ lines.push(` Fallback ${fbCx.name} ${fbIcon} ${fbCx.status}`);
697
+ }
698
+ }
699
+ lines.push('');
700
+ }
701
+
702
+ console.log('');
703
+ console.log(titledBox('Services LLM', lines, { border: style.nemesisAccent }));
704
+ console.log('');
705
+ }