@koi-language/koi 1.0.6 → 1.1.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 (113) hide show
  1. package/README.md +4 -125
  2. package/examples/.build/agent-dialogue.ts +138 -0
  3. package/examples/.build/agent-dialogue.ts.map +1 -0
  4. package/examples/.build/chess.ts +77 -0
  5. package/examples/.build/chess.ts.map +1 -0
  6. package/examples/.build/delegation-test.ts +140 -0
  7. package/examples/.build/delegation-test.ts.map +1 -0
  8. package/examples/.build/dialog-demo.ts +77 -0
  9. package/examples/.build/dialog-demo.ts.map +1 -0
  10. package/examples/.build/hello-world.ts +77 -0
  11. package/examples/.build/hello-world.ts.map +1 -0
  12. package/examples/.build/lover-dialog-demo.ts +77 -0
  13. package/examples/.build/lover-dialog-demo.ts.map +1 -0
  14. package/examples/.build/package.json +3 -0
  15. package/examples/.build/registry-interactive-demo.ts +202 -0
  16. package/examples/.build/registry-interactive-demo.ts.map +1 -0
  17. package/examples/.build/registry-playbook-demo.ts +201 -0
  18. package/examples/.build/registry-playbook-demo.ts.map +1 -0
  19. package/examples/.build/tic-tac-toe.ts +77 -0
  20. package/examples/.build/tic-tac-toe.ts.map +1 -0
  21. package/examples/actions-demo.koi +8 -9
  22. package/examples/activists-dialogue.koi +75 -0
  23. package/examples/agent-dialogue.koi +66 -0
  24. package/examples/chess.koi +19 -0
  25. package/examples/counter.koi +20 -69
  26. package/examples/delegation-test.koi +16 -18
  27. package/examples/dialog-demo.koi +20 -0
  28. package/examples/hello-world.koi +7 -43
  29. package/examples/mcp-stdio-demo.koi +29 -0
  30. package/examples/memory-test.koi +49 -0
  31. package/examples/mobile-mcp-demo.koi +32 -0
  32. package/examples/multi-event-handler-test.koi +16 -18
  33. package/examples/pipeline.koi +15 -17
  34. package/examples/prompt-demo.koi +20 -0
  35. package/examples/{registry-playbook-email-compositor.koi → registry-interactive-demo.koi} +27 -27
  36. package/examples/registry-playbook-demo.koi +28 -28
  37. package/examples/skill-import-test.koi +7 -9
  38. package/examples/skills/.build/math-operations.ts +1656 -0
  39. package/examples/skills/.build/math-operations.ts.map +1 -0
  40. package/examples/skills/.build/package.json +3 -0
  41. package/examples/skills/.build/string-operations.ts +1643 -0
  42. package/examples/skills/.build/string-operations.ts.map +1 -0
  43. package/examples/skills/advanced/.build/index.ts +3223 -0
  44. package/examples/skills/advanced/.build/index.ts.map +1 -0
  45. package/examples/skills/advanced/.build/package.json +3 -0
  46. package/examples/skills/advanced/index.koi +3 -5
  47. package/examples/skills/math-operations.koi +1 -3
  48. package/examples/skills/string-operations.koi +1 -3
  49. package/examples/tic-tac-toe.koi +19 -0
  50. package/examples/utils/echo-mcp-server.js +141 -0
  51. package/examples/web-delegation-demo.koi +15 -17
  52. package/package.json +2 -1
  53. package/src/cli/koi.js +30 -41
  54. package/src/compiler/build-optimizer.js +204 -289
  55. package/src/compiler/cache-manager.js +1 -1
  56. package/src/compiler/import-resolver.js +5 -9
  57. package/src/compiler/parser.js +6072 -3476
  58. package/src/compiler/transpiler.js +346 -38
  59. package/src/grammar/koi.pegjs +302 -62
  60. package/src/runtime/actions/{format.js → call-llm.js} +37 -44
  61. package/src/runtime/actions/call-mcp.js +97 -0
  62. package/src/runtime/actions/if.js +179 -0
  63. package/src/runtime/actions/print.js +3 -1
  64. package/src/runtime/actions/prompt-user.js +75 -0
  65. package/src/runtime/actions/repeat.js +147 -0
  66. package/src/runtime/actions/shell.js +185 -0
  67. package/src/runtime/actions/while.js +205 -0
  68. package/src/runtime/agent.js +592 -178
  69. package/src/runtime/cli-display.js +26 -0
  70. package/src/runtime/cli-input.js +421 -0
  71. package/src/runtime/cli-logger.js +2 -5
  72. package/src/runtime/cli-markdown.js +61 -0
  73. package/src/runtime/cli-select.js +106 -0
  74. package/src/runtime/incremental-json-parser.js +27 -17
  75. package/src/runtime/index.js +1 -0
  76. package/src/runtime/llm-provider.js +1083 -572
  77. package/src/runtime/mcp-registry.js +141 -0
  78. package/src/runtime/mcp-stdio-client.js +334 -0
  79. package/src/runtime/planner.js +1 -1
  80. package/src/runtime/playbook-session.js +259 -0
  81. package/src/runtime/registry-backends/keyv-sqlite.js +1 -1
  82. package/src/runtime/registry-backends/local.js +1 -1
  83. package/src/runtime/router.js +22 -26
  84. package/src/runtime/runtime.js +7 -1
  85. package/examples/cache-test.koi +0 -29
  86. package/examples/calculator.koi +0 -61
  87. package/examples/clear-registry.js +0 -33
  88. package/examples/clear-registry.koi +0 -30
  89. package/examples/code-introspection-test.koi +0 -149
  90. package/examples/directory-import-test.koi +0 -84
  91. package/examples/hello-world-claude.koi +0 -52
  92. package/examples/hello.koi +0 -24
  93. package/examples/mcp-example.koi +0 -70
  94. package/examples/new-import-test.koi +0 -89
  95. package/examples/registry-demo.koi +0 -184
  96. package/examples/registry-playbook-email-compositor-2.koi +0 -140
  97. package/examples/sentiment.koi +0 -90
  98. package/examples/simple.koi +0 -48
  99. package/examples/task-chaining-demo.koi +0 -244
  100. package/examples/test-await.koi +0 -22
  101. package/examples/test-crypto-sha256.koi +0 -196
  102. package/examples/test-delegation.koi +0 -41
  103. package/examples/test-multi-team-routing.koi +0 -258
  104. package/examples/test-no-handler.koi +0 -35
  105. package/examples/test-npm-import.koi +0 -67
  106. package/examples/test-parse.koi +0 -10
  107. package/examples/test-peers-with-team.koi +0 -59
  108. package/examples/test-permissions-fail.koi +0 -20
  109. package/examples/test-permissions.koi +0 -36
  110. package/examples/test-simple-registry.koi +0 -31
  111. package/examples/test-typescript-import.koi +0 -64
  112. package/examples/test-uses-team-syntax.koi +0 -25
  113. package/examples/test-uses-team.koi +0 -31
