@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,333 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* nemesis inbox — Manage human document inbox pipeline.
|
|
3
|
+
*
|
|
4
|
+
* Subcommands:
|
|
5
|
+
* list List pending documents
|
|
6
|
+
* add <file> Add a document to the inbox
|
|
7
|
+
* process [docId] Dispatch a document to the Leader for MC draft
|
|
8
|
+
* history Show completed documents
|
|
9
|
+
*
|
|
10
|
+
* Options:
|
|
11
|
+
* -h, --help Help
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import { resolve } from 'node:path';
|
|
15
|
+
import { ensureProject, pickFromList, BACK_ITEM, HOME_ITEM } from './_helpers.js';
|
|
16
|
+
import { readRegistry, getLanes } from '../../lib/core/registry.js';
|
|
17
|
+
import {
|
|
18
|
+
addDocument,
|
|
19
|
+
addDocumentFromText,
|
|
20
|
+
listDocuments,
|
|
21
|
+
readDocument,
|
|
22
|
+
markProcessing,
|
|
23
|
+
cleanupDone,
|
|
24
|
+
} from '../../lib/core/inbox.js';
|
|
25
|
+
import { launchHeadless } from '../../lib/kairos/agent-runner.js';
|
|
26
|
+
import { createDispatchTransaction } from '../../lib/kairos/dispatcher-router.js';
|
|
27
|
+
import { titledBox } from '../../lib/ui/box.js';
|
|
28
|
+
import { style } from '../../lib/ui/colors.js';
|
|
29
|
+
import { interactiveMenu } from '../../lib/ui/menu.js';
|
|
30
|
+
import { askText } from '../../lib/ui/prompt.js';
|
|
31
|
+
import { formatError, COMMON_HINTS } from '../../lib/ui/error-hints.js';
|
|
32
|
+
|
|
33
|
+
export async function handler({ args, flags, config }) {
|
|
34
|
+
if (flags.help) {
|
|
35
|
+
printHelp();
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const sub = args[0];
|
|
40
|
+
const subArgs = args.slice(1);
|
|
41
|
+
|
|
42
|
+
if (sub === 'list') return handleList(config);
|
|
43
|
+
if (sub === 'add') return handleAdd(subArgs, config);
|
|
44
|
+
if (sub === 'process') return handleProcess(subArgs, flags, config);
|
|
45
|
+
if (sub === 'history') return handleHistory(config);
|
|
46
|
+
|
|
47
|
+
// No subcommand → interactive menu
|
|
48
|
+
const choice = await interactiveMenu([
|
|
49
|
+
{ label: 'Documents en attente', value: 'list', description: 'Voir les documents pending' },
|
|
50
|
+
{ label: 'Ajouter un document', value: 'add', description: 'Ajouter un fichier a l\'inbox' },
|
|
51
|
+
{ label: 'Traiter un document', value: 'process', description: 'Dispatcher au Leader' },
|
|
52
|
+
{ label: 'Historique', value: 'history', description: 'Documents traites' },
|
|
53
|
+
BACK_ITEM, HOME_ITEM,
|
|
54
|
+
], { title: 'nemesis › Inbox' });
|
|
55
|
+
|
|
56
|
+
if (!choice || choice === '__back__') return '__back__';
|
|
57
|
+
if (choice === '__home__') return '__home__';
|
|
58
|
+
|
|
59
|
+
if (choice === 'list') return handleList(config);
|
|
60
|
+
if (choice === 'add') return handleAdd(subArgs, config);
|
|
61
|
+
if (choice === 'process') return handleProcess(subArgs, flags, config);
|
|
62
|
+
if (choice === 'history') return handleHistory(config);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// ---------------------------------------------------------------------------
|
|
66
|
+
// List pending documents
|
|
67
|
+
// ---------------------------------------------------------------------------
|
|
68
|
+
|
|
69
|
+
async function handleList(config) {
|
|
70
|
+
const root = config.cwd;
|
|
71
|
+
const docs = listDocuments(root, 'pending');
|
|
72
|
+
const processing = listDocuments(root, 'processing');
|
|
73
|
+
|
|
74
|
+
if (docs.length === 0 && processing.length === 0) {
|
|
75
|
+
console.log(`\n ${style.dim('Aucun document en attente.')}`);
|
|
76
|
+
console.log(` ${style.dim('Ajoutez un document :')} nemesis inbox add <fichier>\n`);
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
if (docs.length > 0) {
|
|
81
|
+
console.log('');
|
|
82
|
+
const lines = docs.map(d => {
|
|
83
|
+
const age = formatRelativeTime(d.created_at);
|
|
84
|
+
return `${style.bold(d.id)} ${d.title} ${style.dim(age)} [${d.source}]`;
|
|
85
|
+
});
|
|
86
|
+
console.log(titledBox(`Pending (${docs.length})`, lines, { border: style.yellow }));
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
if (processing.length > 0) {
|
|
90
|
+
console.log('');
|
|
91
|
+
const lines = processing.map(d => {
|
|
92
|
+
const age = formatRelativeTime(d.processing_at || d.created_at);
|
|
93
|
+
return `${style.bold(d.id)} ${d.title} ${style.dim(age)} ${style.nemesisAccent('en cours')}`;
|
|
94
|
+
});
|
|
95
|
+
console.log(titledBox(`En cours (${processing.length})`, lines, { border: style.nemesisAccent }));
|
|
96
|
+
}
|
|
97
|
+
console.log('');
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// ---------------------------------------------------------------------------
|
|
101
|
+
// Add document
|
|
102
|
+
// ---------------------------------------------------------------------------
|
|
103
|
+
|
|
104
|
+
async function handleAdd(args, config) {
|
|
105
|
+
const root = config.cwd;
|
|
106
|
+
|
|
107
|
+
const filePath = args[0];
|
|
108
|
+
|
|
109
|
+
if (filePath) {
|
|
110
|
+
const absPath = resolve(root, filePath);
|
|
111
|
+
try {
|
|
112
|
+
const { id } = addDocument(root, absPath);
|
|
113
|
+
console.log(`\n ${style.green('✓')} Document ${style.bold(id)} ajoute a l'inbox.`);
|
|
114
|
+
console.log(` ${style.dim('→ Traitez-le :')} nemesis inbox process ${id}\n`);
|
|
115
|
+
} catch (err) {
|
|
116
|
+
console.log(`\n ${style.red('✗')} ${err.message}\n`);
|
|
117
|
+
}
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// Interactive: ask for text or file
|
|
122
|
+
const choice = await interactiveMenu([
|
|
123
|
+
{ label: 'Depuis un fichier', value: 'file', description: 'Copier un fichier dans l\'inbox' },
|
|
124
|
+
{ label: 'Texte libre', value: 'text', description: 'Saisir le contenu directement' },
|
|
125
|
+
BACK_ITEM,
|
|
126
|
+
], { title: 'Type de document' });
|
|
127
|
+
|
|
128
|
+
if (!choice || choice === '__back__') return '__back__';
|
|
129
|
+
|
|
130
|
+
if (choice === 'file') {
|
|
131
|
+
const path = await askText('Chemin du fichier');
|
|
132
|
+
if (!path) return;
|
|
133
|
+
const absPath = resolve(root, path);
|
|
134
|
+
try {
|
|
135
|
+
const { id } = addDocument(root, absPath);
|
|
136
|
+
console.log(`\n ${style.green('✓')} Document ${style.bold(id)} ajoute.\n`);
|
|
137
|
+
} catch (err) {
|
|
138
|
+
console.log(`\n ${style.red('✗')} ${err.message}\n`);
|
|
139
|
+
}
|
|
140
|
+
} else {
|
|
141
|
+
const title = await askText('Titre du document');
|
|
142
|
+
const content = await askText('Contenu');
|
|
143
|
+
if (!content) return;
|
|
144
|
+
const { id } = addDocumentFromText(root, content, { title: title || undefined });
|
|
145
|
+
console.log(`\n ${style.green('✓')} Document ${style.bold(id)} ajoute.\n`);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// ---------------------------------------------------------------------------
|
|
150
|
+
// Process — dispatch document to Leader
|
|
151
|
+
// ---------------------------------------------------------------------------
|
|
152
|
+
|
|
153
|
+
async function handleProcess(args, flags, config) {
|
|
154
|
+
const project = await ensureProject(config, flags);
|
|
155
|
+
if (!project) return;
|
|
156
|
+
|
|
157
|
+
const root = config.cwd;
|
|
158
|
+
const hcmDir = project.hcm_dir;
|
|
159
|
+
|
|
160
|
+
// Find the Leader
|
|
161
|
+
const registry = readRegistry(hcmDir);
|
|
162
|
+
if (!registry) {
|
|
163
|
+
console.log(formatError('Registry introuvable.', COMMON_HINTS.REGISTRY_NOT_FOUND));
|
|
164
|
+
return;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
const lanes = getLanes(registry);
|
|
168
|
+
const leader = lanes.find(l => l.leader);
|
|
169
|
+
if (!leader) {
|
|
170
|
+
console.log(formatError('Aucun Leader dans l\'equipe.', {
|
|
171
|
+
hint: 'Ajoutez un agent Leader avec nemesis team add.',
|
|
172
|
+
command: 'nemesis team add',
|
|
173
|
+
}));
|
|
174
|
+
return;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
if (!leader.session_id) {
|
|
178
|
+
console.log(formatError(`Le Leader "${leader.name}" n'a pas de session.`, COMMON_HINTS.AGENT_NO_SESSION));
|
|
179
|
+
return;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// Pick document
|
|
183
|
+
let docId = args[0];
|
|
184
|
+
if (!docId) {
|
|
185
|
+
const docs = listDocuments(root, 'pending');
|
|
186
|
+
if (docs.length === 0) {
|
|
187
|
+
console.log(`\n ${style.dim('Aucun document en attente.')}\n`);
|
|
188
|
+
return;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
docId = await pickFromList(docs, {
|
|
192
|
+
title: 'Document a traiter',
|
|
193
|
+
labelFn: d => `${d.id} ${d.title} ${style.dim(formatRelativeTime(d.created_at))}`,
|
|
194
|
+
});
|
|
195
|
+
if (!docId) return;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
const doc = readDocument(root, docId, 'pending');
|
|
199
|
+
if (!doc) {
|
|
200
|
+
console.log(`\n ${style.red('✗')} Document "${docId}" introuvable dans pending.\n`);
|
|
201
|
+
return;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
// Build inbox prompt for Leader
|
|
205
|
+
const prompt = buildInboxPrompt(doc);
|
|
206
|
+
|
|
207
|
+
// Create transaction
|
|
208
|
+
const txnResult = createDispatchTransaction({
|
|
209
|
+
from: 'PM',
|
|
210
|
+
to: leader.name,
|
|
211
|
+
projectId: project.id,
|
|
212
|
+
root,
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
// Launch headless
|
|
216
|
+
try {
|
|
217
|
+
const { pid, txnId } = await launchHeadless(leader, prompt, {
|
|
218
|
+
txnId: txnResult.txnId,
|
|
219
|
+
projectId: project.id,
|
|
220
|
+
root,
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
// Mark as processing
|
|
224
|
+
markProcessing(root, docId, { txnId, agentName: leader.name });
|
|
225
|
+
|
|
226
|
+
console.log('');
|
|
227
|
+
console.log(titledBox('Document dispatche au Leader', [
|
|
228
|
+
`${style.bold('Document')} : ${doc.metadata.title} (${docId})`,
|
|
229
|
+
`${style.bold('Leader')} : ${leader.name}`,
|
|
230
|
+
`${style.bold('TXN')} : ${txnId}`,
|
|
231
|
+
`${style.bold('PID')} : ${pid || 'detache'}`,
|
|
232
|
+
'',
|
|
233
|
+
`Le Leader analysera le document et proposera un Mission Contract.`,
|
|
234
|
+
], { border: style.green }));
|
|
235
|
+
console.log('');
|
|
236
|
+
} catch (err) {
|
|
237
|
+
console.log(`\n ${style.red('✗')} ${err.message}\n`);
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
// ---------------------------------------------------------------------------
|
|
242
|
+
// History — completed documents
|
|
243
|
+
// ---------------------------------------------------------------------------
|
|
244
|
+
|
|
245
|
+
async function handleHistory(config) {
|
|
246
|
+
const root = config.cwd;
|
|
247
|
+
const docs = listDocuments(root, 'done');
|
|
248
|
+
|
|
249
|
+
if (docs.length === 0) {
|
|
250
|
+
console.log(`\n ${style.dim('Aucun document traite.')}\n`);
|
|
251
|
+
return;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
console.log('');
|
|
255
|
+
const lines = docs.map(d => {
|
|
256
|
+
const age = formatRelativeTime(d.done_at || d.created_at);
|
|
257
|
+
const mc = d.mission_id ? ` → ${style.green(d.mission_id)}` : '';
|
|
258
|
+
return `${style.bold(d.id)} ${d.title} ${style.dim(age)}${mc}`;
|
|
259
|
+
});
|
|
260
|
+
console.log(titledBox(`Traites (${docs.length})`, lines, { border: style.green }));
|
|
261
|
+
console.log('');
|
|
262
|
+
|
|
263
|
+
// Cleanup old done
|
|
264
|
+
const cleaned = cleanupDone(root);
|
|
265
|
+
if (cleaned > 0) {
|
|
266
|
+
console.log(` ${style.dim(`${cleaned} document(s) archive(s) nettoye(s) (>30j)`)}\n`);
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
// ---------------------------------------------------------------------------
|
|
271
|
+
// Helpers
|
|
272
|
+
// ---------------------------------------------------------------------------
|
|
273
|
+
|
|
274
|
+
/**
|
|
275
|
+
* Build a prompt for the Leader to analyze an inbox document and produce an MC draft.
|
|
276
|
+
*/
|
|
277
|
+
function buildInboxPrompt(doc) {
|
|
278
|
+
const parts = [
|
|
279
|
+
'# Document inbox a analyser',
|
|
280
|
+
'',
|
|
281
|
+
`**Titre** : ${doc.metadata.title}`,
|
|
282
|
+
`**Source** : ${doc.metadata.source}`,
|
|
283
|
+
`**Date** : ${doc.metadata.created_at}`,
|
|
284
|
+
'',
|
|
285
|
+
'## Contenu du document',
|
|
286
|
+
'',
|
|
287
|
+
doc.content,
|
|
288
|
+
'',
|
|
289
|
+
'## Instructions',
|
|
290
|
+
'',
|
|
291
|
+
'Lis ce document attentivement. Propose un Mission Contract avec :',
|
|
292
|
+
'- Titre et objectifs',
|
|
293
|
+
'- Scope (in/out)',
|
|
294
|
+
'- Liste des OdMs necessaires avec assignation agent',
|
|
295
|
+
'- Criteres de succes',
|
|
296
|
+
'',
|
|
297
|
+
'Depose le MC draft dans .nemesis/HCM/contracts/ au format JSON structure.',
|
|
298
|
+
'Utilise le template mission_contract : nemesis mission init <ID>',
|
|
299
|
+
'',
|
|
300
|
+
];
|
|
301
|
+
return parts.join('\n');
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
export { buildInboxPrompt };
|
|
305
|
+
|
|
306
|
+
function formatRelativeTime(isoDate) {
|
|
307
|
+
if (!isoDate) return '';
|
|
308
|
+
const diff = Date.now() - new Date(isoDate).getTime();
|
|
309
|
+
const minutes = Math.floor(diff / 60000);
|
|
310
|
+
if (minutes < 1) return 'a l\'instant';
|
|
311
|
+
if (minutes < 60) return `il y a ${minutes}m`;
|
|
312
|
+
const hours = Math.floor(minutes / 60);
|
|
313
|
+
if (hours < 24) return `il y a ${hours}h`;
|
|
314
|
+
const days = Math.floor(hours / 24);
|
|
315
|
+
return `il y a ${days}j`;
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
function printHelp() {
|
|
319
|
+
console.log(`
|
|
320
|
+
${style.bold('nemesis inbox')} — Pipeline de documents humains
|
|
321
|
+
|
|
322
|
+
${style.bold('Usage:')} nemesis inbox [subcommand] [options]
|
|
323
|
+
|
|
324
|
+
${style.bold('Subcommands:')}
|
|
325
|
+
list Documents en attente
|
|
326
|
+
add <file> Ajouter un document a l'inbox
|
|
327
|
+
process [docId] Dispatcher au Leader pour MC draft
|
|
328
|
+
history Documents traites
|
|
329
|
+
|
|
330
|
+
${style.bold('Options:')}
|
|
331
|
+
-h, --help Aide
|
|
332
|
+
`);
|
|
333
|
+
}
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
import { initProject, storeHcmStatus } from '../../lib/core/project.js';
|
|
2
|
+
import { createHcmClient } from '../../lib/sync/hcm-client.js';
|
|
3
|
+
import { askText } from '../../lib/ui/prompt.js';
|
|
4
|
+
import { createMultiStepSpinner } from '../../lib/ui/spinner.js';
|
|
5
|
+
import { renderBrandHeader } from '../../lib/ui/brand.js';
|
|
6
|
+
import { style } from '../../lib/ui/colors.js';
|
|
7
|
+
import { existsSync, readFileSync, writeFileSync } from 'node:fs';
|
|
8
|
+
import { join } from 'node:path';
|
|
9
|
+
import { homedir } from 'node:os';
|
|
10
|
+
|
|
11
|
+
const HELP = `
|
|
12
|
+
nemesis init — Scaffold structure projet
|
|
13
|
+
|
|
14
|
+
Usage: nemesis init [options]
|
|
15
|
+
|
|
16
|
+
Options:
|
|
17
|
+
--bare Mode non-interactif (defaults)
|
|
18
|
+
--id <id> ID du projet
|
|
19
|
+
--name <name> Nom du projet
|
|
20
|
+
--desc <desc> Description
|
|
21
|
+
-h, --help Aide
|
|
22
|
+
`;
|
|
23
|
+
|
|
24
|
+
export async function handler({ args, flags, config }) {
|
|
25
|
+
if (flags.help || args.includes('--help') || args.includes('-h')) {
|
|
26
|
+
console.log(HELP);
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const bare = args.includes('--bare');
|
|
31
|
+
|
|
32
|
+
if (!bare) {
|
|
33
|
+
const header = renderBrandHeader();
|
|
34
|
+
for (const line of header) console.log(line);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
let projectId, projectName, description;
|
|
38
|
+
|
|
39
|
+
if (bare) {
|
|
40
|
+
projectId = getFlag(args, '--id') || 'PROJECT-001';
|
|
41
|
+
projectName = getFlag(args, '--name') || projectId;
|
|
42
|
+
description = getFlag(args, '--desc') || '';
|
|
43
|
+
} else {
|
|
44
|
+
projectName = await askText('Nom du projet');
|
|
45
|
+
projectId = await askText('ID projet', projectName.toUpperCase().replace(/\s+/g, '-'));
|
|
46
|
+
description = await askText('Description', '');
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const multiSpinner = createMultiStepSpinner(['Initialisation du projet', 'Validation HCM']);
|
|
50
|
+
multiSpinner.start();
|
|
51
|
+
|
|
52
|
+
const result = initProject({
|
|
53
|
+
projectId,
|
|
54
|
+
projectName,
|
|
55
|
+
description,
|
|
56
|
+
root: config.cwd,
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
multiSpinner.nextStep();
|
|
60
|
+
|
|
61
|
+
// Validate HCM connectivity
|
|
62
|
+
const client = createHcmClient();
|
|
63
|
+
const ping = await client.ping();
|
|
64
|
+
storeHcmStatus(result.hcmDir, projectId, {
|
|
65
|
+
connected: ping.ok,
|
|
66
|
+
url: client.baseUrl,
|
|
67
|
+
latency: ping.latency,
|
|
68
|
+
checked_at: new Date().toISOString(),
|
|
69
|
+
...(ping.error ? { error: ping.error } : {}),
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
if (ping.ok) {
|
|
73
|
+
multiSpinner.complete('Projet initialise');
|
|
74
|
+
writeMcpConfig(config.cwd, client.baseUrl);
|
|
75
|
+
} else {
|
|
76
|
+
multiSpinner.fail('HCM non connecte');
|
|
77
|
+
console.log(` ${style.dim('\u2192')} ${style.dim(ping.error || 'timeout')}`);
|
|
78
|
+
console.log(` ${style.dim('\u2192 Configurez avec')} nemesis auth login`);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
console.log('');
|
|
82
|
+
console.log(' Cree :');
|
|
83
|
+
for (const dir of result.created) {
|
|
84
|
+
console.log(` ${style.green('\u2713')} .nemesis/HCM/${dir}/`);
|
|
85
|
+
}
|
|
86
|
+
if (result.created.length === 0) {
|
|
87
|
+
console.log(' (structure deja existante)');
|
|
88
|
+
}
|
|
89
|
+
console.log(` ${style.green('\u2713')} .nemesis/HCM/PROCESS.md`);
|
|
90
|
+
console.log(` ${style.green('\u2713')} .nemesis/template/ (templates)`);
|
|
91
|
+
console.log(` ${style.green('\u2713')} .nemesis/HCM/project/CONTEXT_PROJECT_${projectId}.json`);
|
|
92
|
+
console.log(` ${style.green('\u2713')} .nemesis/HCM/project/REGISTRY-${projectId}.json (PM seul)`);
|
|
93
|
+
console.log('');
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Write or update .mcp.json at project root with HCM MCP server config.
|
|
98
|
+
* Reads saved auth from ~/.nemesis/config.json for API key and MCP server path.
|
|
99
|
+
* Creates the file if missing, updates the API key if stale.
|
|
100
|
+
*/
|
|
101
|
+
function writeMcpConfig(root, hcmUrl) {
|
|
102
|
+
const mcpPath = join(root, '.mcp.json');
|
|
103
|
+
const saved = loadSavedNemesisConfig();
|
|
104
|
+
const apiKey = saved.hcm_api_key || process.env.HCM_API_KEY || '';
|
|
105
|
+
const mcpServerPath = saved.hcm_mcp_path || process.env.HCM_MCP_PATH || '';
|
|
106
|
+
|
|
107
|
+
// Update existing .mcp.json if API key is stale
|
|
108
|
+
if (existsSync(mcpPath)) {
|
|
109
|
+
try {
|
|
110
|
+
const existing = JSON.parse(readFileSync(mcpPath, 'utf-8'));
|
|
111
|
+
const currentKey = existing?.mcpServers?.HCM_Sipher?.env?.HCM_API_KEY || '';
|
|
112
|
+
if (apiKey && currentKey !== apiKey) {
|
|
113
|
+
existing.mcpServers.HCM_Sipher.env.HCM_API_KEY = apiKey;
|
|
114
|
+
if (hcmUrl) existing.mcpServers.HCM_Sipher.env.HCM_API_URL = hcmUrl;
|
|
115
|
+
writeFileSync(mcpPath, JSON.stringify(existing, null, 2) + '\n', 'utf-8');
|
|
116
|
+
console.log(` ${style.green('\u2713')} .mcp.json mis a jour (cle API synchronisee)`);
|
|
117
|
+
}
|
|
118
|
+
} catch { /* malformed — recreate below */ }
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
if (!mcpServerPath) {
|
|
123
|
+
console.log(` ${style.yellow('\u26A0')} .mcp.json non genere — hcm_mcp_path absent`);
|
|
124
|
+
console.log(` ${style.dim('\u2192 Ajoutez "hcm_mcp_path" dans ~/.nemesis/config.json')}`);
|
|
125
|
+
console.log(` ${style.dim(' ou definissez HCM_MCP_PATH dans l\'environnement')}`);
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
const mcpConfig = {
|
|
130
|
+
mcpServers: {
|
|
131
|
+
HCM_Sipher: {
|
|
132
|
+
type: 'stdio',
|
|
133
|
+
command: 'npx',
|
|
134
|
+
args: ['tsx', mcpServerPath],
|
|
135
|
+
env: {
|
|
136
|
+
HCM_API_URL: hcmUrl,
|
|
137
|
+
...(apiKey ? { HCM_API_KEY: apiKey } : {}),
|
|
138
|
+
},
|
|
139
|
+
},
|
|
140
|
+
},
|
|
141
|
+
};
|
|
142
|
+
|
|
143
|
+
writeFileSync(mcpPath, JSON.stringify(mcpConfig, null, 2) + '\n', 'utf-8');
|
|
144
|
+
console.log(` ${style.green('\u2713')} .mcp.json (MCP HCM pour tous les agents)`);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function loadSavedNemesisConfig() {
|
|
148
|
+
const configFile = join(homedir(), '.nemesis', 'config.json');
|
|
149
|
+
if (!existsSync(configFile)) return {};
|
|
150
|
+
try {
|
|
151
|
+
return JSON.parse(readFileSync(configFile, 'utf-8'));
|
|
152
|
+
} catch {
|
|
153
|
+
return {};
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function getFlag(args, flag) {
|
|
158
|
+
const idx = args.indexOf(flag);
|
|
159
|
+
return idx !== -1 && idx + 1 < args.length ? args[idx + 1] : null;
|
|
160
|
+
}
|