@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,297 @@
1
+ import { emitKeypressEvents } from 'node:readline';
2
+ import { formatDate, formatTime, wrapText, CARD_WIDTH } from './note-card.js';
3
+ import { levelColor, levelLabel } from './note-colors.js';
4
+ import { style } from './colors.js';
5
+ import { padEndVisible, stringWidth } from './box.js';
6
+
7
+ const SINGLE = { tl: '\u250c', tr: '\u2510', bl: '\u2514', br: '\u2518', h: '\u2500', v: '\u2502' };
8
+ const DOUBLE = { tl: '\u2554', tr: '\u2557', bl: '\u255a', br: '\u255d', h: '\u2550', v: '\u2551' };
9
+
10
+ function buildNoteDetailLines(note) {
11
+ const level = note.level || 'L1';
12
+ const color = (ch) => levelColor(level, ch);
13
+ const agentShort = (note.agentId || '').replace(/^Agent_/i, '');
14
+ const W = CARD_WIDTH;
15
+ const lines = [];
16
+
17
+ // Header
18
+ const headerText = ` ${note.id || ''} `;
19
+ const headerFill = SINGLE.h.repeat(Math.max(0, W - stringWidth(headerText) - 1));
20
+ lines.push(color(SINGLE.tl + SINGLE.h) + color(headerText) + color(headerFill + SINGLE.tr));
21
+
22
+ lines.push(color(SINGLE.v) + padEndVisible('', W) + color(SINGLE.v));
23
+
24
+ // Level + label + date
25
+ const lbl = levelLabel(level);
26
+ const dateStr = formatDate(note.timestamp);
27
+ const timeStr = formatTime(note.timestamp);
28
+ const meta1 = ` ${level} \u00b7 ${lbl}`;
29
+ const meta1Right = `${dateStr} \u00b7 ${timeStr} `;
30
+ const meta1Gap = W - stringWidth(meta1) - stringWidth(meta1Right);
31
+ lines.push(color(SINGLE.v) + padEndVisible(meta1 + ' '.repeat(Math.max(1, meta1Gap)) + meta1Right, W) + color(SINGLE.v));
32
+
33
+ // Agent + session + turn
34
+ const meta2 = ` ${agentShort} \u00b7 Session ${(note.sessionId || '').slice(0, 8)} \u00b7 Tour ${note.turn || '-'}`;
35
+ lines.push(color(SINGLE.v) + padEndVisible(meta2, W) + color(SINGLE.v));
36
+
37
+ // LLM provider
38
+ if (note.llmProvider) {
39
+ const providerInfo = ` LLM : ${note.llmProvider}${note.llmFallback ? ' (fallback)' : ''}`;
40
+ lines.push(color(SINGLE.v) + padEndVisible(providerInfo, W) + color(SINGLE.v));
41
+ }
42
+
43
+ // CR parent
44
+ if (note.previousCrId) {
45
+ const crRef = ` CR parent : ${note.previousCrId}`;
46
+ lines.push(color(SINGLE.v) + padEndVisible(crRef, W) + color(SINGLE.v));
47
+ }
48
+
49
+ lines.push(color(SINGLE.v) + padEndVisible('', W) + color(SINGLE.v));
50
+
51
+ // Separator
52
+ const sep = ' ' + SINGLE.h.repeat(W - 4) + ' ';
53
+ lines.push(color(SINGLE.v) + padEndVisible(sep, W) + color(SINGLE.v));
54
+ lines.push(color(SINGLE.v) + padEndVisible('', W) + color(SINGLE.v));
55
+
56
+ // Content
57
+ for (const cl of wrapText(note.content || '', W - 4)) {
58
+ lines.push(color(SINGLE.v) + padEndVisible(` ${cl}`, W) + color(SINGLE.v));
59
+ }
60
+
61
+ lines.push(color(SINGLE.v) + padEndVisible('', W) + color(SINGLE.v));
62
+
63
+ // Actions
64
+ if (note.extractedActions?.length > 0) {
65
+ lines.push(color(SINGLE.v) + padEndVisible(sep, W) + color(SINGLE.v));
66
+ lines.push(color(SINGLE.v) + padEndVisible('', W) + color(SINGLE.v));
67
+ lines.push(color(SINGLE.v) + padEndVisible(` ${style.bold('ACTIONS')}`, W) + color(SINGLE.v));
68
+ for (const a of note.extractedActions) {
69
+ const wrapped = wrapText(`\u2192 ${a}`, W - 4);
70
+ for (let i = 0; i < wrapped.length; i++) {
71
+ const prefix = i === 0 ? ' ' : ' ';
72
+ lines.push(color(SINGLE.v) + padEndVisible(`${prefix}${wrapped[i]}`, W) + color(SINGLE.v));
73
+ }
74
+ }
75
+ lines.push(color(SINGLE.v) + padEndVisible('', W) + color(SINGLE.v));
76
+ }
77
+
78
+ // Tags
79
+ if (note.tags?.length > 0) {
80
+ const tagsRaw = note.tags.map(t => `# ${t}`).join(' ');
81
+ for (const tl of wrapText(`TAGS ${tagsRaw}`, W - 4)) {
82
+ lines.push(color(SINGLE.v) + padEndVisible(` ${tl}`, W) + color(SINGLE.v));
83
+ }
84
+ lines.push(color(SINGLE.v) + padEndVisible('', W) + color(SINGLE.v));
85
+ }
86
+
87
+ // Footer keybindings
88
+ const footerStr = ' [\u2190 Retour] [\u2191 Precedente] [\u2193 Suivante] [c CR parent]';
89
+ lines.push(color(SINGLE.v) + padEndVisible(footerStr, W) + color(SINGLE.v));
90
+ lines.push(color(SINGLE.bl + SINGLE.h.repeat(W) + SINGLE.br));
91
+
92
+ return lines;
93
+ }
94
+
95
+ function buildCrDetailLines(cr) {
96
+ const agentShort = (cr.agentId || '').replace(/^Agent_/i, '');
97
+ const W = CARD_WIDTH;
98
+ const dbl = (ch) => style.nemesisAccent(ch);
99
+ const lines = [];
100
+
101
+ // Header
102
+ const headerText = ` ${cr.id || ''} `;
103
+ const headerFill = DOUBLE.h.repeat(Math.max(0, W - stringWidth(headerText) - 1));
104
+ lines.push(dbl(DOUBLE.tl + DOUBLE.h) + dbl(headerText) + dbl(headerFill + DOUBLE.tr));
105
+
106
+ lines.push(dbl(DOUBLE.v) + padEndVisible('', W) + dbl(DOUBLE.v));
107
+
108
+ // Title
109
+ lines.push(dbl(DOUBLE.v) + padEndVisible(` ${style.bold(cr.title || 'CR sans titre')}`, W) + dbl(DOUBLE.v));
110
+
111
+ // Meta
112
+ const dateStr = formatDate(cr.period?.from);
113
+ const timeFrom = formatTime(cr.period?.from);
114
+ const timeTo = formatTime(cr.period?.to);
115
+ const meta = ` ${dateStr} \u00b7 ${timeFrom} \u2192 ${timeTo} \u00b7 ${cr.noteCount || 0} notes \u00b7 ${cr.trigger || '-'}`;
116
+ lines.push(dbl(DOUBLE.v) + padEndVisible(meta, W) + dbl(DOUBLE.v));
117
+
118
+ // Agent + previous CR
119
+ let agentLine = ` ${agentShort}`;
120
+ if (cr.previousCrId) agentLine += ` \u00b7 Precedent : ${cr.previousCrId}`;
121
+ lines.push(dbl(DOUBLE.v) + padEndVisible(agentLine, W) + dbl(DOUBLE.v));
122
+
123
+ lines.push(dbl(DOUBLE.v) + padEndVisible('', W) + dbl(DOUBLE.v));
124
+
125
+ // Section helper
126
+ function addSection(title, content) {
127
+ const sepLine = ` \u2500\u2500 ${title} ` + '\u2500'.repeat(Math.max(0, W - stringWidth(` \u2500\u2500 ${title} `) - 2)) + ' ';
128
+ lines.push(dbl(DOUBLE.v) + padEndVisible(sepLine, W) + dbl(DOUBLE.v));
129
+ if (Array.isArray(content)) {
130
+ for (const item of content) {
131
+ const wrapped = wrapText(item, W - 4);
132
+ for (let i = 0; i < wrapped.length; i++) {
133
+ const prefix = i === 0 ? ' ' : ' ';
134
+ lines.push(dbl(DOUBLE.v) + padEndVisible(`${prefix}${wrapped[i]}`, W) + dbl(DOUBLE.v));
135
+ }
136
+ }
137
+ } else {
138
+ for (const cl of wrapText(content, W - 4)) {
139
+ lines.push(dbl(DOUBLE.v) + padEndVisible(` ${cl}`, W) + dbl(DOUBLE.v));
140
+ }
141
+ }
142
+ lines.push(dbl(DOUBLE.v) + padEndVisible('', W) + dbl(DOUBLE.v));
143
+ }
144
+
145
+ // CONTEXTE PRECEDENT
146
+ if (cr.previousCrSummary) {
147
+ addSection('CONTEXTE PRECEDENT', cr.previousCrSummary);
148
+ }
149
+
150
+ // SYNTHESE
151
+ if (cr.summary) {
152
+ addSection('SYNTHESE', cr.summary);
153
+ }
154
+
155
+ // DECISIONS
156
+ if (cr.decisions?.length > 0) {
157
+ addSection('DECISIONS', cr.decisions.map(d => `\u2022 ${d}`));
158
+ }
159
+
160
+ // ACTIONS EN ATTENTE
161
+ if (cr.actions?.length > 0) {
162
+ addSection('ACTIONS EN ATTENTE', cr.actions.map(a => `\u2022 ${a}`));
163
+ }
164
+
165
+ // RECONTEXTUALISATION
166
+ if (cr.recontextualization) {
167
+ addSection('RECONTEXTUALISATION', cr.recontextualization);
168
+ }
169
+
170
+ // Footer keybindings
171
+ const footerStr = ' [\u2190 Retour] [n notes liees] [p CR prec.] [s CR suiv.]';
172
+ lines.push(dbl(DOUBLE.v) + padEndVisible(footerStr, W) + dbl(DOUBLE.v));
173
+ lines.push(dbl(DOUBLE.bl + DOUBLE.h.repeat(W) + DOUBLE.br));
174
+
175
+ return lines;
176
+ }
177
+
178
+ export { buildNoteDetailLines, buildCrDetailLines };
179
+
180
+ export async function renderNoteDetail(note, _context = {}) {
181
+ const { stdin, stdout } = process;
182
+ if (!stdin.isTTY) return { action: 'back' };
183
+
184
+ return new Promise(resolve => {
185
+ // Clear screen
186
+ stdout.write('\x1b[2J\x1b[H');
187
+
188
+ const lines = buildNoteDetailLines(note);
189
+ for (const l of lines) stdout.write(l + '\n');
190
+
191
+ emitKeypressEvents(stdin);
192
+ stdin.setRawMode(true);
193
+ stdin.resume();
194
+ stdin.setEncoding('utf8');
195
+
196
+ function cleanup() {
197
+ stdin.removeListener('keypress', onKeypress);
198
+ stdin.setRawMode(false);
199
+ stdin.pause();
200
+ }
201
+
202
+ function onKeypress(ch, key) {
203
+ if (!key) return;
204
+
205
+ if (key.ctrl && key.name === 'c') {
206
+ cleanup();
207
+ resolve({ action: 'back' });
208
+ return;
209
+ }
210
+
211
+ if (key.name === 'escape' || key.name === 'backspace' || (ch === 'q' && !key.ctrl && !key.meta)) {
212
+ cleanup();
213
+ resolve({ action: 'back' });
214
+ return;
215
+ }
216
+
217
+ if (key.name === 'up' || ch === 'k') {
218
+ cleanup();
219
+ resolve({ action: 'prev' });
220
+ return;
221
+ }
222
+
223
+ if (key.name === 'down' || ch === 'j') {
224
+ cleanup();
225
+ resolve({ action: 'next' });
226
+ return;
227
+ }
228
+
229
+ if (ch === 'c' && !key.ctrl && !key.meta) {
230
+ cleanup();
231
+ resolve({ action: 'crParent', data: note.previousCrId });
232
+ return;
233
+ }
234
+ }
235
+
236
+ stdin.on('keypress', onKeypress);
237
+ });
238
+ }
239
+
240
+ export async function renderCrDetail(cr, _context = {}) {
241
+ const { stdin, stdout } = process;
242
+ if (!stdin.isTTY) return { action: 'back' };
243
+
244
+ return new Promise(resolve => {
245
+ stdout.write('\x1b[2J\x1b[H');
246
+
247
+ const lines = buildCrDetailLines(cr);
248
+ for (const l of lines) stdout.write(l + '\n');
249
+
250
+ emitKeypressEvents(stdin);
251
+ stdin.setRawMode(true);
252
+ stdin.resume();
253
+ stdin.setEncoding('utf8');
254
+
255
+ function cleanup() {
256
+ stdin.removeListener('keypress', onKeypress);
257
+ stdin.setRawMode(false);
258
+ stdin.pause();
259
+ }
260
+
261
+ function onKeypress(ch, key) {
262
+ if (!key) return;
263
+
264
+ if (key.ctrl && key.name === 'c') {
265
+ cleanup();
266
+ resolve({ action: 'back' });
267
+ return;
268
+ }
269
+
270
+ if (key.name === 'escape' || key.name === 'backspace' || (ch === 'q' && !key.ctrl && !key.meta)) {
271
+ cleanup();
272
+ resolve({ action: 'back' });
273
+ return;
274
+ }
275
+
276
+ if (ch === 'n' && !key.ctrl && !key.meta) {
277
+ cleanup();
278
+ resolve({ action: 'notes', data: cr.noteIds });
279
+ return;
280
+ }
281
+
282
+ if (ch === 'p' && !key.ctrl && !key.meta) {
283
+ cleanup();
284
+ resolve({ action: 'prevCr', data: cr.previousCrId });
285
+ return;
286
+ }
287
+
288
+ if (ch === 's' && !key.ctrl && !key.meta) {
289
+ cleanup();
290
+ resolve({ action: 'nextCr' });
291
+ return;
292
+ }
293
+ }
294
+
295
+ stdin.on('keypress', onKeypress);
296
+ });
297
+ }
@@ -0,0 +1,252 @@
1
+ import { emitKeypressEvents } from 'node:readline';
2
+ import { style } from './colors.js';
3
+ import { levelColor } from './note-colors.js';
4
+
5
+ const LEVELS = ['L1', 'L2', 'L3', 'L4', 'L5'];
6
+ const TYPE_OPTIONS = [null, 'NOTE', 'CR'];
7
+ const CONTENT_OPTIONS = [null, 'decisions', 'actions'];
8
+
9
+ export function formatFilterSummary(filters) {
10
+ const parts = [];
11
+ if (filters.type) parts.push(filters.type);
12
+ if (filters.levels?.length > 0 && filters.levels.length < 5) {
13
+ parts.push(filters.levels.join(' '));
14
+ }
15
+ if (filters.tags?.length > 0) parts.push(filters.tags.map(t => `#${t}`).join(' '));
16
+ if (filters.sessionId) parts.push(`session ${filters.sessionId.slice(0, 8)}`);
17
+ if (filters.content) parts.push(filters.content);
18
+ return parts.length > 0 ? parts.join(' \u00b7 ') : 'aucun';
19
+ }
20
+
21
+ export async function renderFilterPanel(currentFilters, availableTags = [], availableSessions = []) {
22
+ const { stdin, stdout } = process;
23
+ if (!stdin.isTTY) return { action: 'cancel', filters: currentFilters };
24
+
25
+ return new Promise(resolve => {
26
+ let focusRow = 0; // 0=type, 1=level, 2=tag, 3=session, 4=contenu
27
+ const ROWS = 5;
28
+
29
+ // Draft state
30
+ const draft = {
31
+ type: currentFilters.type || null,
32
+ levels: [...(currentFilters.levels || [])],
33
+ tags: [...(currentFilters.tags || [])],
34
+ sessionId: currentFilters.sessionId || null,
35
+ content: currentFilters.content || null,
36
+ };
37
+
38
+ // Cursor within multi-select rows
39
+ let levelCursor = 0;
40
+ let tagCursor = 0;
41
+
42
+ emitKeypressEvents(stdin);
43
+ stdin.setRawMode(true);
44
+ stdin.resume();
45
+ stdin.setEncoding('utf8');
46
+
47
+ // Clear screen before initial render to avoid stacking
48
+ stdout.write('\x1b[2J\x1b[H');
49
+ let lastLineCount = 0;
50
+
51
+ function draw() {
52
+ const lines = [];
53
+ lines.push(` ${style.bold('Filtres')}`);
54
+ lines.push('');
55
+
56
+ // TYPE
57
+ const typeIdx = TYPE_OPTIONS.indexOf(draft.type);
58
+ const typeLabels = TYPE_OPTIONS.map((t, i) => {
59
+ const label = t || 'Tous';
60
+ return i === typeIdx ? style.bold(`\u276f ${label}`) : ` ${label}`;
61
+ }).join(' ');
62
+ lines.push(focusRow === 0
63
+ ? ` ${style.bold('TYPE')} ${typeLabels}`
64
+ : ` ${style.dim('TYPE')} ${typeLabels}`);
65
+ lines.push('');
66
+
67
+ // NIVEAU
68
+ const levelLabels = LEVELS.map((l, i) => {
69
+ const checked = draft.levels.includes(l);
70
+ const icon = checked ? '\u2611' : '\u2610';
71
+ const colored = levelColor(l, `${icon} ${l}`);
72
+ if (focusRow === 1 && i === levelCursor) return style.bold(colored);
73
+ return colored;
74
+ }).join(' ');
75
+ lines.push(focusRow === 1
76
+ ? ` ${style.bold('NIVEAU')} ${levelLabels}`
77
+ : ` ${style.dim('NIVEAU')} ${levelLabels}`);
78
+ lines.push('');
79
+
80
+ // TAG
81
+ if (availableTags.length > 0) {
82
+ const tagLabels = availableTags.map((t, i) => {
83
+ const checked = draft.tags.includes(t);
84
+ const icon = checked ? '\u2611' : '\u2610';
85
+ if (focusRow === 2 && i === tagCursor) return style.bold(`${icon} ${t}`);
86
+ return `${icon} ${t}`;
87
+ }).join(' ');
88
+ lines.push(focusRow === 2
89
+ ? ` ${style.bold('TAG')} ${tagLabels}`
90
+ : ` ${style.dim('TAG')} ${tagLabels}`);
91
+ } else {
92
+ lines.push(` ${style.dim('TAG')} ${style.dim('(aucun tag)')}`);
93
+ }
94
+ lines.push('');
95
+
96
+ // SESSION
97
+ const sessOptions = [null, ...availableSessions];
98
+ const sessIdx = sessOptions.indexOf(draft.sessionId);
99
+ const sessLabels = sessOptions.map((s, i) => {
100
+ const label = s ? s.slice(0, 8) : 'Toutes';
101
+ return i === sessIdx ? style.bold(`\u276f ${label}`) : ` ${label}`;
102
+ }).join(' ');
103
+ lines.push(focusRow === 3
104
+ ? ` ${style.bold('SESSION')} ${sessLabels}`
105
+ : ` ${style.dim('SESSION')} ${sessLabels}`);
106
+ lines.push('');
107
+
108
+ // CONTENU
109
+ const contIdx = CONTENT_OPTIONS.indexOf(draft.content);
110
+ const contLabels = CONTENT_OPTIONS.map((c, i) => {
111
+ const label = c ? (c === 'decisions' ? 'Decisions' : 'Actions') : 'Tous';
112
+ return i === contIdx ? style.bold(`\u276f ${label}`) : ` ${label}`;
113
+ }).join(' ');
114
+ lines.push(focusRow === 4
115
+ ? ` ${style.bold('CONTENU')} ${contLabels}`
116
+ : ` ${style.dim('CONTENU')} ${contLabels}`);
117
+ lines.push('');
118
+
119
+ // Footer
120
+ lines.push(` ${style.dim('[Entree appliquer] [r reinitialiser] [Esc annuler]')}`);
121
+
122
+ return lines;
123
+ }
124
+
125
+ function render() {
126
+ if (lastLineCount > 0) {
127
+ stdout.write(`\x1b[${lastLineCount}A\x1b[J`);
128
+ }
129
+ const lines = draw();
130
+ for (const l of lines) stdout.write(l + '\n');
131
+ lastLineCount = lines.length;
132
+ }
133
+
134
+ function cleanup() {
135
+ stdin.removeListener('keypress', onKeypress);
136
+ stdin.setRawMode(false);
137
+ stdin.pause();
138
+ }
139
+
140
+ function buildFilters() {
141
+ const f = {};
142
+ if (draft.type) f.type = draft.type;
143
+ if (draft.levels.length > 0 && draft.levels.length < 5) f.levels = [...draft.levels];
144
+ if (draft.tags.length > 0) f.tags = [...draft.tags];
145
+ if (draft.sessionId) f.sessionId = draft.sessionId;
146
+ if (draft.content) f.content = draft.content;
147
+ return f;
148
+ }
149
+
150
+ function onKeypress(ch, key) {
151
+ if (!key) return;
152
+
153
+ if (key.ctrl && key.name === 'c') {
154
+ cleanup();
155
+ resolve({ action: 'cancel', filters: currentFilters });
156
+ return;
157
+ }
158
+
159
+ if (key.name === 'escape') {
160
+ cleanup();
161
+ resolve({ action: 'cancel', filters: currentFilters });
162
+ return;
163
+ }
164
+
165
+ if (key.name === 'return') {
166
+ cleanup();
167
+ resolve({ action: 'apply', filters: buildFilters() });
168
+ return;
169
+ }
170
+
171
+ if (ch === 'r' && !key.ctrl && !key.meta) {
172
+ cleanup();
173
+ resolve({ action: 'reset', filters: {} });
174
+ return;
175
+ }
176
+
177
+ // Navigate rows
178
+ if (key.name === 'up' || ch === 'k') {
179
+ focusRow = (focusRow - 1 + ROWS) % ROWS;
180
+ render();
181
+ return;
182
+ }
183
+
184
+ if (key.name === 'down' || ch === 'j') {
185
+ focusRow = (focusRow + 1) % ROWS;
186
+ render();
187
+ return;
188
+ }
189
+
190
+ // Cycle / toggle within row
191
+ if (key.name === 'left' || ch === 'h') {
192
+ if (focusRow === 0) {
193
+ const idx = TYPE_OPTIONS.indexOf(draft.type);
194
+ draft.type = TYPE_OPTIONS[(idx - 1 + TYPE_OPTIONS.length) % TYPE_OPTIONS.length];
195
+ } else if (focusRow === 1) {
196
+ levelCursor = (levelCursor - 1 + LEVELS.length) % LEVELS.length;
197
+ } else if (focusRow === 2 && availableTags.length > 0) {
198
+ tagCursor = (tagCursor - 1 + availableTags.length) % availableTags.length;
199
+ } else if (focusRow === 3) {
200
+ const sessOptions = [null, ...availableSessions];
201
+ const idx = sessOptions.indexOf(draft.sessionId);
202
+ draft.sessionId = sessOptions[(idx - 1 + sessOptions.length) % sessOptions.length];
203
+ } else if (focusRow === 4) {
204
+ const idx = CONTENT_OPTIONS.indexOf(draft.content);
205
+ draft.content = CONTENT_OPTIONS[(idx - 1 + CONTENT_OPTIONS.length) % CONTENT_OPTIONS.length];
206
+ }
207
+ render();
208
+ return;
209
+ }
210
+
211
+ if (key.name === 'right' || ch === 'l') {
212
+ if (focusRow === 0) {
213
+ const idx = TYPE_OPTIONS.indexOf(draft.type);
214
+ draft.type = TYPE_OPTIONS[(idx + 1) % TYPE_OPTIONS.length];
215
+ } else if (focusRow === 1) {
216
+ levelCursor = (levelCursor + 1) % LEVELS.length;
217
+ } else if (focusRow === 2 && availableTags.length > 0) {
218
+ tagCursor = (tagCursor + 1) % availableTags.length;
219
+ } else if (focusRow === 3) {
220
+ const sessOptions = [null, ...availableSessions];
221
+ const idx = sessOptions.indexOf(draft.sessionId);
222
+ draft.sessionId = sessOptions[(idx + 1) % sessOptions.length];
223
+ } else if (focusRow === 4) {
224
+ const idx = CONTENT_OPTIONS.indexOf(draft.content);
225
+ draft.content = CONTENT_OPTIONS[(idx + 1) % CONTENT_OPTIONS.length];
226
+ }
227
+ render();
228
+ return;
229
+ }
230
+
231
+ // Space = toggle for multi-select (level, tag)
232
+ if (key.name === 'space') {
233
+ if (focusRow === 1) {
234
+ const lvl = LEVELS[levelCursor];
235
+ const idx = draft.levels.indexOf(lvl);
236
+ if (idx >= 0) draft.levels.splice(idx, 1);
237
+ else draft.levels.push(lvl);
238
+ } else if (focusRow === 2 && availableTags.length > 0) {
239
+ const tag = availableTags[tagCursor];
240
+ const idx = draft.tags.indexOf(tag);
241
+ if (idx >= 0) draft.tags.splice(idx, 1);
242
+ else draft.tags.push(tag);
243
+ }
244
+ render();
245
+ return;
246
+ }
247
+ }
248
+
249
+ render();
250
+ stdin.on('keypress', onKeypress);
251
+ });
252
+ }