@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.
- package/README.md +4 -125
- package/examples/.build/agent-dialogue.ts +138 -0
- package/examples/.build/agent-dialogue.ts.map +1 -0
- package/examples/.build/chess.ts +77 -0
- package/examples/.build/chess.ts.map +1 -0
- package/examples/.build/delegation-test.ts +140 -0
- package/examples/.build/delegation-test.ts.map +1 -0
- package/examples/.build/dialog-demo.ts +77 -0
- package/examples/.build/dialog-demo.ts.map +1 -0
- package/examples/.build/hello-world.ts +77 -0
- package/examples/.build/hello-world.ts.map +1 -0
- package/examples/.build/lover-dialog-demo.ts +77 -0
- package/examples/.build/lover-dialog-demo.ts.map +1 -0
- package/examples/.build/package.json +3 -0
- package/examples/.build/registry-interactive-demo.ts +202 -0
- package/examples/.build/registry-interactive-demo.ts.map +1 -0
- package/examples/.build/registry-playbook-demo.ts +201 -0
- package/examples/.build/registry-playbook-demo.ts.map +1 -0
- package/examples/.build/tic-tac-toe.ts +77 -0
- package/examples/.build/tic-tac-toe.ts.map +1 -0
- package/examples/actions-demo.koi +8 -9
- package/examples/activists-dialogue.koi +75 -0
- package/examples/agent-dialogue.koi +66 -0
- package/examples/chess.koi +19 -0
- package/examples/counter.koi +20 -69
- package/examples/delegation-test.koi +16 -18
- package/examples/dialog-demo.koi +20 -0
- package/examples/hello-world.koi +7 -43
- package/examples/mcp-stdio-demo.koi +29 -0
- package/examples/memory-test.koi +49 -0
- package/examples/mobile-mcp-demo.koi +32 -0
- package/examples/multi-event-handler-test.koi +16 -18
- package/examples/pipeline.koi +15 -17
- package/examples/prompt-demo.koi +20 -0
- package/examples/{registry-playbook-email-compositor.koi → registry-interactive-demo.koi} +27 -27
- package/examples/registry-playbook-demo.koi +28 -28
- package/examples/skill-import-test.koi +7 -9
- package/examples/skills/.build/math-operations.ts +1656 -0
- package/examples/skills/.build/math-operations.ts.map +1 -0
- package/examples/skills/.build/package.json +3 -0
- package/examples/skills/.build/string-operations.ts +1643 -0
- package/examples/skills/.build/string-operations.ts.map +1 -0
- package/examples/skills/advanced/.build/index.ts +3223 -0
- package/examples/skills/advanced/.build/index.ts.map +1 -0
- package/examples/skills/advanced/.build/package.json +3 -0
- package/examples/skills/advanced/index.koi +3 -5
- package/examples/skills/math-operations.koi +1 -3
- package/examples/skills/string-operations.koi +1 -3
- package/examples/tic-tac-toe.koi +19 -0
- package/examples/utils/echo-mcp-server.js +141 -0
- package/examples/web-delegation-demo.koi +15 -17
- package/package.json +2 -1
- package/src/cli/koi.js +30 -41
- package/src/compiler/build-optimizer.js +204 -289
- package/src/compiler/cache-manager.js +1 -1
- package/src/compiler/import-resolver.js +5 -9
- package/src/compiler/parser.js +6072 -3476
- package/src/compiler/transpiler.js +346 -38
- package/src/grammar/koi.pegjs +302 -62
- package/src/runtime/actions/{format.js → call-llm.js} +37 -44
- package/src/runtime/actions/call-mcp.js +97 -0
- package/src/runtime/actions/if.js +179 -0
- package/src/runtime/actions/print.js +3 -1
- package/src/runtime/actions/prompt-user.js +75 -0
- package/src/runtime/actions/repeat.js +147 -0
- package/src/runtime/actions/shell.js +185 -0
- package/src/runtime/actions/while.js +205 -0
- package/src/runtime/agent.js +592 -178
- package/src/runtime/cli-display.js +26 -0
- package/src/runtime/cli-input.js +421 -0
- package/src/runtime/cli-logger.js +2 -5
- package/src/runtime/cli-markdown.js +61 -0
- package/src/runtime/cli-select.js +106 -0
- package/src/runtime/incremental-json-parser.js +27 -17
- package/src/runtime/index.js +1 -0
- package/src/runtime/llm-provider.js +1083 -572
- package/src/runtime/mcp-registry.js +141 -0
- package/src/runtime/mcp-stdio-client.js +334 -0
- package/src/runtime/planner.js +1 -1
- package/src/runtime/playbook-session.js +259 -0
- package/src/runtime/registry-backends/keyv-sqlite.js +1 -1
- package/src/runtime/registry-backends/local.js +1 -1
- package/src/runtime/router.js +22 -26
- package/src/runtime/runtime.js +7 -1
- package/examples/cache-test.koi +0 -29
- package/examples/calculator.koi +0 -61
- package/examples/clear-registry.js +0 -33
- package/examples/clear-registry.koi +0 -30
- package/examples/code-introspection-test.koi +0 -149
- package/examples/directory-import-test.koi +0 -84
- package/examples/hello-world-claude.koi +0 -52
- package/examples/hello.koi +0 -24
- package/examples/mcp-example.koi +0 -70
- package/examples/new-import-test.koi +0 -89
- package/examples/registry-demo.koi +0 -184
- package/examples/registry-playbook-email-compositor-2.koi +0 -140
- package/examples/sentiment.koi +0 -90
- package/examples/simple.koi +0 -48
- package/examples/task-chaining-demo.koi +0 -244
- package/examples/test-await.koi +0 -22
- package/examples/test-crypto-sha256.koi +0 -196
- package/examples/test-delegation.koi +0 -41
- package/examples/test-multi-team-routing.koi +0 -258
- package/examples/test-no-handler.koi +0 -35
- package/examples/test-npm-import.koi +0 -67
- package/examples/test-parse.koi +0 -10
- package/examples/test-peers-with-team.koi +0 -59
- package/examples/test-permissions-fail.koi +0 -20
- package/examples/test-permissions.koi +0 -36
- package/examples/test-simple-registry.koi +0 -31
- package/examples/test-typescript-import.koi +0 -64
- package/examples/test-uses-team-syntax.koi +0 -25
- 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
|
-
//
|
|
145
|
-
|
|
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
|
+
}
|