@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.
- package/LICENSE +201 -0
- package/README.md +668 -0
- package/lib/core/agent-launcher.js +193 -0
- package/lib/core/audit.js +210 -0
- package/lib/core/connexions.js +80 -0
- package/lib/core/flowmap/api.js +111 -0
- package/lib/core/flowmap/cli-helpers.js +80 -0
- package/lib/core/flowmap/machine.js +281 -0
- package/lib/core/flowmap/persistence.js +83 -0
- package/lib/core/generators.js +183 -0
- package/lib/core/inbox.js +275 -0
- package/lib/core/logger.js +20 -0
- package/lib/core/mission.js +109 -0
- package/lib/core/notewriter/config.js +36 -0
- package/lib/core/notewriter/cr.js +237 -0
- package/lib/core/notewriter/log.js +112 -0
- package/lib/core/notewriter/notes.js +168 -0
- package/lib/core/notewriter/paths.js +45 -0
- package/lib/core/notewriter/reader.js +121 -0
- package/lib/core/notewriter/registry.js +80 -0
- package/lib/core/odm.js +191 -0
- package/lib/core/profile-picker.js +323 -0
- package/lib/core/project.js +287 -0
- package/lib/core/registry.js +129 -0
- package/lib/core/secrets.js +137 -0
- package/lib/core/services.js +45 -0
- package/lib/core/team.js +287 -0
- package/lib/core/templates.js +80 -0
- package/lib/kairos/agent-runner.js +261 -0
- package/lib/kairos/claude-invoker.js +90 -0
- package/lib/kairos/context-injector.js +331 -0
- package/lib/kairos/context-loader.js +108 -0
- package/lib/kairos/context-writer.js +45 -0
- package/lib/kairos/dispatcher-router.js +173 -0
- package/lib/kairos/dispatcher.js +139 -0
- package/lib/kairos/event-bus.js +287 -0
- package/lib/kairos/event-router.js +131 -0
- package/lib/kairos/flowmap-bridge.js +120 -0
- package/lib/kairos/hook-handlers.js +351 -0
- package/lib/kairos/hook-installer.js +207 -0
- package/lib/kairos/hook-prompts.js +54 -0
- package/lib/kairos/leader-rules.js +94 -0
- package/lib/kairos/pid-checker.js +108 -0
- package/lib/kairos/situation-detector.js +123 -0
- package/lib/sync/fallback-engine.js +97 -0
- package/lib/sync/hcm-client.js +170 -0
- package/lib/sync/health.js +47 -0
- package/lib/sync/llm-client.js +387 -0
- package/lib/sync/nemesis-client.js +379 -0
- package/lib/sync/service-session.js +74 -0
- package/lib/sync/sync-engine.js +178 -0
- package/lib/ui/box.js +104 -0
- package/lib/ui/brand.js +42 -0
- package/lib/ui/colors.js +57 -0
- package/lib/ui/dashboard.js +580 -0
- package/lib/ui/error-hints.js +49 -0
- package/lib/ui/format.js +61 -0
- package/lib/ui/menu.js +306 -0
- package/lib/ui/note-card.js +198 -0
- package/lib/ui/note-colors.js +26 -0
- package/lib/ui/note-detail.js +297 -0
- package/lib/ui/note-filters.js +252 -0
- package/lib/ui/note-views.js +283 -0
- package/lib/ui/prompt.js +81 -0
- package/lib/ui/spinner.js +139 -0
- package/lib/ui/streambox.js +46 -0
- package/lib/ui/table.js +42 -0
- package/lib/ui/tree.js +33 -0
- package/package.json +53 -0
- package/src/cli.js +457 -0
- package/src/commands/_helpers.js +119 -0
- package/src/commands/audit.js +187 -0
- package/src/commands/auth.js +316 -0
- package/src/commands/doctor.js +243 -0
- package/src/commands/hcm.js +147 -0
- package/src/commands/inbox.js +333 -0
- package/src/commands/init.js +160 -0
- package/src/commands/kairos.js +216 -0
- package/src/commands/kars.js +134 -0
- package/src/commands/mission.js +275 -0
- package/src/commands/notes.js +316 -0
- package/src/commands/notewriter.js +296 -0
- package/src/commands/odm.js +329 -0
- package/src/commands/orch.js +68 -0
- package/src/commands/project.js +123 -0
- package/src/commands/run.js +123 -0
- package/src/commands/services.js +705 -0
- package/src/commands/status.js +231 -0
- package/src/commands/team.js +572 -0
- package/src/config.js +84 -0
- package/src/index.js +5 -0
- package/templates/project-context.json +10 -0
- package/templates/template_CONTRIB-NAME.json +22 -0
- package/templates/template_CR-ODM-NAME-000.exemple.json +32 -0
- package/templates/template_DEC-NAME-000.json +18 -0
- package/templates/template_INTV-NAME-000.json +15 -0
- package/templates/template_MISSION_CONTRACT.json +46 -0
- package/templates/template_ODM-NAME-000.json +89 -0
- package/templates/template_REGISTRY-PROJECT.json +26 -0
- 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
|
+
}
|