@@ -0,0 +1,26 @@
1
+ /**
2
+ * CLI Display helpers for progress spinner text.
3
+ * Separated from agent.js to avoid circular dependencies with action files.
4
+ */
5
+
6
+ /**
7
+ * Build display text for progress spinner based on action type.
8
+ * For MCP calls: "[🤖 Agent 🔌 mcp] tool(summary)"
9
+ * For others: "[🤖 Agent] Thinking" (or desc if provided)
10
+ */
11
+ export function buildActionDisplay(agentName, action) {
12
+ const intent = action.intent || action.type;
13
+
14
+ if (intent === 'call_mcp' && action.mcp && action.tool) {
15
+ let inputSummary = '';
16
+ if (action.input && typeof action.input === 'object' && Object.keys(action.input).length > 0) {
17
+ const raw = JSON.stringify(action.input);
18
+ inputSummary = raw.length > 80 ? raw.substring(0, 77) + '...' : raw;
19
+ }
20
+ const toolCall = inputSummary ? `${action.tool}(${inputSummary})` : action.tool;
21
+ return `[🤖 ${agentName} 🔌 ${action.mcp}] ${toolCall}`;
22
+ }
23
+
24
+ const displayText = action.desc ? action.desc.replace(/\.\.\.$/, '') : 'Thinking';
25
+ return `[🤖 ${agentName}] ${displayText}`;
26
+ }
@@ -0,0 +1,421 @@
1
+ /**
2
+ * CLI Input - Rich line editor with blinking underscore cursor, multi-line, and history.
3
+ *
4
+ * Features:
5
+ * - Blinking underscore cursor (classic terminal style)
6
+ * - Left/Right: move by character
7
+ * - Option+Left/Right: move by word
8
+ * - Up/Down: navigate visual lines
9
+ * - Up at start of text: previous history entry
10
+ * - Down at end of text: next history entry (empty at end)
11
+ * - Ctrl+A / Home: beginning of line
12
+ * - Ctrl+E / End: end of line
13
+ * - Backspace / Option+Backspace: delete char / word
14
+ * - Delete / Ctrl+D: delete at cursor
15
+ * - Ctrl+U: clear before cursor
16
+ * - Ctrl+K: clear after cursor
17
+ * - Ctrl+W: delete word before cursor
18
+ * - Enter: submit
19
+ * - Ctrl+C / Escape: cancel
20
+ *
21
+ * Uses process.stdout.write exclusively — no console.log calls.
22
+ */
23
+
24
+ import readline from 'readline';
25
+
26
+ // Session-wide input history (persists across cliInput calls, resets on process exit)
27
+ const inputHistory = [];
28
+
29
+ /**
30
+ * Show a line editor with block cursor, multi-line support, and history.
31
+ * @param {string} promptText - The prompt prefix (shown in bold white)
32
+ * @returns {Promise<string>} The user's input text
33
+ */
34
+ export function cliInput(promptText) {
35
+ return new Promise((resolve) => {
36
+ const stdout = process.stdout;
37
+ const stdin = process.stdin;
38
+
39
+ // Ensure prompt ends with space
40
+ const prompt = promptText.endsWith(' ') ? promptText : promptText + ' ';
41
+ const promptLen = prompt.length;
42
+
43
+ let text = '';
44
+ let cursor = 0;
45
+ let prevLineCount = 0;
46
+
47
+ // Blinking cursor state
48
+ let cursorVisible = true;
49
+ let blinkInterval = null;
50
+ let lastKeypressTime = 0;
51
+
52
+ // History navigation
53
+ let historyIndex = -1; // -1 = not browsing history
54
+ let savedText = ''; // current text saved before entering history
55
+
56
+ // --- Visual line calculations ---
57
+
58
+ function getCols() {
59
+ return stdout.columns || parseInt(process.env.COLUMNS) || 80;
60
+ }
61
+
62
+ function firstLineCap() {
63
+ return Math.max(1, getCols() - promptLen);
64
+ }
65
+
66
+ function cursorToRowCol(pos) {
67
+ const flc = firstLineCap();
68
+ const c = getCols();
69
+ if (pos < flc) return { row: 0, col: pos };
70
+ const rem = pos - flc;
71
+ return { row: 1 + Math.floor(rem / c), col: rem % c };
72
+ }
73
+
74
+ function rowColToCursor(row, col) {
75
+ const flc = firstLineCap();
76
+ const c = getCols();
77
+ if (row === 0) return Math.min(col, text.length, flc);
78
+ const pos = flc + (row - 1) * c + col;
79
+ return Math.min(pos, text.length);
80
+ }
81
+
82
+ function totalRows() {
83
+ const flc = firstLineCap();
84
+ const c = getCols();
85
+ if (text.length <= flc) return 1;
86
+ return 1 + Math.ceil((text.length - flc) / c);
87
+ }
88
+
89
+ function lineLen(row) {
90
+ const flc = firstLineCap();
91
+ const c = getCols();
92
+ if (row === 0) return Math.min(text.length, flc);
93
+ const start = flc + (row - 1) * c;
94
+ if (start >= text.length) return 0;
95
+ return Math.min(c, text.length - start);
96
+ }
97
+
98
+ // --- Rendering ---
99
+
100
+ function render() {
101
+ const flc = firstLineCap();
102
+ const c = getCols();
103
+ const numRows = totalRows();
104
+ const { row: curRow, col: curCol } = cursorToRowCol(cursor);
105
+
106
+ // Ensure cursor row exists (cursor at end of text may be on new row)
107
+ const renderRows = Math.max(numRows, curRow + 1);
108
+
109
+ // Move to first line of editor area
110
+ if (prevLineCount > 1) {
111
+ stdout.write(`\x1b[${prevLineCount - 1}A`);
112
+ }
113
+ stdout.write('\r');
114
+
115
+ for (let r = 0; r < renderRows; r++) {
116
+ // Overwrite in place (no \x1b[2K clear-first) to avoid flicker.
117
+ // After writing content, \x1b[K clears any leftover chars to the right.
118
+
119
+ // Compute text slice for this row
120
+ let lineStart, lineEnd;
121
+ if (r === 0) {
122
+ lineStart = 0;
123
+ lineEnd = Math.min(text.length, flc);
124
+ stdout.write(`\x1b[1m${prompt}\x1b[0m`);
125
+ } else {
126
+ lineStart = flc + (r - 1) * c;
127
+ lineEnd = Math.min(text.length, lineStart + c);
128
+ }
129
+
130
+ const lineText = (lineStart < text.length) ? text.substring(lineStart, lineEnd) : '';
131
+
132
+ if (r === curRow) {
133
+ // Render with blinking underscore cursor
134
+ const before = lineText.substring(0, curCol);
135
+ const atCursor = curCol < lineText.length ? lineText[curCol] : ' ';
136
+ const after = curCol < lineText.length ? lineText.substring(curCol + 1) : '';
137
+ if (cursorVisible) {
138
+ // Cursor ON: show character with underline decoration
139
+ stdout.write(`\x1b[36m${before}\x1b[4;36m${atCursor}\x1b[24m${after}\x1b[0m`);
140
+ } else {
141
+ // Cursor OFF: show character normally (no decoration)
142
+ stdout.write(`\x1b[36m${before}${atCursor}${after}\x1b[0m`);
143
+ }
144
+ } else {
145
+ stdout.write(`\x1b[36m${lineText}\x1b[0m`);
146
+ }
147
+
148
+ // Clear any leftover characters after content
149
+ stdout.write('\x1b[K');
150
+
151
+ if (r < renderRows - 1) stdout.write('\n');
152
+ }
153
+
154
+ // Clear leftover lines from previous render
155
+ if (prevLineCount > renderRows) {
156
+ const extra = prevLineCount - renderRows;
157
+ for (let i = 0; i < extra; i++) {
158
+ stdout.write('\n\x1b[2K');
159
+ }
160
+ stdout.write(`\x1b[${extra}A`);
161
+ }
162
+
163
+ prevLineCount = renderRows;
164
+ }
165
+
166
+ // --- History helpers ---
167
+
168
+ function historyUp() {
169
+ if (inputHistory.length === 0) return;
170
+ if (historyIndex === -1) {
171
+ savedText = text;
172
+ historyIndex = inputHistory.length - 1;
173
+ } else if (historyIndex > 0) {
174
+ historyIndex--;
175
+ } else {
176
+ return; // already at oldest
177
+ }
178
+ text = inputHistory[historyIndex];
179
+ cursor = 0;
180
+ }
181
+
182
+ function historyDown() {
183
+ if (historyIndex === -1) return;
184
+ if (historyIndex < inputHistory.length - 1) {
185
+ historyIndex++;
186
+ text = inputHistory[historyIndex];
187
+ cursor = text.length;
188
+ } else {
189
+ // Past last entry → restore saved text
190
+ text = savedText;
191
+ cursor = text.length;
192
+ historyIndex = -1;
193
+ }
194
+ }
195
+
196
+ // --- Setup ---
197
+
198
+ readline.emitKeypressEvents(stdin);
199
+ const wasRaw = stdin.isRaw;
200
+ stdin.setRawMode(true);
201
+ stdin.resume();
202
+ stdout.write('\x1b[?25l'); // hide native cursor
203
+
204
+ // Start blink interval (500ms on, 500ms off = 1s cycle)
205
+ blinkInterval = setInterval(() => {
206
+ // Skip blink render if user is actively typing (prevents flicker)
207
+ if (Date.now() - lastKeypressTime < 100) return;
208
+ cursorVisible = !cursorVisible;
209
+ render();
210
+ }, 500);
211
+
212
+ function cleanup() {
213
+ if (blinkInterval) { clearInterval(blinkInterval); blinkInterval = null; }
214
+ stdin.removeListener('keypress', onKeypress);
215
+ stdin.setRawMode(wasRaw);
216
+ stdout.write('\x1b[?25h'); // restore native cursor
217
+ }
218
+
219
+ function submit() {
220
+ cleanup();
221
+
222
+ // Add to history if non-empty
223
+ if (text.trim()) {
224
+ inputHistory.push(text);
225
+ }
226
+ historyIndex = -1;
227
+
228
+ // Clear editor area and show final text (no cursor)
229
+ if (prevLineCount > 1) {
230
+ stdout.write(`\x1b[${prevLineCount - 1}A`);
231
+ }
232
+ stdout.write('\r');
233
+ for (let i = 0; i < prevLineCount; i++) {
234
+ stdout.write('\x1b[2K');
235
+ if (i < prevLineCount - 1) stdout.write('\n');
236
+ }
237
+ if (prevLineCount > 1) {
238
+ stdout.write(`\x1b[${prevLineCount - 1}A`);
239
+ }
240
+ stdout.write('\r');
241
+
242
+ // Write final output (let terminal wrap naturally)
243
+ stdout.write(`\x1b[1m${prompt}\x1b[0m\x1b[36m${text}\x1b[0m\n`);
244
+ resolve(text.trim());
245
+ }
246
+
247
+ function resetBlink() {
248
+ cursorVisible = true;
249
+ lastKeypressTime = Date.now();
250
+ if (blinkInterval) clearInterval(blinkInterval);
251
+ blinkInterval = setInterval(() => {
252
+ // Skip blink render if user is actively typing (prevents flicker)
253
+ if (Date.now() - lastKeypressTime < 100) return;
254
+ cursorVisible = !cursorVisible;
255
+ render();
256
+ }, 500);
257
+ }
258
+
259
+ function onKeypress(str, key) {
260
+ resetBlink();
261
+
262
+ if (key) {
263
+ // --- Submit / Cancel ---
264
+ if (key.name === 'return') { submit(); return; }
265
+ if (key.name === 'escape' || (key.ctrl && key.name === 'c')) {
266
+ cleanup();
267
+ stdout.write('\r\x1b[2K');
268
+ stdout.write(`\x1b[1m${prompt}\x1b[0m\n`);
269
+ resolve('');
270
+ return;
271
+ }
272
+
273
+ // --- Up / Down ---
274
+ if (key.name === 'up') {
275
+ const { row, col } = cursorToRowCol(cursor);
276
+ if (row > 0) {
277
+ // Move to previous visual line, clamp column
278
+ const targetCol = Math.min(col, lineLen(row - 1));
279
+ cursor = rowColToCursor(row - 1, targetCol);
280
+ } else if (cursor > 0) {
281
+ // First line, not at start → go to start
282
+ cursor = 0;
283
+ } else {
284
+ // At start → history up
285
+ historyUp();
286
+ }
287
+ render();
288
+ return;
289
+ }
290
+
291
+ if (key.name === 'down') {
292
+ const { row, col } = cursorToRowCol(cursor);
293
+ const lr = totalRows() - 1;
294
+ if (row < lr) {
295
+ // Move to next visual line, clamp column
296
+ const targetCol = Math.min(col, lineLen(row + 1));
297
+ cursor = rowColToCursor(row + 1, targetCol);
298
+ } else if (cursor < text.length) {
299
+ // Last line, not at end → go to end
300
+ cursor = text.length;
301
+ } else {
302
+ // At end → history down
303
+ historyDown();
304
+ }
305
+ render();
306
+ return;
307
+ }
308
+
309
+ // --- Left / Right ---
310
+ if (key.name === 'left') {
311
+ if (key.meta || key.ctrl) {
312
+ cursor = prevWordBoundary(text, cursor);
313
+ } else {
314
+ cursor = Math.max(0, cursor - 1);
315
+ }
316
+ render(); return;
317
+ }
318
+ if (key.name === 'right') {
319
+ if (key.meta || key.ctrl) {
320
+ cursor = nextWordBoundary(text, cursor);
321
+ } else {
322
+ cursor = Math.min(text.length, cursor + 1);
323
+ }
324
+ render(); return;
325
+ }
326
+
327
+ // ESC-b / ESC-f (word movement in some terminals)
328
+ if (key.meta && key.name === 'b') { cursor = prevWordBoundary(text, cursor); render(); return; }
329
+ if (key.meta && key.name === 'f') { cursor = nextWordBoundary(text, cursor); render(); return; }
330
+
331
+ // Home / End
332
+ if (key.name === 'home' || (key.ctrl && key.name === 'a')) { cursor = 0; render(); return; }
333
+ if (key.name === 'end' || (key.ctrl && key.name === 'e')) { cursor = text.length; render(); return; }
334
+
335
+ // --- Deletion ---
336
+ if (key.name === 'backspace') {
337
+ if (cursor > 0) {
338
+ if (key.meta || key.ctrl) {
339
+ const prev = prevWordBoundary(text, cursor);
340
+ text = text.substring(0, prev) + text.substring(cursor);
341
+ cursor = prev;
342
+ } else {
343
+ text = text.substring(0, cursor - 1) + text.substring(cursor);
344
+ cursor--;
345
+ }
346
+ render();
347
+ }
348
+ return;
349
+ }
350
+ if (key.name === 'delete' || (key.ctrl && key.name === 'd')) {
351
+ if (cursor < text.length) {
352
+ text = text.substring(0, cursor) + text.substring(cursor + 1);
353
+ render();
354
+ }
355
+ return;
356
+ }
357
+ if (key.ctrl && key.name === 'u') {
358
+ text = text.substring(cursor); cursor = 0; render(); return;
359
+ }
360
+ if (key.ctrl && key.name === 'k') {
361
+ text = text.substring(0, cursor); render(); return;
362
+ }
363
+ if (key.ctrl && key.name === 'w') {
364
+ if (cursor > 0) {
365
+ const prev = prevWordBoundary(text, cursor);
366
+ text = text.substring(0, prev) + text.substring(cursor);
367
+ cursor = prev;
368
+ render();
369
+ }
370
+ return;
371
+ }
372
+
373
+ // Skip other control/meta keys
374
+ if (key.ctrl || key.meta) return;
375
+ }
376
+
377
+ // Regular character input
378
+ if (str && str.length > 0 && str.charCodeAt(0) >= 32) {
379
+ let ch = str;
380
+ // Handle dead key combining characters (macOS international keyboards)
381
+ const code = str.charCodeAt(0);
382
+ if (code >= 0x0300 && code <= 0x036F) {
383
+ const deadKeyMap = {
384
+ 0x0300: '`', // combining grave → backtick
385
+ 0x0301: "'", // combining acute → apostrophe
386
+ 0x0302: '^', // combining circumflex → caret
387
+ 0x0303: '~', // combining tilde → tilde
388
+ 0x0308: '"', // combining diaeresis → double quote
389
+ };
390
+ ch = deadKeyMap[code] || '';
391
+ }
392
+ if (ch) {
393
+ text = text.substring(0, cursor) + ch + text.substring(cursor);
394
+ cursor += ch.length;
395
+ render();
396
+ }
397
+ }
398
+ }
399
+
400
+ stdin.on('keypress', onKeypress);
401
+ render();
402
+ });
403
+ }
404
+
405
+ // --- Word boundary helpers ---
406
+
407
+ function prevWordBoundary(text, pos) {
408
+ if (pos <= 0) return 0;
409
+ let i = pos - 1;
410
+ while (i > 0 && text[i] === ' ') i--;
411
+ while (i > 0 && text[i - 1] !== ' ') i--;
412
+ return i;
413
+ }
414
+
415
+ function nextWordBoundary(text, pos) {
416
+ if (pos >= text.length) return text.length;
417
+ let i = pos;
418
+ while (i < text.length && text[i] !== ' ') i++;
419
+ while (i < text.length && text[i] === ' ') i++;
420
+ return i;
421
+ }
@@ -17,7 +17,6 @@ class CLILogger {
17
17
  this.isAnimating = false; // Track if we're in animation mode
18
18
  this.indentStack = []; // Stack of messages showing delegation hierarchy
19
19
  this.indentLevel = 0;
20
-
21
20
  // Intercept console methods to auto-clear progress
22
21
  this.setupConsoleIntercept();
23
22
  }
