agileflow 2.89.3 → 2.90.1

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 (37) hide show
  1. package/CHANGELOG.md +10 -0
  2. package/lib/placeholder-registry.js +617 -0
  3. package/lib/smart-json-file.js +228 -1
  4. package/lib/table-formatter.js +519 -0
  5. package/lib/transient-status.js +374 -0
  6. package/lib/ui-manager.js +612 -0
  7. package/lib/validate-args.js +213 -0
  8. package/lib/validate-names.js +143 -0
  9. package/lib/validate-paths.js +434 -0
  10. package/lib/validate.js +37 -737
  11. package/package.json +3 -1
  12. package/scripts/check-update.js +17 -3
  13. package/scripts/lib/sessionRegistry.js +678 -0
  14. package/scripts/session-manager.js +77 -10
  15. package/scripts/tui/App.js +151 -0
  16. package/scripts/tui/index.js +31 -0
  17. package/scripts/tui/lib/crashRecovery.js +304 -0
  18. package/scripts/tui/lib/eventStream.js +309 -0
  19. package/scripts/tui/lib/keyboard.js +261 -0
  20. package/scripts/tui/lib/loopControl.js +371 -0
  21. package/scripts/tui/panels/OutputPanel.js +242 -0
  22. package/scripts/tui/panels/SessionPanel.js +170 -0
  23. package/scripts/tui/panels/TracePanel.js +298 -0
  24. package/scripts/tui/simple-tui.js +390 -0
  25. package/tools/cli/commands/config.js +7 -31
  26. package/tools/cli/commands/doctor.js +28 -39
  27. package/tools/cli/commands/list.js +47 -35
  28. package/tools/cli/commands/status.js +20 -38
  29. package/tools/cli/commands/tui.js +59 -0
  30. package/tools/cli/commands/uninstall.js +12 -39
  31. package/tools/cli/installers/core/installer.js +13 -0
  32. package/tools/cli/lib/command-context.js +382 -0
  33. package/tools/cli/lib/config-manager.js +394 -0
  34. package/tools/cli/lib/ide-registry.js +186 -0
  35. package/tools/cli/lib/npm-utils.js +17 -3
  36. package/tools/cli/lib/self-update.js +148 -0
  37. package/tools/cli/lib/validation-middleware.js +491 -0
