@compilr-dev/cli 0.4.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 (152) hide show
  1. package/README.md +110 -0
  2. package/dist/agent.d.ts +62 -0
  3. package/dist/agent.js +317 -0
  4. package/dist/agents/registry.d.ts +66 -0
  5. package/dist/agents/registry.js +238 -0
  6. package/dist/agents/types.d.ts +40 -0
  7. package/dist/agents/types.js +94 -0
  8. package/dist/commands/custom-registry.d.ts +69 -0
  9. package/dist/commands/custom-registry.js +246 -0
  10. package/dist/commands/index.d.ts +7 -0
  11. package/dist/commands/index.js +7 -0
  12. package/dist/commands/types.d.ts +31 -0
  13. package/dist/commands/types.js +26 -0
  14. package/dist/commands.d.ts +63 -0
  15. package/dist/commands.js +324 -0
  16. package/dist/db/index.d.ts +42 -0
  17. package/dist/db/index.js +146 -0
  18. package/dist/db/repositories/document-repository.d.ts +63 -0
  19. package/dist/db/repositories/document-repository.js +184 -0
  20. package/dist/db/repositories/index.d.ts +9 -0
  21. package/dist/db/repositories/index.js +6 -0
  22. package/dist/db/repositories/project-repository.d.ts +132 -0
  23. package/dist/db/repositories/project-repository.js +337 -0
  24. package/dist/db/repositories/work-item-repository.d.ts +115 -0
  25. package/dist/db/repositories/work-item-repository.js +389 -0
  26. package/dist/db/schema.d.ts +83 -0
  27. package/dist/db/schema.js +143 -0
  28. package/dist/debug.d.ts +8 -0
  29. package/dist/debug.js +48 -0
  30. package/dist/index.d.ts +2 -0
  31. package/dist/index.js +348 -0
  32. package/dist/index.old.d.ts +7 -0
  33. package/dist/index.old.js +1014 -0
  34. package/dist/repl.d.ts +121 -0
  35. package/dist/repl.js +1878 -0
  36. package/dist/settings/index.d.ts +80 -0
  37. package/dist/settings/index.js +195 -0
  38. package/dist/shared-handlers.d.ts +63 -0
  39. package/dist/shared-handlers.js +57 -0
  40. package/dist/slash-autocomplete.d.ts +41 -0
  41. package/dist/slash-autocomplete.js +638 -0
  42. package/dist/state.d.ts +75 -0
  43. package/dist/state.js +130 -0
  44. package/dist/tabbed-menu.d.ts +11 -0
  45. package/dist/tabbed-menu.js +328 -0
  46. package/dist/templates/backlog-md.d.ts +7 -0
  47. package/dist/templates/backlog-md.js +94 -0
  48. package/dist/templates/claude-md.d.ts +7 -0
  49. package/dist/templates/claude-md.js +189 -0
  50. package/dist/templates/coding-standards.d.ts +7 -0
  51. package/dist/templates/coding-standards.js +299 -0
  52. package/dist/templates/compilr-md.d.ts +7 -0
  53. package/dist/templates/compilr-md.js +189 -0
  54. package/dist/templates/config-json.d.ts +38 -0
  55. package/dist/templates/config-json.js +39 -0
  56. package/dist/templates/gitignore.d.ts +7 -0
  57. package/dist/templates/gitignore.js +85 -0
  58. package/dist/templates/index.d.ts +19 -0
  59. package/dist/templates/index.js +302 -0
  60. package/dist/templates/package-json.d.ts +7 -0
  61. package/dist/templates/package-json.js +111 -0
  62. package/dist/templates/readme-md.d.ts +7 -0
  63. package/dist/templates/readme-md.js +161 -0
  64. package/dist/templates/tsconfig.d.ts +7 -0
  65. package/dist/templates/tsconfig.js +61 -0
  66. package/dist/templates/types.d.ts +33 -0
  67. package/dist/templates/types.js +24 -0
  68. package/dist/test-autocomplete.d.ts +7 -0
  69. package/dist/test-autocomplete.js +85 -0
  70. package/dist/test-tabbed-menu.d.ts +7 -0
  71. package/dist/test-tabbed-menu.js +25 -0
  72. package/dist/themes/colors.d.ts +49 -0
  73. package/dist/themes/colors.js +135 -0
  74. package/dist/themes/index.d.ts +23 -0
  75. package/dist/themes/index.js +24 -0
  76. package/dist/themes/registry.d.ts +60 -0
  77. package/dist/themes/registry.js +195 -0
  78. package/dist/themes/types.d.ts +82 -0
  79. package/dist/themes/types.js +7 -0
  80. package/dist/tool-selector.d.ts +71 -0
  81. package/dist/tool-selector.js +184 -0
  82. package/dist/tools/ask-user-simple.d.ts +19 -0
  83. package/dist/tools/ask-user-simple.js +86 -0
  84. package/dist/tools/ask-user.d.ts +32 -0
  85. package/dist/tools/ask-user.js +113 -0
  86. package/dist/tools/backlog.d.ts +53 -0
  87. package/dist/tools/backlog.js +709 -0
  88. package/dist/tools.d.ts +15 -0
  89. package/dist/tools.js +121 -0
  90. package/dist/ui/agents-overlay.d.ts +12 -0
  91. package/dist/ui/agents-overlay.js +501 -0
  92. package/dist/ui/arch-type-overlay.d.ts +20 -0
  93. package/dist/ui/arch-type-overlay.js +229 -0
  94. package/dist/ui/ask-user-overlay.d.ts +26 -0
  95. package/dist/ui/ask-user-overlay.js +647 -0
  96. package/dist/ui/ask-user-simple-overlay.d.ts +25 -0
  97. package/dist/ui/ask-user-simple-overlay.js +242 -0
  98. package/dist/ui/backlog-overlay.d.ts +17 -0
  99. package/dist/ui/backlog-overlay.js +786 -0
  100. package/dist/ui/commands-overlay.d.ts +11 -0
  101. package/dist/ui/commands-overlay.js +410 -0
  102. package/dist/ui/config-overlay.d.ts +34 -0
  103. package/dist/ui/config-overlay.js +977 -0
  104. package/dist/ui/conversation.d.ts +82 -0
  105. package/dist/ui/conversation.js +508 -0
  106. package/dist/ui/diff.d.ts +38 -0
  107. package/dist/ui/diff.js +182 -0
  108. package/dist/ui/ephemeral.d.ts +111 -0
  109. package/dist/ui/ephemeral.js +413 -0
  110. package/dist/ui/file-autocomplete.d.ts +45 -0
  111. package/dist/ui/file-autocomplete.js +237 -0
  112. package/dist/ui/footer.d.ts +153 -0
  113. package/dist/ui/footer.js +422 -0
  114. package/dist/ui/index.d.ts +12 -0
  115. package/dist/ui/index.js +15 -0
  116. package/dist/ui/init-overlay.d.ts +24 -0
  117. package/dist/ui/init-overlay.js +525 -0
  118. package/dist/ui/input-prompt-v2.d.ts +179 -0
  119. package/dist/ui/input-prompt-v2.js +991 -0
  120. package/dist/ui/input-prompt.d.ts +97 -0
  121. package/dist/ui/input-prompt.js +800 -0
  122. package/dist/ui/iteration-limit-overlay.d.ts +21 -0
  123. package/dist/ui/iteration-limit-overlay.js +150 -0
  124. package/dist/ui/keys-overlay.d.ts +14 -0
  125. package/dist/ui/keys-overlay.js +181 -0
  126. package/dist/ui/model-warning-overlay.d.ts +30 -0
  127. package/dist/ui/model-warning-overlay.js +171 -0
  128. package/dist/ui/overlay-controller.d.ts +25 -0
  129. package/dist/ui/overlay-controller.js +35 -0
  130. package/dist/ui/overlays.d.ts +47 -0
  131. package/dist/ui/overlays.js +627 -0
  132. package/dist/ui/permission-overlay.d.ts +16 -0
  133. package/dist/ui/permission-overlay.js +494 -0
  134. package/dist/ui/terminal.d.ts +117 -0
  135. package/dist/ui/terminal.js +237 -0
  136. package/dist/ui/todo-zone.d.ts +112 -0
  137. package/dist/ui/todo-zone.js +353 -0
  138. package/dist/ui/tools-overlay.d.ts +26 -0
  139. package/dist/ui/tools-overlay.js +278 -0
  140. package/dist/ui/tutorial-overlay.d.ts +10 -0
  141. package/dist/ui/tutorial-overlay.js +936 -0
  142. package/dist/ui/types.d.ts +103 -0
  143. package/dist/ui/types.js +33 -0
  144. package/dist/utils/credentials.d.ts +55 -0
  145. package/dist/utils/credentials.js +268 -0
  146. package/dist/utils/model-tiers.d.ts +37 -0
  147. package/dist/utils/model-tiers.js +118 -0
  148. package/dist/utils/project-memory.d.ts +47 -0
  149. package/dist/utils/project-memory.js +117 -0
  150. package/dist/utils/project-status.d.ts +56 -0
  151. package/dist/utils/project-status.js +237 -0
  152. package/package.json +66 -0