@@ -141,10 +140,8 @@ class CLILogger {
141
140
  this.stopAnimation();
142
141
  }
143
142
 
144
- // Clear previous line
145
- if (this.isProgress) {
146
- process.stdout.write('\r\x1b[K');
147
- }
143
+ // Always clear current line (handles parent process leftovers like "🌊 Running...")
144
+ process.stdout.write('\r\x1b[K');
148
145
 
149
146
  // Write new message
150
147
  process.stdout.write(message);
@@ -0,0 +1,61 @@
1
+ /**
2
+ * CLI Markdown - Renders basic markdown to ANSI-formatted terminal text.
3
+ *
4
+ * Supports:
5
+ * - **bold** → ANSI bold
6
+ * - *italic* → ANSI italic
7
+ * - `code` → ANSI cyan
8
+ * - # Heading → bold + underline
9
+ * - ## Heading → bold
10
+ * - ### Heading → bold
11
+ * - --- / ___ / *** → horizontal rule
12
+ */
13
+
14
+ /**
15
+ * Convert markdown text to ANSI-formatted terminal output.
16
+ * @param {string} text - Raw markdown text
17
+ * @returns {string} ANSI-formatted text
18
+ */
19
+ export function renderMarkdown(text) {
20
+ if (!text || typeof text !== 'string') return text;
21
+
22
+ const lines = text.split('\n');
23
+ const rendered = lines.map(line => renderLine(line));
24
+ return rendered.join('\n');
25
+ }
26
+
27
+ function renderLine(line) {
28
+ const trimmed = line.trim();
29
+
30
+ // Horizontal rules: ---, ___, ***
31
+ if (/^[-_*]{3,}\s*$/.test(trimmed)) {
32
+ const cols = process.stdout.columns || parseInt(process.env.COLUMNS) || 80;
33
+ return '\x1b[2m' + '─'.repeat(Math.min(cols, 60)) + '\x1b[0m';
34
+ }
35
+
36
+ // Headings
37
+ if (trimmed.startsWith('### ')) {
38
+ return '\x1b[1m' + renderInline(trimmed.substring(4)) + '\x1b[0m';
39
+ }
40
+ if (trimmed.startsWith('## ')) {
41
+ return '\x1b[1m' + renderInline(trimmed.substring(3)) + '\x1b[0m';
42
+ }
43
+ if (trimmed.startsWith('# ')) {
44
+ return '\x1b[1;4m' + renderInline(trimmed.substring(2)) + '\x1b[0m';
45
+ }
46
+
47
+ return renderInline(line);
48
+ }
49
+
50
+ function renderInline(text) {
51
+ // Inline code: `code` → cyan
52
+ text = text.replace(/`([^`]+)`/g, '\x1b[36m$1\x1b[39m');
53
+
54
+ // Bold: **text** → bold
55
+ text = text.replace(/\*\*([^*]+)\*\*/g, '\x1b[1m$1\x1b[22m');
56
+
57
+ // Italic: *text* → italic (but not ** which is already handled)
58
+ text = text.replace(/(?<!\*)\*([^*]+)\*(?!\*)/g, '\x1b[3m$1\x1b[23m');
59
+
60
+ return text;
61
+ }
@@ -0,0 +1,106 @@
1
+ /**
2
+ * CLI Select - Simple interactive select menu using raw ANSI codes.
3
+ *
4
+ * Replaces the `prompts` library's select component to avoid
5
+ * interference with cli-logger's console intercept.
6
+ *
7
+ * Uses process.stdout.write exclusively — no console.log/error calls.
8
+ * Redraws line-by-line (no full-screen clear) to avoid flicker.
9
+ */
10
+
11
+ import readline from 'readline';
12
+
13
+ /**
14
+ * Show an interactive select menu.
15
+ * @param {string} message - The question/prompt to display
16
+ * @param {Array<{title: string, value: any, description?: string}>} choices
17
+ * @param {number} [initial=0] - Initially selected index
18
+ * @returns {Promise<any>} The selected choice's value, or undefined if cancelled
19
+ */
20
+ export function cliSelect(message, choices, initial = 0) {
21
+ return new Promise((resolve) => {
22
+ let selected = initial;
23
+ const stdout = process.stdout;
24
+ const stdin = process.stdin;
25
+ let firstRender = true;
26
+
27
+ // Calculate actual terminal lines the header occupies (may wrap)
28
+ const termWidth = stdout.columns || 80;
29
+ const headerVisualLen = `? ${message} › Use arrow-keys. Return to submit.`.length;
30
+ const headerLines = Math.max(1, Math.ceil(headerVisualLen / termWidth));
31
+ const totalLines = headerLines + choices.length;
32
+
33
+ function render() {
34
+ // On re-render, move cursor to start of menu area and clear everything below
35
+ if (!firstRender) {
36
+ stdout.write(`\x1b[${totalLines}A`);
37
+ stdout.write('\x1b[J'); // Clear from cursor to end of screen
38
+ }
39
+
40
+ // Header line — clear line then write
41
+ stdout.write(`\x1b[2K? \x1b[1m${message}\x1b[0m \x1b[2m› Use arrow-keys. Return to submit.\x1b[0m\n`);
42
+
43
+ // Choices — clear each line individually then write
44
+ for (let i = 0; i < choices.length; i++) {
45
+ const choice = choices[i];
46
+ const desc = choice.description ? ` \x1b[2m- ${choice.description}\x1b[0m` : '';
47
+
48
+ if (i === selected) {
49
+ stdout.write(`\x1b[2K\x1b[36m❯ ${choice.title}${desc}\x1b[0m\n`);
50
+ } else {
51
+ stdout.write(`\x1b[2K ${choice.title}${desc}\n`);
52
+ }
53
+ }
54
+
55
+ firstRender = false;
56
+ }
57
+
58
+ // Enable raw mode for keypress detection
59
+ readline.emitKeypressEvents(stdin);
60
+ const wasRaw = stdin.isRaw;
61
+ stdin.setRawMode(true);
62
+ stdin.resume();
63
+
64
+ // Hide cursor
65
+ stdout.write('\x1b[?25l');
66
+
67
+ function cleanup() {
68
+ stdin.removeListener('keypress', onKeypress);
69
+ stdin.setRawMode(wasRaw);
70
+ // Show cursor
71
+ stdout.write('\x1b[?25h');
72
+ }
73
+
74
+ function onKeypress(str, key) {
75
+ if (!key) return;
76
+
77
+ if (key.name === 'up' || key.name === 'k') {
78
+ selected = selected > 0 ? selected - 1 : choices.length - 1;
79
+ render();
80
+ } else if (key.name === 'down' || key.name === 'j') {
81
+ selected = selected < choices.length - 1 ? selected + 1 : 0;
82
+ render();
83
+ } else if (key.name === 'return') {
84
+ cleanup();
85
+ // Overwrite menu with final selection (line-by-line clear)
86
+ stdout.write(`\x1b[${totalLines}A`);
87
+ stdout.write(`\x1b[2K? \x1b[1m${message}\x1b[0m \x1b[36m${choices[selected].title}\x1b[0m\n`);
88
+ for (let i = 1; i < totalLines; i++) {
89
+ stdout.write('\x1b[2K\n');
90
+ }
91
+ // Move back up past the blank lines
92
+ if (totalLines > 1) {
93
+ stdout.write(`\x1b[${totalLines - 1}A`);
94
+ }
95
+ resolve(choices[selected].value);
96
+ } else if (key.name === 'escape' || (key.ctrl && key.name === 'c')) {
97
+ cleanup();
98
+ stdout.write('\n');
99
+ resolve(undefined);
100
+ }
101
+ }
102
+
103
+ stdin.on('keypress', onKeypress);
104
+ render();
105
+ });
106
+ }