@@ -0,0 +1,309 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * Event Stream - Real-time event monitoring from agent bus
5
+ *
6
+ * Watches docs/09-agents/bus/log.jsonl for new events and emits them
7
+ * to subscribers. Handles file rotation and truncation gracefully.
8
+ */
9
+
10
+ const fs = require('fs');
11
+ const path = require('path');
12
+ const EventEmitter = require('events');
13
+
14
+ // Get project root
15
+ let getProjectRoot;
16
+ try {
17
+ getProjectRoot = require('../../../lib/paths').getProjectRoot;
18
+ } catch (e) {
19
+ // Fallback
20
+ getProjectRoot = () => process.cwd();
21
+ }
22
+
23
+ /**
24
+ * EventStream class - watches agent bus log and emits events
25
+ */
26
+ class EventStream extends EventEmitter {
27
+ constructor(options = {}) {
28
+ super();
29
+
30
+ this.options = {
31
+ // Path to log file (defaults to agent bus)
32
+ logPath:
33
+ options.logPath || path.join(getProjectRoot(), 'docs', '09-agents', 'bus', 'log.jsonl'),
34
+ // Polling interval in ms (fallback if fs.watch fails)
35
+ pollInterval: options.pollInterval || 1000,
36
+ // Maximum events to keep in buffer
37
+ maxBufferSize: options.maxBufferSize || 100,
38
+ // Whether to emit historical events on start
39
+ emitHistory: options.emitHistory || false,
40
+ // How many historical events to emit
41
+ historyLimit: options.historyLimit || 10,
42
+ };
43
+
44
+ this.buffer = [];
45
+ this.filePosition = 0;
46
+ this.watcher = null;
47
+ this.pollTimer = null;
48
+ this.isWatching = false;
49
+ }
50
+
51
+ /**
52
+ * Start watching the log file
53
+ */
54
+ start() {
55
+ if (this.isWatching) return;
56
+
57
+ // Check if file exists
58
+ if (!fs.existsSync(this.options.logPath)) {
59
+ this.emit('error', new Error(`Log file not found: ${this.options.logPath}`));
60
+ // Continue anyway - file might be created later
61
+ }
62
+
63
+ // Get initial file size
64
+ this._updateFilePosition();
65
+
66
+ // Emit historical events if requested
67
+ if (this.options.emitHistory) {
68
+ this._emitHistory();
69
+ }
70
+
71
+ // Try to use fs.watch (more efficient)
72
+ try {
73
+ this.watcher = fs.watch(this.options.logPath, eventType => {
74
+ if (eventType === 'change') {
75
+ this._processNewLines();
76
+ }
77
+ });
78
+
79
+ this.watcher.on('error', err => {
80
+ this.emit('error', err);
81
+ // Fall back to polling
82
+ this._startPolling();
83
+ });
84
+ } catch (e) {
85
+ // Fall back to polling
86
+ this._startPolling();
87
+ }
88
+
89
+ this.isWatching = true;
90
+ this.emit('started');
91
+ }
92
+
93
+ /**
94
+ * Stop watching the log file
95
+ */
96
+ stop() {
97
+ if (!this.isWatching) return;
98
+
99
+ if (this.watcher) {
100
+ this.watcher.close();
101
+ this.watcher = null;
102
+ }
103
+
104
+ if (this.pollTimer) {
105
+ clearInterval(this.pollTimer);
106
+ this.pollTimer = null;
107
+ }
108
+
109
+ this.isWatching = false;
110
+ this.emit('stopped');
111
+ }
112
+
113
+ /**
114
+ * Start polling as fallback
115
+ */
116
+ _startPolling() {
117
+ if (this.pollTimer) return;
118
+
119
+ this.pollTimer = setInterval(() => {
120
+ this._processNewLines();
121
+ }, this.options.pollInterval);
122
+ }
123
+
124
+ /**
125
+ * Update tracked file position
126
+ */
127
+ _updateFilePosition() {
128
+ try {
129
+ if (fs.existsSync(this.options.logPath)) {
130
+ const stats = fs.statSync(this.options.logPath);
131
+ this.filePosition = stats.size;
132
+ }
133
+ } catch (e) {
134
+ // Ignore errors
135
+ }
136
+ }
137
+
138
+ /**
139
+ * Emit historical events from file start
140
+ */
141
+ _emitHistory() {
142
+ try {
143
+ if (!fs.existsSync(this.options.logPath)) return;
144
+
145
+ const content = fs.readFileSync(this.options.logPath, 'utf8');
146
+ const lines = content.trim().split('\n').filter(Boolean);
147
+
148
+ // Get last N lines
149
+ const historyLines = lines.slice(-this.options.historyLimit);
150
+
151
+ for (const line of historyLines) {
152
+ try {
153
+ const event = JSON.parse(line);
154
+ this._addToBuffer(event);
155
+ this.emit('event', event);
156
+ } catch (e) {
157
+ // Skip invalid JSON
158
+ }
159
+ }
160
+ } catch (e) {
161
+ this.emit('error', e);
162
+ }
163
+ }
164
+
165
+ /**
166
+ * Process new lines added to the file
167
+ */
168
+ _processNewLines() {
169
+ try {
170
+ if (!fs.existsSync(this.options.logPath)) return;
171
+
172
+ const stats = fs.statSync(this.options.logPath);
173
+
174
+ // Handle file truncation (rotation)
175
+ if (stats.size < this.filePosition) {
176
+ this.filePosition = 0;
177
+ this.emit('truncated');
178
+ }
179
+
180
+ // No new content
181
+ if (stats.size <= this.filePosition) return;
182
+
183
+ // Read new content
184
+ const fd = fs.openSync(this.options.logPath, 'r');
185
+ const bufferSize = stats.size - this.filePosition;
186
+ const buffer = Buffer.alloc(bufferSize);
187
+
188
+ fs.readSync(fd, buffer, 0, bufferSize, this.filePosition);
189
+ fs.closeSync(fd);
190
+
191
+ // Update position
192
+ this.filePosition = stats.size;
193
+
194
+ // Process lines
195
+ const content = buffer.toString('utf8');
196
+ const lines = content.split('\n').filter(Boolean);
197
+
198
+ for (const line of lines) {
199
+ try {
200
+ const event = JSON.parse(line);
201
+ this._addToBuffer(event);
202
+ this.emit('event', event);
203
+ } catch (e) {
204
+ // Skip invalid JSON lines
205
+ }
206
+ }
207
+ } catch (e) {
208
+ this.emit('error', e);
209
+ }
210
+ }
211
+
212
+ /**
213
+ * Add event to buffer (with size limit)
214
+ */
215
+ _addToBuffer(event) {
216
+ this.buffer.push(event);
217
+
218
+ // Trim buffer if too large
219
+ while (this.buffer.length > this.options.maxBufferSize) {
220
+ this.buffer.shift();
221
+ }
222
+ }
223
+
224
+ /**
225
+ * Get buffered events
226
+ */
227
+ getBuffer() {
228
+ return [...this.buffer];
229
+ }
230
+
231
+ /**
232
+ * Clear buffer
233
+ */
234
+ clearBuffer() {
235
+ this.buffer = [];
236
+ }
237
+
238
+ /**
239
+ * Get events by type
240
+ */
241
+ getEventsByType(type) {
242
+ return this.buffer.filter(e => e.type === type);
243
+ }
244
+
245
+ /**
246
+ * Get events by agent
247
+ */
248
+ getEventsByAgent(agent) {
249
+ return this.buffer.filter(e => e.agent === agent);
250
+ }
251
+ }
252
+
253
+ /**
254
+ * Create a singleton event stream instance
255
+ */
256
+ let defaultStream = null;
257
+
258
+ function getDefaultStream() {
259
+ if (!defaultStream) {
260
+ defaultStream = new EventStream();
261
+ }
262
+ return defaultStream;
263
+ }
264
+
265
+ /**
266
+ * Format event for display
267
+ */
268
+ function formatEvent(event) {
269
+ const timestamp = event.timestamp ? new Date(event.timestamp).toLocaleTimeString() : '';
270
+
271
+ const agent = event.agent || 'unknown';
272
+ const eventType = event.event || event.type || 'unknown';
273
+
274
+ let message = '';
275
+
276
+ switch (event.event) {
277
+ case 'init':
278
+ message = `Loop started: gate=${event.gate}, max=${event.max_iterations}`;
279
+ break;
280
+ case 'iteration':
281
+ message = `Iteration ${event.iter}: value=${event.value}, passed=${event.passed}`;
282
+ break;
283
+ case 'passed':
284
+ message = `Loop passed! final=${event.final_value}`;
285
+ break;
286
+ case 'failed':
287
+ message = `Loop failed: ${event.reason}, final=${event.final_value}`;
288
+ break;
289
+ case 'abort':
290
+ message = `Loop aborted: ${event.reason}`;
291
+ break;
292
+ default:
293
+ message = JSON.stringify(event);
294
+ }
295
+
296
+ return {
297
+ timestamp,
298
+ agent,
299
+ eventType,
300
+ message,
301
+ raw: event,
302
+ };
303
+ }
304
+
305
+ module.exports = {
306
+ EventStream,
307
+ getDefaultStream,
308
+ formatEvent,
309
+ };
@@ -0,0 +1,261 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * Keyboard Handler - Key bindings for TUI
5
+ *
6
+ * Provides centralized keyboard handling with configurable bindings
7
+ * and event emission for TUI components.
8
+ */
9
+
10
+ const EventEmitter = require('events');
11
+
12
+ /**
13
+ * Default key bindings
14
+ */
15
+ const DEFAULT_BINDINGS = {
16
+ quit: { key: 'q', description: 'Quit TUI' },
17
+ start: { key: 's', description: 'Start loop' },
18
+ pause: { key: 'p', description: 'Pause loop' },
19
+ resume: { key: 'r', description: 'Resume loop' },
20
+ trace: { key: 't', description: 'Toggle trace' },
21
+ help: { key: '?', description: 'Show help' },
22
+ // Session switching (1-9)
23
+ session1: { key: '1', description: 'Session 1' },
24
+ session2: { key: '2', description: 'Session 2' },
25
+ session3: { key: '3', description: 'Session 3' },
26
+ session4: { key: '4', description: 'Session 4' },
27
+ session5: { key: '5', description: 'Session 5' },
28
+ session6: { key: '6', description: 'Session 6' },
29
+ session7: { key: '7', description: 'Session 7' },
30
+ session8: { key: '8', description: 'Session 8' },
31
+ session9: { key: '9', description: 'Session 9' },
32
+ };
33
+
34
+ /**
35
+ * KeyboardHandler class - centralized keyboard handling
36
+ */
37
+ class KeyboardHandler extends EventEmitter {
38
+ constructor(options = {}) {
39
+ super();
40
+
41
+ this.bindings = { ...DEFAULT_BINDINGS, ...options.bindings };
42
+ this.enabled = true;
43
+ this.keyHistory = [];
44
+ this.historyLimit = options.historyLimit || 50;
45
+ }
46
+
47
+ /**
48
+ * Process a key press
49
+ * @param {string} input - The key character
50
+ * @param {object} key - The key object from useInput
51
+ * @returns {object|null} - Action performed or null
52
+ */
53
+ processKey(input, key = {}) {
54
+ if (!this.enabled) return null;
55
+
56
+ // Record key press
57
+ this._recordKey(input, key);
58
+
59
+ // Handle Ctrl+C for quit
60
+ if (key.ctrl && input === 'c') {
61
+ const action = { action: 'quit', key: 'ctrl+c' };
62
+ this.emit('action', action);
63
+ this.emit('quit');
64
+ return action;
65
+ }
66
+
67
+ // Handle escape for quit
68
+ if (key.escape) {
69
+ const action = { action: 'quit', key: 'escape' };
70
+ this.emit('action', action);
71
+ this.emit('quit');
72
+ return action;
73
+ }
74
+
75
+ // Normalize input to lowercase for matching
76
+ const normalizedInput = input.toLowerCase();
77
+
78
+ // Check against bindings
79
+ for (const [actionName, binding] of Object.entries(this.bindings)) {
80
+ if (binding.key === normalizedInput || binding.key === input) {
81
+ const action = { action: actionName, key: input };
82
+ this.emit('action', action);
83
+ this.emit(actionName, action);
84
+
85
+ // Special handling for session switching
86
+ if (actionName.startsWith('session')) {
87
+ const sessionNum = parseInt(actionName.replace('session', ''), 10);
88
+ this.emit('sessionSwitch', { session: sessionNum });
89
+ }
90
+
91
+ return action;
92
+ }
93
+ }
94
+
95
+ // Unknown key
96
+ this.emit('unknownKey', { key: input });
97
+ return null;
98
+ }
99
+
100
+ /**
101
+ * Record key press to history
102
+ */
103
+ _recordKey(input, key) {
104
+ this.keyHistory.push({
105
+ input,
106
+ key,
107
+ timestamp: Date.now(),
108
+ });
109
+
110
+ // Trim history
111
+ while (this.keyHistory.length > this.historyLimit) {
112
+ this.keyHistory.shift();
113
+ }
114
+ }
115
+
116
+ /**
117
+ * Get key history
118
+ */
119
+ getHistory() {
120
+ return [...this.keyHistory];
121
+ }
122
+
123
+ /**
124
+ * Clear key history
125
+ */
126
+ clearHistory() {
127
+ this.keyHistory = [];
128
+ }
129
+
130
+ /**
131
+ * Enable keyboard handling
132
+ */
133
+ enable() {
134
+ this.enabled = true;
135
+ }
136
+
137
+ /**
138
+ * Disable keyboard handling
139
+ */
140
+ disable() {
141
+ this.enabled = false;
142
+ }
143
+
144
+ /**
145
+ * Check if a key is bound
146
+ */
147
+ isBound(key) {
148
+ return Object.values(this.bindings).some(b => b.key === key);
149
+ }
150
+
151
+ /**
152
+ * Get binding for action
153
+ */
154
+ getBinding(action) {
155
+ return this.bindings[action] || null;
156
+ }
157
+
158
+ /**
159
+ * Get all bindings
160
+ */
161
+ getBindings() {
162
+ return { ...this.bindings };
163
+ }
164
+
165
+ /**
166
+ * Set custom binding
167
+ */
168
+ setBinding(action, key, description) {
169
+ this.bindings[action] = { key, description };
170
+ }
171
+ }
172
+
173
+ /**
174
+ * Format key bindings for footer display
175
+ * @param {object} bindings - Key bindings object
176
+ * @returns {string[]} - Array of formatted binding strings
177
+ */
178
+ function formatBindings(bindings = DEFAULT_BINDINGS) {
179
+ // Primary bindings to show in footer (exclude session numbers)
180
+ const primaryBindings = ['quit', 'start', 'pause', 'resume', 'trace', 'help'];
181
+
182
+ return primaryBindings
183
+ .filter(name => bindings[name])
184
+ .map(name => {
185
+ const binding = bindings[name];
186
+ return `${binding.key.toUpperCase()}:${binding.description}`;
187
+ });
188
+ }
189
+
190
+ /**
191
+ * Format bindings as help text
192
+ * @param {object} bindings - Key bindings object
193
+ * @returns {string} - Multi-line help text
194
+ */
195
+ function formatHelp(bindings = DEFAULT_BINDINGS) {
196
+ const lines = ['Key Bindings:', ''];
197
+
198
+ // Group bindings
199
+ const groups = {
200
+ 'Loop Control': ['start', 'pause', 'resume'],
201
+ View: ['trace', 'help'],
202
+ Navigation: ['quit'],
203
+ Sessions: [
204
+ 'session1',
205
+ 'session2',
206
+ 'session3',
207
+ 'session4',
208
+ 'session5',
209
+ 'session6',
210
+ 'session7',
211
+ 'session8',
212
+ 'session9',
213
+ ],
214
+ };
215
+
216
+ for (const [groupName, actions] of Object.entries(groups)) {
217
+ lines.push(` ${groupName}:`);
218
+ for (const action of actions) {
219
+ if (bindings[action]) {
220
+ const binding = bindings[action];
221
+ lines.push(` ${binding.key.toUpperCase()} - ${binding.description}`);
222
+ }
223
+ }
224
+ lines.push('');
225
+ }
226
+
227
+ return lines.join('\n');
228
+ }
229
+
230
+ /**
231
+ * Create default keyboard handler instance
232
+ */
233
+ let defaultHandler = null;
234
+
235
+ function getDefaultHandler() {
236
+ if (!defaultHandler) {
237
+ defaultHandler = new KeyboardHandler();
238
+ }
239
+ return defaultHandler;
240
+ }
241
+
242
+ /**
243
+ * React hook factory for keyboard handling
244
+ * Returns a function that can be used with useInput
245
+ */
246
+ function createKeyHandler(handler = null) {
247
+ const kbd = handler || getDefaultHandler();
248
+
249
+ return function handleKey(input, key) {
250
+ return kbd.processKey(input, key);
251
+ };
252
+ }
253
+
254
+ module.exports = {
255
+ KeyboardHandler,
256
+ DEFAULT_BINDINGS,
257
+ formatBindings,
258
+ formatHelp,
259
+ getDefaultHandler,
260
+ createKeyHandler,
261
+ };