@@ -0,0 +1,1014 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Agent Demo - CLI Interface
4
+ *
5
+ * Simple readline-based REPL for testing the agent.
6
+ */
7
+ import pc from 'picocolors';
8
+ import { marked } from 'marked';
9
+ import { markedTerminal } from 'marked-terminal';
10
+ import { createAgent } from './agent.js';
11
+ import { selectToolNamesByIntent, estimateTokenSavings } from './tool-selector.js';
12
+ import { createInteractiveInput } from './slash-autocomplete.js';
13
+ import { showHelpMenu } from './tabbed-menu.js';
14
+ // Configure marked for terminal output
15
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
16
+ marked.use(markedTerminal({
17
+ reflowText: true,
18
+ width: 80,
19
+ }));
20
+ /**
21
+ * Render markdown text for terminal display
22
+ */
23
+ function renderMarkdown(text) {
24
+ try {
25
+ // Preprocess: remove common leading indentation that breaks markdown parsing
26
+ // (4 spaces makes it a code block, which preserves raw **bold** markers)
27
+ const lines = text.split('\n');
28
+ const processedLines = lines.map(line => {
29
+ // Remove leading 4 spaces that would make it a code block
30
+ // But preserve intentional code blocks (``` fenced)
31
+ if (line.startsWith(' ') && !line.startsWith(' ```')) {
32
+ return line.slice(4);
33
+ }
34
+ return line;
35
+ });
36
+ const processed = processedLines.join('\n');
37
+ // marked.parse returns string synchronously with these options
38
+ let rendered = marked.parse(processed, { async: false });
39
+ // Post-process: marked-terminal doesn't render bold/italic inside list items
40
+ // Convert remaining **bold** and *italic* to ANSI codes
41
+ rendered = rendered
42
+ .replace(/\*\*([^*]+)\*\*/g, (_, text) => pc.bold(text)) // **bold**
43
+ .replace(/(?<!\*)\*([^*]+)\*(?!\*)/g, (_, text) => pc.italic(text)) // *italic* (not **)
44
+ .replace(/`([^`]+)`/g, (_, text) => pc.cyan(text)); // `code`
45
+ // Remove trailing newlines that marked adds
46
+ return rendered.replace(/\n+$/, '');
47
+ }
48
+ catch {
49
+ return text; // Fallback to raw text
50
+ }
51
+ }
52
+ // Funny thinking words (inspired by Claude Code)
53
+ const THINKING_WORDS = [
54
+ 'Thinking',
55
+ 'Pondering',
56
+ 'Contemplating',
57
+ 'Ruminating',
58
+ 'Cogitating',
59
+ 'Deliberating',
60
+ 'Musing',
61
+ 'Reflecting',
62
+ 'Considering',
63
+ 'Analyzing',
64
+ 'Processing',
65
+ 'Computing',
66
+ 'Synthesizing',
67
+ 'Extrapolating',
68
+ 'Hypothesizing',
69
+ 'Discombobulating',
70
+ 'Nebulizing',
71
+ 'Percolating',
72
+ 'Marinating',
73
+ 'Simmering',
74
+ 'Brewing',
75
+ 'Fermenting',
76
+ 'Distilling',
77
+ 'Crystallizing',
78
+ 'Transmuting',
79
+ ];
80
+ // Global spinner instance for Esc handling
81
+ let spinnerInstance = null;
82
+ /**
83
+ * Format duration in human-readable format (e.g., "1m 23s", "45s")
84
+ */
85
+ function formatDuration(ms) {
86
+ const seconds = Math.floor(ms / 1000);
87
+ if (seconds < 60) {
88
+ return `${seconds}s`;
89
+ }
90
+ const minutes = Math.floor(seconds / 60);
91
+ const remainingSeconds = seconds % 60;
92
+ return `${minutes}m ${remainingSeconds}s`;
93
+ }
94
+ /**
95
+ * Format token count (e.g., "1.2k", "500")
96
+ */
97
+ function formatTokens(tokens) {
98
+ if (tokens >= 1000) {
99
+ return `${(tokens / 1000).toFixed(1)}k`;
100
+ }
101
+ return tokens.toString();
102
+ }
103
+ /**
104
+ * Current todo list (stored globally for display during agent execution)
105
+ */
106
+ let currentTodos = [];
107
+ /**
108
+ * Render todo list with visual styling like Claude Code
109
+ */
110
+ function renderTodoList(todos) {
111
+ if (!todos || todos.length === 0) {
112
+ return '';
113
+ }
114
+ const lines = [];
115
+ for (const todo of todos) {
116
+ let checkbox;
117
+ let text;
118
+ switch (todo.status) {
119
+ case 'completed':
120
+ checkbox = pc.dim('☒');
121
+ text = pc.strikethrough(pc.dim(todo.content));
122
+ break;
123
+ case 'in_progress':
124
+ checkbox = pc.cyan('☐');
125
+ text = pc.bold(todo.content);
126
+ break;
127
+ case 'pending':
128
+ default:
129
+ checkbox = pc.dim('☐');
130
+ text = pc.dim(todo.content);
131
+ break;
132
+ }
133
+ lines.push(`${checkbox} ${text}`);
134
+ }
135
+ return lines.join('\n');
136
+ }
137
+ /**
138
+ * Get the currently active todo (in_progress)
139
+ */
140
+ function getActiveTodo() {
141
+ return currentTodos.find(t => t.status === 'in_progress');
142
+ }
143
+ /**
144
+ * Format tool result for display - handles strings, objects, arrays
145
+ */
146
+ function formatToolResult(result) {
147
+ if (result === null || result === undefined) {
148
+ return '';
149
+ }
150
+ // String result - count lines or truncate
151
+ if (typeof result === 'string') {
152
+ const lines = result.split('\n').filter(l => l.trim()).length;
153
+ if (lines > 3) {
154
+ return pc.dim(`${lines} lines`);
155
+ }
156
+ const truncated = result.slice(0, 80).replace(/\n/g, ' ');
157
+ return pc.dim(truncated + (result.length > 80 ? '...' : ''));
158
+ }
159
+ // Array result - show count and type hint
160
+ if (Array.isArray(result)) {
161
+ const len = result.length;
162
+ if (len === 0)
163
+ return pc.dim('empty array');
164
+ // Try to describe contents
165
+ const first = result[0];
166
+ if (typeof first === 'string') {
167
+ return pc.dim(`${len} items`);
168
+ }
169
+ else if (typeof first === 'object' && first !== null) {
170
+ // Check for common patterns like todos, files, branches
171
+ if ('content' in first && 'status' in first) {
172
+ return pc.dim(`${len} todo items`);
173
+ }
174
+ if ('name' in first || 'path' in first) {
175
+ return pc.dim(`${len} items`);
176
+ }
177
+ }
178
+ return pc.dim(`${len} items`);
179
+ }
180
+ // Object result - try to extract meaningful summary
181
+ if (typeof result === 'object') {
182
+ const obj = result;
183
+ // Git status pattern
184
+ if ('branch' in obj && 'clean' in obj) {
185
+ const branch = obj.branch;
186
+ const clean = obj.clean;
187
+ return pc.dim(`${branch}${clean ? ' (clean)' : ' (changes)'}`);
188
+ }
189
+ // Git diff pattern
190
+ if ('files' in obj && Array.isArray(obj.files)) {
191
+ const files = obj.files;
192
+ if (files.length === 0)
193
+ return pc.dim('no changes');
194
+ return pc.dim(`${files.length} file(s) changed`);
195
+ }
196
+ // Git branch pattern
197
+ if ('current' in obj && 'branches' in obj) {
198
+ const branches = obj.branches;
199
+ return pc.dim(`${branches?.length || 0} branches`);
200
+ }
201
+ // File read pattern
202
+ if ('content' in obj) {
203
+ const content = String(obj.content);
204
+ const lines = content.split('\n').length;
205
+ return pc.dim(`${lines} lines`);
206
+ }
207
+ // Bash/command pattern
208
+ if ('stdout' in obj || 'output' in obj) {
209
+ const out = String(obj.stdout || obj.output || '');
210
+ const lines = out.split('\n').filter(l => l.trim()).length;
211
+ if (lines > 0)
212
+ return pc.dim(`${lines} lines`);
213
+ return pc.dim('done');
214
+ }
215
+ // Success message pattern
216
+ if ('success' in obj && typeof obj.message === 'string') {
217
+ return pc.dim(obj.message.slice(0, 60));
218
+ }
219
+ // Fallback - show key count
220
+ const keys = Object.keys(obj);
221
+ if (keys.length <= 3) {
222
+ return pc.dim(keys.join(', '));
223
+ }
224
+ return pc.dim(`${keys.length} fields`);
225
+ }
226
+ // Primitive - just stringify
227
+ return pc.dim(String(result).slice(0, 60));
228
+ }
229
+ /**
230
+ * Animated thinking spinner with Knight Rider scanning effect
231
+ * Shows elapsed time, token usage, and todo list
232
+ */
233
+ class ThinkingSpinner {
234
+ word = '';
235
+ position = 0;
236
+ direction = 1; // 1 = right, -1 = left
237
+ interval = null;
238
+ abortController = null;
239
+ startTime = 0;
240
+ tokens = 0;
241
+ currentTool = null;
242
+ renderedLines = 0; // Track how many lines we've rendered
243
+ start() {
244
+ this.abortController = new AbortController();
245
+ spinnerInstance = this;
246
+ this.startTime = Date.now();
247
+ this.tokens = 0;
248
+ this.currentTool = null;
249
+ this.renderedLines = 0;
250
+ // Pick a random word
251
+ this.word = THINKING_WORDS[Math.floor(Math.random() * THINKING_WORDS.length)] + '...';
252
+ this.position = 0;
253
+ this.direction = 1;
254
+ // Start animation
255
+ this.render();
256
+ this.interval = setInterval(() => {
257
+ // Move position
258
+ this.position += this.direction;
259
+ // Bounce at edges
260
+ if (this.position >= this.word.length - 1) {
261
+ this.direction = -1;
262
+ }
263
+ else if (this.position <= 0) {
264
+ this.direction = 1;
265
+ }
266
+ this.render();
267
+ }, 150); // Smooth scanning speed
268
+ return this.abortController;
269
+ }
270
+ /**
271
+ * Update token count (called when tokens are received)
272
+ */
273
+ addTokens(count) {
274
+ this.tokens += count;
275
+ }
276
+ /**
277
+ * Set current tool being executed
278
+ */
279
+ setTool(toolName) {
280
+ this.currentTool = toolName;
281
+ // Re-render immediately to show tool
282
+ if (this.interval) {
283
+ this.render();
284
+ }
285
+ }
286
+ /**
287
+ * Clear the spinner area (all rendered lines)
288
+ */
289
+ clearArea() {
290
+ if (this.renderedLines > 0) {
291
+ // Move to start of current line
292
+ process.stdout.write('\r');
293
+ // Move up to first line
294
+ if (this.renderedLines > 1) {
295
+ process.stdout.write(`\x1b[${this.renderedLines - 1}A`);
296
+ }
297
+ // Clear from cursor to end of screen
298
+ process.stdout.write('\x1b[J');
299
+ }
300
+ else {
301
+ process.stdout.write('\r\x1b[K');
302
+ }
303
+ }
304
+ render() {
305
+ const elapsed = formatDuration(Date.now() - this.startTime);
306
+ const tokenStr = formatTokens(this.tokens);
307
+ // Clear previous render
308
+ this.clearArea();
309
+ // Build stats string
310
+ const stats = pc.dim(` (Esc to interrupt · ${elapsed} · ↓ ${tokenStr} tokens)`);
311
+ // If a tool is running, show that instead of the thinking word
312
+ if (this.currentTool) {
313
+ const toolDisplay = pc.yellow(`● ${this.currentTool}`);
314
+ process.stdout.write(`${toolDisplay}${stats}`);
315
+ // Render todo list below if we have todos
316
+ if (currentTodos.length > 0) {
317
+ process.stdout.write('\n' + renderTodoList(currentTodos));
318
+ this.renderedLines = 1 + currentTodos.length; // status + todos
319
+ }
320
+ else {
321
+ this.renderedLines = 1;
322
+ }
323
+ return;
324
+ }
325
+ // Check if there's an active todo - show that with animation
326
+ const activeTodo = getActiveTodo();
327
+ const displayText = activeTodo
328
+ ? (activeTodo.activeForm || activeTodo.content) + '...'
329
+ : this.word;
330
+ // Build the text with scanning highlight (knight rider effect)
331
+ let result = '';
332
+ for (let i = 0; i < displayText.length; i++) {
333
+ const distance = Math.abs(i - (this.position % displayText.length));
334
+ const char = displayText[i];
335
+ if (distance === 0) {
336
+ result += pc.bold(pc.magenta(char));
337
+ }
338
+ else if (distance === 1) {
339
+ result += pc.magenta(char);
340
+ }
341
+ else if (distance === 2) {
342
+ result += pc.red(char);
343
+ }
344
+ else {
345
+ result += pc.dim(pc.red(char));
346
+ }
347
+ }
348
+ // If we have an active todo, show the indicator
349
+ if (activeTodo) {
350
+ result = pc.red('✱ ') + result;
351
+ }
352
+ process.stdout.write(`${result}${stats}`);
353
+ // Render todo list below if we have todos
354
+ if (currentTodos.length > 0) {
355
+ process.stdout.write('\n' + renderTodoList(currentTodos));
356
+ this.renderedLines = 1 + currentTodos.length; // status + todos
357
+ }
358
+ else {
359
+ this.renderedLines = 1;
360
+ }
361
+ }
362
+ stop(wasAborted = false) {
363
+ if (this.interval) {
364
+ clearInterval(this.interval);
365
+ this.interval = null;
366
+ }
367
+ spinnerInstance = null;
368
+ // Clear all spinner lines
369
+ this.clearArea();
370
+ this.renderedLines = 0;
371
+ if (wasAborted) {
372
+ console.log(pc.yellow('Interrupted by user.\n'));
373
+ }
374
+ }
375
+ /**
376
+ * Pause the spinner animation (for permission prompts)
377
+ */
378
+ pause() {
379
+ if (this.interval) {
380
+ clearInterval(this.interval);
381
+ this.interval = null;
382
+ }
383
+ // Clear the spinner line
384
+ process.stdout.write('\r\x1b[K');
385
+ }
386
+ /**
387
+ * Clear all spinner lines for external output (tool results, etc.)
388
+ * After calling this, call render() to redraw if spinner should continue.
389
+ */
390
+ clearForOutput() {
391
+ this.clearArea();
392
+ this.renderedLines = 0;
393
+ }
394
+ /**
395
+ * Force an immediate render (e.g., after todos are updated)
396
+ */
397
+ forceRender() {
398
+ if (spinnerInstance === this) {
399
+ this.render();
400
+ }
401
+ }
402
+ /**
403
+ * Resume the spinner animation after pause
404
+ */
405
+ resume() {
406
+ if (!this.interval && spinnerInstance === this) {
407
+ this.render();
408
+ this.interval = setInterval(() => {
409
+ this.position += this.direction;
410
+ if (this.position >= this.word.length - 1) {
411
+ this.direction = -1;
412
+ }
413
+ else if (this.position <= 0) {
414
+ this.direction = 1;
415
+ }
416
+ this.render();
417
+ }, 150);
418
+ }
419
+ }
420
+ abort() {
421
+ if (this.abortController) {
422
+ this.abortController.abort();
423
+ }
424
+ this.stop(true);
425
+ }
426
+ isAborted() {
427
+ return this.abortController?.signal.aborted ?? false;
428
+ }
429
+ }
430
+ /**
431
+ * Set up global Esc key handler (called once at startup)
432
+ */
433
+ function setupEscHandler() {
434
+ // Listen for escape key (0x1b by itself, not part of arrow key sequence)
435
+ process.stdin.on('data', (data) => {
436
+ if (data.length === 1 && data[0] === 0x1b && spinnerInstance) {
437
+ spinnerInstance.abort();
438
+ }
439
+ });
440
+ }
441
+ // Parse command line arguments
442
+ const args = process.argv.slice(2);
443
+ const verbose = args.includes('--verbose') || args.includes('-v');
444
+ const minimal = args.includes('--minimal') || args.includes('-m');
445
+ const help = args.includes('--help') || args.includes('-h');
446
+ const showFiltering = args.includes('--show-filtering') || args.includes('-f');
447
+ // Parse --provider flag
448
+ let provider = 'claude';
449
+ const providerIdx = args.findIndex((a) => a === '--provider' || a === '-p');
450
+ if (providerIdx !== -1 && args[providerIdx + 1]) {
451
+ const p = args[providerIdx + 1].toLowerCase();
452
+ if (p === 'claude' || p === 'ollama' || p === 'openai' || p === 'gemini') {
453
+ provider = p;
454
+ }
455
+ else {
456
+ console.error(`Unknown provider: ${p}. Use 'claude', 'ollama', 'openai', or 'gemini'.`);
457
+ process.exit(1);
458
+ }
459
+ }
460
+ // Parse --model flag
461
+ let model;
462
+ const modelIdx = args.findIndex((a) => a === '--model');
463
+ if (modelIdx !== -1 && args[modelIdx + 1]) {
464
+ model = args[modelIdx + 1];
465
+ }
466
+ if (help) {
467
+ console.log(`
468
+ Agent Demo - PoC for @compilr-dev/agents
469
+
470
+ Usage:
471
+ npm run dev [options]
472
+
473
+ Options:
474
+ -v, --verbose Show tool execution events
475
+ -m, --minimal Use minimal tool set (faster startup)
476
+ -f, --show-filtering Show tool filtering analysis (token savings)
477
+ -p, --provider <name> LLM provider: claude (default), ollama, openai, gemini
478
+ --model <name> Model to use (provider-specific, see examples)
479
+ -h, --help Show this help message
480
+
481
+ Commands (in REPL):
482
+ exit Quit the demo
483
+ clear Clear conversation history
484
+ tools List available tools
485
+
486
+ Environment Variables:
487
+ ANTHROPIC_API_KEY Required for Claude provider
488
+ OPENAI_API_KEY Required for OpenAI provider
489
+ GOOGLE_AI_API_KEY Required for Gemini provider (or GEMINI_API_KEY)
490
+ OLLAMA_BASE_URL Ollama server URL (default: http://localhost:11434)
491
+
492
+ Examples:
493
+ npm run dev -- --provider claude # Claude with claude-sonnet-4
494
+ npm run dev -- --provider ollama # Ollama with llama3.1
495
+ npm run dev -- -p ollama --model mistral # Ollama with mistral
496
+ npm run dev -- --provider openai # OpenAI with gpt-4o
497
+ npm run dev -- -p openai --model gpt-4o-mini # OpenAI with gpt-4o-mini
498
+ npm run dev -- --provider gemini # Gemini with gemini-2.0-flash
499
+ npm run dev -- -p gemini --model gemini-1.5-pro # Gemini with gemini-1.5-pro
500
+ `);
501
+ process.exit(0);
502
+ }
503
+ async function main() {
504
+ // Clear screen and move cursor to top
505
+ process.stdout.write('\x1B[2J\x1B[H');
506
+ // ASCII Logo (from compilr-dev-cli)
507
+ console.log(pc.cyan(` ___ ___ _ __ ___ _ __ (_) |_ __
508
+ / __|/ _ \\| '_ \` _ \\| '_ \\| | | '__|
509
+ | (__| (_) | | | | | | |_) | | | |
510
+ \\___|\\___/|_| |_| |_| .__/|_|_|_|
511
+ | | .dev
512
+ |_|`));
513
+ console.log('');
514
+ console.log(pc.bold(pc.cyan('compilr-dev-cli')) + pc.dim(' - AI-powered terminal assistant'));
515
+ console.log('');
516
+ // Interactive input handler (will be initialized later)
517
+ let inputHandler = null;
518
+ /**
519
+ * Simple single-key input for permission prompts
520
+ */
521
+ const askYesNo = (prompt) => {
522
+ return new Promise((resolve) => {
523
+ process.stdout.write(prompt);
524
+ // Ensure stdin is in the right state for reading
525
+ if (process.stdin.isTTY) {
526
+ process.stdin.setRawMode(true);
527
+ }
528
+ process.stdin.resume();
529
+ const onData = (data) => {
530
+ const key = data.toString().toLowerCase();
531
+ process.stdin.removeListener('data', onData);
532
+ // Restore stdin state
533
+ if (process.stdin.isTTY) {
534
+ process.stdin.setRawMode(false);
535
+ }
536
+ process.stdin.pause();
537
+ if (key === 'y') {
538
+ process.stdout.write('y\n');
539
+ resolve(true);
540
+ }
541
+ else {
542
+ process.stdout.write('n\n');
543
+ resolve(false);
544
+ }
545
+ };
546
+ process.stdin.once('data', onData);
547
+ });
548
+ };
549
+ /**
550
+ * Permission handler - prompts user for approval
551
+ */
552
+ const handlePermissionRequest = async (request) => {
553
+ // Pause spinner if active (stop the interval, clear the line)
554
+ let wasSpinnerActive = false;
555
+ if (spinnerInstance) {
556
+ wasSpinnerActive = true;
557
+ spinnerInstance.pause();
558
+ }
559
+ // Show permission request
560
+ console.log('');
561
+ console.log(pc.yellow('⚠ Permission Required'));
562
+ console.log(pc.bold(` Tool: ${request.toolName}`));
563
+ if (request.description) {
564
+ console.log(pc.dim(` ${request.description}`));
565
+ }
566
+ // Show preview of what will happen
567
+ const input = request.input;
568
+ if (input.command) {
569
+ console.log(pc.dim(` Command: ${String(input.command).slice(0, 60)}${String(input.command).length > 60 ? '...' : ''}`));
570
+ }
571
+ else if (input.path) {
572
+ console.log(pc.dim(` Path: ${input.path}`));
573
+ }
574
+ else if (input.message) {
575
+ console.log(pc.dim(` Message: ${String(input.message).slice(0, 60)}`));
576
+ }
577
+ console.log('');
578
+ const allowed = await askYesNo(pc.cyan(' Allow? [y/N]: '));
579
+ if (allowed) {
580
+ console.log(pc.green(' ✓ Allowed\n'));
581
+ }
582
+ else {
583
+ console.log(pc.red(' ✗ Denied\n'));
584
+ }
585
+ // Resume spinner if it was active
586
+ if (wasSpinnerActive && spinnerInstance) {
587
+ spinnerInstance.resume();
588
+ }
589
+ return allowed;
590
+ };
591
+ // Create agent with permission handler
592
+ let agent;
593
+ try {
594
+ agent = createAgent({
595
+ verbose,
596
+ minimal,
597
+ provider,
598
+ model,
599
+ onPermissionRequest: handlePermissionRequest,
600
+ });
601
+ console.log(pc.green('✓') + ` Agent initialized with ${minimal ? 'minimal' : 'full'} tool set.`);
602
+ console.log(pc.green('✓') + ` Permissions enabled for: bash, write_file, edit, git_commit`);
603
+ console.log(pc.dim('Type / for commands, /exit to quit.\n'));
604
+ }
605
+ catch (error) {
606
+ console.error('Failed to create agent:', error.message);
607
+ process.exit(1);
608
+ }
609
+ // Session-wide token tracking
610
+ let sessionInputTokens = 0;
611
+ let sessionOutputTokens = 0;
612
+ let sessionRequests = 0;
613
+ // Set up Esc key handler for interrupting agent (once at startup)
614
+ setupEscHandler();
615
+ // Handle slash commands
616
+ function handleCommand(input) {
617
+ const trimmed = input.trim();
618
+ // Commands must start with /
619
+ if (!trimmed.startsWith('/')) {
620
+ return false;
621
+ }
622
+ const cmd = trimmed.slice(1).toLowerCase();
623
+ switch (cmd) {
624
+ case 'exit':
625
+ case 'quit':
626
+ case 'q':
627
+ console.log('\nGoodbye!');
628
+ inputHandler?.stop();
629
+ process.exit(0);
630
+ case 'clear':
631
+ agent.clearHistory();
632
+ console.log(pc.green('✓') + ' Conversation history cleared.\n');
633
+ return true;
634
+ case 'tools': {
635
+ const tools = agent.getToolDefinitions();
636
+ console.log(pc.bold(`\nAvailable tools (${tools.length}):`));
637
+ for (const tool of tools) {
638
+ console.log(pc.dim(' •') + ` ${tool.name}`);
639
+ }
640
+ console.log('');
641
+ return true;
642
+ }
643
+ case 'tokens': {
644
+ const sessionTotal = sessionInputTokens + sessionOutputTokens;
645
+ console.log(pc.bold(`\nSession Token Usage:`));
646
+ console.log(` Total: ${pc.cyan(sessionTotal.toLocaleString())} tokens`);
647
+ console.log(` Input: ${pc.dim(sessionInputTokens.toLocaleString())} tokens`);
648
+ console.log(` Output: ${pc.dim(sessionOutputTokens.toLocaleString())} tokens`);
649
+ console.log(` Requests: ${sessionRequests}`);
650
+ if (sessionRequests > 0) {
651
+ console.log(` Average: ${Math.round(sessionTotal / sessionRequests).toLocaleString()} tokens/req`);
652
+ }
653
+ console.log('');
654
+ return true;
655
+ }
656
+ case 'context': {
657
+ const stats = agent.getContextStats?.();
658
+ if (stats) {
659
+ const pct = (stats.utilization * 100).toFixed(2);
660
+ const sessionTotal = sessionInputTokens + sessionOutputTokens;
661
+ // Color utilization based on level
662
+ const pctNum = parseFloat(pct);
663
+ const pctColor = pctNum > 80 ? pc.red : pctNum > 50 ? pc.yellow : pc.green;
664
+ console.log(pc.bold(`\nContext Window:`));
665
+ console.log(` History: ${pc.cyan(stats.currentTokens.toLocaleString())} tokens ${pc.dim(`(${stats.messageCount} msgs, ${stats.turnCount} turns)`)}`);
666
+ console.log(` Limit: ${pc.dim(stats.maxTokens.toLocaleString())} tokens`);
667
+ console.log(` Utilization: ${pctColor(pct + '%')}`);
668
+ console.log(pc.bold(`\nSession Usage:`));
669
+ console.log(` API tokens: ${pc.cyan(sessionTotal.toLocaleString())} ${pc.dim(`(${sessionInputTokens.toLocaleString()} in / ${sessionOutputTokens.toLocaleString()} out)`)}`);
670
+ console.log(` Requests: ${sessionRequests}`);
671
+ if (stats.compactionCount > 0 || stats.summarizationCount > 0) {
672
+ console.log(pc.bold(`\nContext Management:`));
673
+ if (stats.compactionCount > 0) {
674
+ console.log(` Compactions: ${stats.compactionCount}`);
675
+ }
676
+ if (stats.summarizationCount > 0) {
677
+ console.log(` Summarizations: ${stats.summarizationCount}`);
678
+ }
679
+ }
680
+ }
681
+ else {
682
+ console.log(pc.yellow('\nContext manager not enabled or no stats available.'));
683
+ }
684
+ console.log('');
685
+ return true;
686
+ }
687
+ case 'compact': {
688
+ // Mark as async command - will be handled separately
689
+ return 'async';
690
+ }
691
+ case 'help':
692
+ case '?':
693
+ // Mark as async command - will show tabbed help menu
694
+ return 'async';
695
+ default:
696
+ if (trimmed.startsWith('/')) {
697
+ console.log(pc.yellow(`Unknown command: ${trimmed}`) + pc.dim(` - Type /help for available commands.\n`));
698
+ return true;
699
+ }
700
+ return false;
701
+ }
702
+ }
703
+ // Handle async /compact command
704
+ async function handleCompact() {
705
+ const statsBefore = agent.getContextStats?.();
706
+ if (!statsBefore || statsBefore.currentTokens === 0) {
707
+ console.log(pc.yellow('\nNothing to compact (history is empty).\n'));
708
+ return;
709
+ }
710
+ const history = agent.getHistory();
711
+ if (history.length < 6) {
712
+ console.log(pc.yellow('\nNot enough history to compact (need at least 6 messages).\n'));
713
+ return;
714
+ }
715
+ console.log(pc.bold('\nCompacting context...'));
716
+ console.log(` Before: ${pc.cyan(statsBefore.currentTokens.toLocaleString())} tokens (${history.length} messages)`);
717
+ try {
718
+ // Keep the last 4 messages (2 turns), but only text-only messages
719
+ // Tool call messages can cause issues when restored without proper context
720
+ const keepCount = 4;
721
+ // Filter to get only "clean" messages (text-only, no tool calls)
722
+ const cleanMessages = history.filter(m => {
723
+ if (typeof m.content === 'string')
724
+ return true;
725
+ // Check if any content block is a tool_use or tool_result
726
+ const hasToolContent = m.content.some(b => b.type === 'tool_use' || b.type === 'tool_result');
727
+ return !hasToolContent;
728
+ });
729
+ if (cleanMessages.length < 4) {
730
+ console.log(pc.yellow(' Not enough text messages to compact (mostly tool calls).\n'));
731
+ console.log(pc.dim(' Use /clear to reset history completely.\n'));
732
+ return;
733
+ }
734
+ const recentMessages = cleanMessages.slice(-keepCount);
735
+ const oldMessages = cleanMessages.slice(0, -keepCount);
736
+ if (oldMessages.length === 0) {
737
+ console.log(pc.yellow(' Nothing to summarize (only recent messages remain).\n'));
738
+ return;
739
+ }
740
+ // Generate a summary of the old messages using the agent
741
+ console.log(pc.dim(` Summarizing ${oldMessages.length} old messages...`));
742
+ // Build a summary prompt from old messages
743
+ const oldContent = oldMessages
744
+ .filter(m => m.role !== 'system')
745
+ .map(m => {
746
+ const role = m.role === 'user' ? 'User' : 'Assistant';
747
+ const text = typeof m.content === 'string'
748
+ ? m.content
749
+ : m.content
750
+ .filter((b) => b.type === 'text')
751
+ .map(b => b.text)
752
+ .join('\n');
753
+ return `${role}: ${text.slice(0, 500)}${text.length > 500 ? '...' : ''}`;
754
+ })
755
+ .join('\n\n');
756
+ // Use a temporary clear to generate summary without old context
757
+ agent.clearHistory();
758
+ // Generate summary
759
+ let summary = '';
760
+ const summaryPrompt = `Summarize this conversation in 2-3 sentences, focusing on key topics discussed and any important outcomes:\n\n${oldContent}`;
761
+ for await (const event of agent.stream(summaryPrompt)) {
762
+ if (event.type === 'llm_chunk' && event.chunk.type === 'text' && event.chunk.text) {
763
+ summary += event.chunk.text;
764
+ }
765
+ }
766
+ // Clear again and set new history with summary + recent messages
767
+ agent.clearHistory();
768
+ // Create a system-style message with the summary
769
+ const summaryMessage = {
770
+ role: 'user',
771
+ content: `[Previous conversation summary: ${summary.trim()}]`,
772
+ };
773
+ const assistantAck = {
774
+ role: 'assistant',
775
+ content: 'I understand. I have context from our previous conversation. How can I help you continue?',
776
+ };
777
+ // Set new compacted history
778
+ await agent.setHistory([summaryMessage, assistantAck, ...recentMessages]);
779
+ const statsAfter = agent.getContextStats?.();
780
+ const saved = statsBefore.currentTokens - (statsAfter?.currentTokens ?? 0);
781
+ const savedPct = ((saved / statsBefore.currentTokens) * 100).toFixed(1);
782
+ console.log(` After: ${pc.cyan(statsAfter?.currentTokens.toLocaleString() ?? '0')} tokens (${(statsAfter?.messageCount ?? 0)} messages)`);
783
+ console.log(pc.green('✓') + ` Compacted! Saved ${pc.cyan(saved.toLocaleString())} tokens (${savedPct}% reduction)`);
784
+ }
785
+ catch (error) {
786
+ console.log(pc.red(' Error:'), error.message);
787
+ }
788
+ console.log('');
789
+ }
790
+ /**
791
+ * Process user input (command or message to agent)
792
+ */
793
+ async function processInput(input) {
794
+ // Handle empty input
795
+ if (!input.trim()) {
796
+ return;
797
+ }
798
+ // Check for slash commands
799
+ const cmdResult = handleCommand(input);
800
+ if (cmdResult === 'async') {
801
+ // Handle async commands
802
+ const cmd = input.trim().toLowerCase();
803
+ if (cmd === '/compact') {
804
+ await handleCompact();
805
+ }
806
+ else if (cmd === '/help' || cmd === '/?') {
807
+ await showHelpMenu();
808
+ }
809
+ return;
810
+ }
811
+ if (cmdResult === true) {
812
+ return;
813
+ }
814
+ // Apply tool filtering - select relevant tools based on user intent
815
+ const totalTools = agent.getToolDefinitions().length;
816
+ const selectedNames = selectToolNamesByIntent(input);
817
+ // Show tool filtering analysis if enabled
818
+ if (showFiltering) {
819
+ const savings = estimateTokenSavings(selectedNames.length, totalTools);
820
+ console.log(`\n[Tool Filtering] Selecting ${selectedNames.length}/${totalTools} tools:`);
821
+ console.log(` Selected: ${selectedNames.join(', ')}`);
822
+ console.log(` Estimated savings: ~${savings.saved} tokens (${savings.percentage}% reduction)`);
823
+ }
824
+ // Send to agent using stream() for real-time output
825
+ // Pass toolFilter to reduce token usage by only sending relevant tools
826
+ const spinner = new ThinkingSpinner();
827
+ try {
828
+ // Start the thinking spinner
829
+ const abortController = spinner.start();
830
+ let totalInputTokens = 0;
831
+ let totalOutputTokens = 0;
832
+ let llmCalls = 0;
833
+ let hasTextOutput = false;
834
+ let usedTools = false;
835
+ let spinnerStopped = false;
836
+ let lastToolInput = null;
837
+ let textBuffer = ''; // Buffer text for markdown rendering
838
+ for await (const event of agent.stream(input, {
839
+ toolFilter: selectedNames,
840
+ signal: abortController.signal,
841
+ })) {
842
+ // Check if aborted
843
+ if (spinner.isAborted()) {
844
+ break;
845
+ }
846
+ if (event.type === 'llm_chunk') {
847
+ if (event.chunk.type === 'text' && event.chunk.text) {
848
+ // Stop spinner on first text output
849
+ if (!spinnerStopped) {
850
+ spinner.stop();
851
+ spinnerStopped = true;
852
+ }
853
+ // Buffer text for markdown rendering at the end
854
+ textBuffer += event.chunk.text;
855
+ hasTextOutput = true;
856
+ }
857
+ else if (event.chunk.type === 'done' && event.chunk.usage) {
858
+ // Update token tracking
859
+ const tokens = event.chunk.usage.inputTokens + event.chunk.usage.outputTokens;
860
+ spinner.addTokens(tokens);
861
+ totalInputTokens += event.chunk.usage.inputTokens;
862
+ totalOutputTokens += event.chunk.usage.outputTokens;
863
+ llmCalls++;
864
+ }
865
+ }
866
+ else if (event.type === 'tool_start') {
867
+ usedTools = true;
868
+ // Store input for when tool ends
869
+ lastToolInput = event.input;
870
+ // Show tool name in spinner while running (but not for todo tools - they're silent)
871
+ if (event.name !== 'todo_read' && event.name !== 'todo_write') {
872
+ spinner.setTool(event.name);
873
+ }
874
+ }
875
+ else if (event.type === 'tool_end') {
876
+ // Clear all spinner lines (including todos) before printing tool result
877
+ spinner.clearForOutput();
878
+ // Format: ● ToolName(args)
879
+ // ⎿ Result summary
880
+ const toolName = event.name;
881
+ const toolInput = lastToolInput;
882
+ const result = event.result;
883
+ lastToolInput = null;
884
+ // Build argument summary (first string arg or key info)
885
+ let argSummary = '';
886
+ if (toolInput) {
887
+ // Common patterns: path, file_path, command, pattern
888
+ const path = toolInput.path || toolInput.file_path || toolInput.filePath;
889
+ const command = toolInput.command;
890
+ const pattern = toolInput.pattern;
891
+ if (typeof path === 'string') {
892
+ // Show just filename, not full path
893
+ const filename = path.split('/').pop() || path;
894
+ argSummary = filename;
895
+ }
896
+ else if (typeof command === 'string') {
897
+ // Truncate long commands
898
+ argSummary = command.length > 40 ? command.slice(0, 40) + '...' : command;
899
+ }
900
+ else if (typeof pattern === 'string') {
901
+ argSummary = pattern;
902
+ }
903
+ }
904
+ // Special handling for todo tools - silent, rendered with spinner
905
+ if (toolName === 'todo_write' && toolInput && Array.isArray(toolInput.todos)) {
906
+ // Normalize todos - handle different property names the agent might use
907
+ currentTodos = toolInput.todos.map((t) => ({
908
+ content: String(t.content || t.title || t.task || t.description || t.text || 'Untitled task'),
909
+ status: (['pending', 'in_progress', 'completed'].includes(String(t.status))
910
+ ? t.status
911
+ : 'pending'),
912
+ activeForm: t.activeForm ? String(t.activeForm) : undefined,
913
+ }));
914
+ // Immediately render updated todos
915
+ spinner.setTool(null);
916
+ spinner.forceRender();
917
+ continue; // Skip normal tool result display
918
+ }
919
+ // Skip todo_read display - silent like todo_write
920
+ if (toolName === 'todo_read') {
921
+ spinner.setTool(null);
922
+ continue;
923
+ }
924
+ // Format tool header
925
+ const toolHeader = argSummary
926
+ ? pc.yellow(`● ${toolName}`) + pc.dim(`(${argSummary})`)
927
+ : pc.yellow(`● ${toolName}`);
928
+ // Format result summary
929
+ let resultSummary = '';
930
+ if (result) {
931
+ if (result.error) {
932
+ resultSummary = pc.red(`Error: ${String(result.error).slice(0, 60)}`);
933
+ }
934
+ else if (result.result !== undefined) {
935
+ resultSummary = formatToolResult(result.result);
936
+ }
937
+ }
938
+ // Print tool log
939
+ console.log(toolHeader);
940
+ if (resultSummary) {
941
+ console.log(pc.dim(' ⎿ ') + resultSummary);
942
+ }
943
+ console.log(''); // Blank line between tools
944
+ // Clear tool from spinner and immediately re-render with todos
945
+ spinner.setTool(null);
946
+ spinner.forceRender();
947
+ }
948
+ }
949
+ // Make sure spinner is stopped
950
+ if (!spinnerStopped && !spinner.isAborted()) {
951
+ spinner.stop();
952
+ }
953
+ // Skip stats if aborted
954
+ if (spinner.isAborted()) {
955
+ return;
956
+ }
957
+ // Output buffered text with markdown rendering
958
+ if (textBuffer) {
959
+ process.stdout.write(pc.bold(pc.green('Agent: ')));
960
+ console.log(renderMarkdown(textBuffer));
961
+ }
962
+ // Warn if tools were used but no text output (common with lite models)
963
+ if (usedTools && !hasTextOutput) {
964
+ console.log(pc.yellow('(No text response from model - try a more capable model like gemini-2.0-flash)'));
965
+ }
966
+ // Update session totals
967
+ sessionInputTokens += totalInputTokens;
968
+ sessionOutputTokens += totalOutputTokens;
969
+ sessionRequests++;
970
+ // Show token usage summary
971
+ if (totalInputTokens > 0 || totalOutputTokens > 0) {
972
+ const total = totalInputTokens + totalOutputTokens;
973
+ const sessionTotal = sessionInputTokens + sessionOutputTokens;
974
+ console.log(pc.dim(`\n[Tokens: ${total.toLocaleString()} (${totalInputTokens.toLocaleString()} in / ${totalOutputTokens.toLocaleString()} out) - ${llmCalls} LLM call(s)]`));
975
+ console.log(pc.dim(`[Session: ${sessionTotal.toLocaleString()} total (${sessionRequests} requests, avg ${Math.round(sessionTotal / sessionRequests).toLocaleString()}/req)]`));
976
+ }
977
+ console.log('');
978
+ }
979
+ catch (error) {
980
+ spinner.stop();
981
+ // Don't show error for abort
982
+ if (error.name !== 'AbortError') {
983
+ console.error('\n' + pc.red('Error:'), error.message);
984
+ }
985
+ console.log('');
986
+ }
987
+ }
988
+ // Create interactive input with slash command autocomplete
989
+ const promptText = pc.bold(pc.cyan('compilr>')) + ' ';
990
+ const handleInput = async (input) => {
991
+ // Stop input handler while processing
992
+ inputHandler?.stop();
993
+ // Process the input
994
+ await processInput(input);
995
+ // Restart input handler for next prompt
996
+ inputHandler?.start();
997
+ };
998
+ inputHandler = createInteractiveInput(promptText, (input) => {
999
+ handleInput(input);
1000
+ }, true, () => currentTodos);
1001
+ // Handle Ctrl+C gracefully
1002
+ process.on('SIGINT', () => {
1003
+ console.log('\n\nGoodbye!');
1004
+ inputHandler?.stop();
1005
+ process.exit(0);
1006
+ });
1007
+ // Start the interactive input
1008
+ inputHandler.start();
1009
+ }
1010
+ // Run
1011
+ main().catch((error) => {
1012
+ console.error('Fatal error:', error);
1013
+ process.exit(1);
1014
+ });