nex-code 0.3.4 → 0.3.7
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 +34 -12
- package/dist/bundle.js +505 -0
- package/dist/nex-code.js +485 -0
- package/package.json +8 -6
- package/bin/nex-code.js +0 -99
- package/cli/agent.js +0 -835
- package/cli/compactor.js +0 -85
- package/cli/context-engine.js +0 -507
- package/cli/context.js +0 -98
- package/cli/costs.js +0 -290
- package/cli/diff.js +0 -366
- package/cli/file-history.js +0 -94
- package/cli/format.js +0 -211
- package/cli/fuzzy-match.js +0 -270
- package/cli/git.js +0 -211
- package/cli/hooks.js +0 -173
- package/cli/index.js +0 -1289
- package/cli/mcp.js +0 -284
- package/cli/memory.js +0 -170
- package/cli/ollama.js +0 -130
- package/cli/permissions.js +0 -124
- package/cli/picker.js +0 -201
- package/cli/planner.js +0 -282
- package/cli/providers/anthropic.js +0 -333
- package/cli/providers/base.js +0 -116
- package/cli/providers/gemini.js +0 -239
- package/cli/providers/local.js +0 -249
- package/cli/providers/ollama.js +0 -228
- package/cli/providers/openai.js +0 -237
- package/cli/providers/registry.js +0 -454
- package/cli/render.js +0 -495
- package/cli/safety.js +0 -241
- package/cli/session.js +0 -133
- package/cli/skills.js +0 -412
- package/cli/spinner.js +0 -371
- package/cli/sub-agent.js +0 -425
- package/cli/tasks.js +0 -179
- package/cli/tool-tiers.js +0 -164
- package/cli/tool-validator.js +0 -138
- package/cli/tools.js +0 -1050
- package/cli/ui.js +0 -93
package/cli/spinner.js
DELETED
|
@@ -1,371 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* cli/spinner.js — Terminal Spinner and Progress Components
|
|
3
|
-
* Spinner, MultiProgress, TaskProgress classes for animated terminal output
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
const C = {
|
|
7
|
-
reset: '\x1b[0m',
|
|
8
|
-
bold: '\x1b[1m',
|
|
9
|
-
dim: '\x1b[2m',
|
|
10
|
-
white: '\x1b[37m',
|
|
11
|
-
red: '\x1b[31m',
|
|
12
|
-
green: '\x1b[32m',
|
|
13
|
-
yellow: '\x1b[33m',
|
|
14
|
-
blue: '\x1b[34m',
|
|
15
|
-
magenta: '\x1b[35m',
|
|
16
|
-
cyan: '\x1b[36m',
|
|
17
|
-
gray: '\x1b[90m',
|
|
18
|
-
bgRed: '\x1b[41m',
|
|
19
|
-
bgGreen: '\x1b[42m',
|
|
20
|
-
brightCyan: '\x1b[96m',
|
|
21
|
-
brightMagenta: '\x1b[95m',
|
|
22
|
-
brightBlue: '\x1b[94m',
|
|
23
|
-
};
|
|
24
|
-
|
|
25
|
-
const SPINNER_FRAMES = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'];
|
|
26
|
-
const TASK_FRAMES = ['✽', '✦', '✧', '✦'];
|
|
27
|
-
|
|
28
|
-
class Spinner {
|
|
29
|
-
constructor(text = 'Thinking...') {
|
|
30
|
-
this.text = text;
|
|
31
|
-
this.frame = 0;
|
|
32
|
-
this.interval = null;
|
|
33
|
-
this.startTime = null;
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
_render() {
|
|
37
|
-
if (this._stopped) return;
|
|
38
|
-
const f = SPINNER_FRAMES[this.frame % SPINNER_FRAMES.length];
|
|
39
|
-
let elapsed = '';
|
|
40
|
-
if (this.startTime) {
|
|
41
|
-
const totalSecs = Math.floor((Date.now() - this.startTime) / 1000);
|
|
42
|
-
if (totalSecs >= 60) {
|
|
43
|
-
const mins = Math.floor(totalSecs / 60);
|
|
44
|
-
const secs = totalSecs % 60;
|
|
45
|
-
elapsed = ` ${C.dim}${mins}m ${String(secs).padStart(2, '0')}s${C.reset}`;
|
|
46
|
-
} else if (totalSecs >= 1) {
|
|
47
|
-
elapsed = ` ${C.dim}${totalSecs}s${C.reset}`;
|
|
48
|
-
}
|
|
49
|
-
}
|
|
50
|
-
process.stderr.write(`\x1b[2K\r${C.cyan}${f}${C.reset} ${C.dim}${this.text}${C.reset}${elapsed}`);
|
|
51
|
-
this.frame++;
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
start() {
|
|
55
|
-
this._stopped = false;
|
|
56
|
-
this.startTime = Date.now();
|
|
57
|
-
process.stderr.write('\x1b[?25l'); // hide cursor
|
|
58
|
-
this._render(); // render first frame immediately
|
|
59
|
-
this.interval = setInterval(() => this._render(), 80);
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
update(text) {
|
|
63
|
-
this.text = text;
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
stop() {
|
|
67
|
-
this._stopped = true;
|
|
68
|
-
if (this.interval) {
|
|
69
|
-
clearInterval(this.interval);
|
|
70
|
-
this.interval = null;
|
|
71
|
-
}
|
|
72
|
-
// Single write: clear line + show cursor (avoids flicker)
|
|
73
|
-
process.stderr.write('\x1b[2K\r\x1b[?25h');
|
|
74
|
-
this.startTime = null;
|
|
75
|
-
}
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
// ─── MultiProgress ────────────────────────────────────────────
|
|
79
|
-
const MULTI_FRAMES = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'];
|
|
80
|
-
|
|
81
|
-
class MultiProgress {
|
|
82
|
-
/**
|
|
83
|
-
* @param {string[]} labels - Labels for each parallel task
|
|
84
|
-
*/
|
|
85
|
-
constructor(labels) {
|
|
86
|
-
this.labels = labels;
|
|
87
|
-
this.statuses = labels.map(() => 'running'); // 'running' | 'done' | 'error'
|
|
88
|
-
this.frame = 0;
|
|
89
|
-
this.interval = null;
|
|
90
|
-
this.startTime = null;
|
|
91
|
-
this.lineCount = labels.length;
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
_formatElapsed() {
|
|
95
|
-
if (!this.startTime) return '';
|
|
96
|
-
const totalSecs = Math.floor((Date.now() - this.startTime) / 1000);
|
|
97
|
-
if (totalSecs < 1) return '';
|
|
98
|
-
const mins = Math.floor(totalSecs / 60);
|
|
99
|
-
const secs = totalSecs % 60;
|
|
100
|
-
return mins > 0 ? `${mins}m ${String(secs).padStart(2, '0')}s` : `${secs}s`;
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
_render() {
|
|
104
|
-
if (this._stopped) return;
|
|
105
|
-
const f = MULTI_FRAMES[this.frame % MULTI_FRAMES.length];
|
|
106
|
-
const elapsed = this._formatElapsed();
|
|
107
|
-
const elapsedStr = elapsed ? ` ${C.dim}${elapsed}${C.reset}` : '';
|
|
108
|
-
let buf = '';
|
|
109
|
-
|
|
110
|
-
for (let i = 0; i < this.labels.length; i++) {
|
|
111
|
-
let icon, color;
|
|
112
|
-
switch (this.statuses[i]) {
|
|
113
|
-
case 'done':
|
|
114
|
-
icon = `${C.green}✓${C.reset}`;
|
|
115
|
-
color = C.dim;
|
|
116
|
-
break;
|
|
117
|
-
case 'error':
|
|
118
|
-
icon = `${C.red}✗${C.reset}`;
|
|
119
|
-
color = C.dim;
|
|
120
|
-
break;
|
|
121
|
-
default:
|
|
122
|
-
icon = `${C.cyan}${f}${C.reset}`;
|
|
123
|
-
color = '';
|
|
124
|
-
}
|
|
125
|
-
// Show elapsed on last line only
|
|
126
|
-
const suffix = (i === this.labels.length - 1) ? elapsedStr : '';
|
|
127
|
-
buf += `\x1b[2K ${icon} ${color}${this.labels[i]}${C.reset}${suffix}\n`;
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
// Move cursor back up to start of our block
|
|
131
|
-
if (this.lineCount > 0) {
|
|
132
|
-
buf += `\x1b[${this.lineCount}A`;
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
process.stderr.write(buf);
|
|
136
|
-
this.frame++;
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
start() {
|
|
140
|
-
this._stopped = false;
|
|
141
|
-
this.startTime = Date.now();
|
|
142
|
-
// Single buffered write: hide cursor + reserve lines + move back up
|
|
143
|
-
let buf = '\x1b[?25l';
|
|
144
|
-
for (let i = 0; i < this.lineCount; i++) buf += '\n';
|
|
145
|
-
if (this.lineCount > 0) buf += `\x1b[${this.lineCount}A`;
|
|
146
|
-
process.stderr.write(buf);
|
|
147
|
-
this._render();
|
|
148
|
-
this.interval = setInterval(() => this._render(), 80);
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
/**
|
|
152
|
-
* @param {number} index - Index of the task to update
|
|
153
|
-
* @param {'running'|'done'|'error'} status
|
|
154
|
-
*/
|
|
155
|
-
update(index, status) {
|
|
156
|
-
if (index >= 0 && index < this.statuses.length) {
|
|
157
|
-
this.statuses[index] = status;
|
|
158
|
-
}
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
stop() {
|
|
162
|
-
this._stopped = true;
|
|
163
|
-
if (this.interval) {
|
|
164
|
-
clearInterval(this.interval);
|
|
165
|
-
this.interval = null;
|
|
166
|
-
}
|
|
167
|
-
// Final render to show final states
|
|
168
|
-
this._renderFinal();
|
|
169
|
-
process.stderr.write('\x1b[?25h'); // show cursor
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
_renderFinal() {
|
|
173
|
-
const elapsed = this._formatElapsed();
|
|
174
|
-
const elapsedStr = elapsed ? ` ${C.dim}${elapsed}${C.reset}` : '';
|
|
175
|
-
let buf = '';
|
|
176
|
-
for (let i = 0; i < this.labels.length; i++) {
|
|
177
|
-
let icon;
|
|
178
|
-
switch (this.statuses[i]) {
|
|
179
|
-
case 'done':
|
|
180
|
-
icon = `${C.green}✓${C.reset}`;
|
|
181
|
-
break;
|
|
182
|
-
case 'error':
|
|
183
|
-
icon = `${C.red}✗${C.reset}`;
|
|
184
|
-
break;
|
|
185
|
-
default:
|
|
186
|
-
icon = `${C.yellow}○${C.reset}`;
|
|
187
|
-
}
|
|
188
|
-
const suffix = (i === this.labels.length - 1) ? elapsedStr : '';
|
|
189
|
-
buf += `\x1b[2K ${icon} ${C.dim}${this.labels[i]}${C.reset}${suffix}\n`;
|
|
190
|
-
}
|
|
191
|
-
process.stderr.write(buf);
|
|
192
|
-
}
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
// ─── TaskProgress ────────────────────────────────────────────
|
|
196
|
-
const TASK_ICONS = { done: '✔', in_progress: '◼', pending: '◻', failed: '✗' };
|
|
197
|
-
const TASK_COLORS = { done: C.green, in_progress: C.cyan, pending: C.dim, failed: C.red };
|
|
198
|
-
|
|
199
|
-
let _activeTaskProgress = null;
|
|
200
|
-
|
|
201
|
-
class TaskProgress {
|
|
202
|
-
/**
|
|
203
|
-
* @param {string} name - Header label for the task list
|
|
204
|
-
* @param {Array<{id: string, description: string, status: string}>} tasks
|
|
205
|
-
*/
|
|
206
|
-
constructor(name, tasks) {
|
|
207
|
-
this.name = name;
|
|
208
|
-
this.tasks = tasks.map(t => ({ id: t.id, description: t.description, status: t.status || 'pending' }));
|
|
209
|
-
this.frame = 0;
|
|
210
|
-
this.interval = null;
|
|
211
|
-
this.startTime = null;
|
|
212
|
-
this.tokens = 0;
|
|
213
|
-
this.lineCount = 1 + this.tasks.length; // header + task lines
|
|
214
|
-
this._paused = false;
|
|
215
|
-
}
|
|
216
|
-
|
|
217
|
-
_formatElapsed() {
|
|
218
|
-
if (!this.startTime) return '';
|
|
219
|
-
const totalSecs = Math.floor((Date.now() - this.startTime) / 1000);
|
|
220
|
-
if (totalSecs < 1) return '';
|
|
221
|
-
const mins = Math.floor(totalSecs / 60);
|
|
222
|
-
const secs = totalSecs % 60;
|
|
223
|
-
return mins > 0 ? `${mins}m ${String(secs).padStart(2, '0')}s` : `${secs}s`;
|
|
224
|
-
}
|
|
225
|
-
|
|
226
|
-
_formatTokens() {
|
|
227
|
-
if (this.tokens <= 0) return '';
|
|
228
|
-
if (this.tokens >= 1000) return `${(this.tokens / 1000).toFixed(1)}k`;
|
|
229
|
-
return String(this.tokens);
|
|
230
|
-
}
|
|
231
|
-
|
|
232
|
-
_render() {
|
|
233
|
-
if (this._stopped) return;
|
|
234
|
-
const f = TASK_FRAMES[this.frame % TASK_FRAMES.length];
|
|
235
|
-
const elapsed = this._formatElapsed();
|
|
236
|
-
const tokStr = this._formatTokens();
|
|
237
|
-
const stats = [elapsed, tokStr ? `↓ ${tokStr} tokens` : ''].filter(Boolean).join(' · ');
|
|
238
|
-
const statsStr = stats ? ` ${C.dim}(${stats})${C.reset}` : '';
|
|
239
|
-
|
|
240
|
-
let buf = `\x1b[2K${C.cyan}${f}${C.reset} ${this.name}…${statsStr}\n`;
|
|
241
|
-
|
|
242
|
-
for (let i = 0; i < this.tasks.length; i++) {
|
|
243
|
-
const t = this.tasks[i];
|
|
244
|
-
const connector = i === 0 ? '⎿' : ' ';
|
|
245
|
-
const icon = TASK_ICONS[t.status] || TASK_ICONS.pending;
|
|
246
|
-
const color = TASK_COLORS[t.status] || TASK_COLORS.pending;
|
|
247
|
-
const desc = t.description.length > 55 ? t.description.substring(0, 52) + '...' : t.description;
|
|
248
|
-
buf += `\x1b[2K ${C.dim}${connector}${C.reset} ${color}${icon}${C.reset} ${desc}\n`;
|
|
249
|
-
}
|
|
250
|
-
|
|
251
|
-
// Move cursor back up
|
|
252
|
-
buf += `\x1b[${this.lineCount}A`;
|
|
253
|
-
process.stderr.write(buf);
|
|
254
|
-
this.frame++;
|
|
255
|
-
}
|
|
256
|
-
|
|
257
|
-
start() {
|
|
258
|
-
this._stopped = false;
|
|
259
|
-
this.startTime = Date.now();
|
|
260
|
-
this._paused = false;
|
|
261
|
-
// Single buffered write: hide cursor + reserve lines + move back up
|
|
262
|
-
let buf = '\x1b[?25l';
|
|
263
|
-
for (let i = 0; i < this.lineCount; i++) buf += '\n';
|
|
264
|
-
buf += `\x1b[${this.lineCount}A`;
|
|
265
|
-
process.stderr.write(buf);
|
|
266
|
-
this._render();
|
|
267
|
-
this.interval = setInterval(() => this._render(), 120);
|
|
268
|
-
_activeTaskProgress = this;
|
|
269
|
-
}
|
|
270
|
-
|
|
271
|
-
stop() {
|
|
272
|
-
this._stopped = true;
|
|
273
|
-
if (this.interval) {
|
|
274
|
-
clearInterval(this.interval);
|
|
275
|
-
this.interval = null;
|
|
276
|
-
}
|
|
277
|
-
if (!this._paused) {
|
|
278
|
-
this._renderFinal();
|
|
279
|
-
}
|
|
280
|
-
process.stderr.write('\x1b[?25h');
|
|
281
|
-
this._paused = false;
|
|
282
|
-
if (_activeTaskProgress === this) _activeTaskProgress = null;
|
|
283
|
-
}
|
|
284
|
-
|
|
285
|
-
/** Erase the block from stderr so text streaming can happen cleanly */
|
|
286
|
-
pause() {
|
|
287
|
-
if (this._paused) return;
|
|
288
|
-
if (this.interval) {
|
|
289
|
-
clearInterval(this.interval);
|
|
290
|
-
this.interval = null;
|
|
291
|
-
}
|
|
292
|
-
// Single buffered write: clear all occupied lines + move back up
|
|
293
|
-
let buf = '';
|
|
294
|
-
for (let i = 0; i < this.lineCount; i++) buf += '\x1b[2K\n';
|
|
295
|
-
buf += `\x1b[${this.lineCount}A`;
|
|
296
|
-
process.stderr.write(buf);
|
|
297
|
-
this._paused = true;
|
|
298
|
-
}
|
|
299
|
-
|
|
300
|
-
/** Re-reserve lines and restart animation after a pause */
|
|
301
|
-
resume() {
|
|
302
|
-
if (!this._paused) return;
|
|
303
|
-
this._paused = false;
|
|
304
|
-
// Single buffered write: hide cursor + reserve lines + move back up
|
|
305
|
-
let buf = '\x1b[?25l';
|
|
306
|
-
for (let i = 0; i < this.lineCount; i++) buf += '\n';
|
|
307
|
-
buf += `\x1b[${this.lineCount}A`;
|
|
308
|
-
process.stderr.write(buf);
|
|
309
|
-
this._render();
|
|
310
|
-
this.interval = setInterval(() => this._render(), 120);
|
|
311
|
-
}
|
|
312
|
-
|
|
313
|
-
/**
|
|
314
|
-
* @param {string} id - Task ID
|
|
315
|
-
* @param {string} status - 'pending' | 'in_progress' | 'done' | 'failed'
|
|
316
|
-
*/
|
|
317
|
-
updateTask(id, status) {
|
|
318
|
-
const t = this.tasks.find(task => task.id === id);
|
|
319
|
-
if (t) t.status = status;
|
|
320
|
-
}
|
|
321
|
-
|
|
322
|
-
setStats({ tokens }) {
|
|
323
|
-
if (tokens !== undefined) this.tokens = tokens;
|
|
324
|
-
}
|
|
325
|
-
|
|
326
|
-
isActive() {
|
|
327
|
-
return this.interval !== null || this._paused;
|
|
328
|
-
}
|
|
329
|
-
|
|
330
|
-
_renderFinal() {
|
|
331
|
-
const elapsed = this._formatElapsed();
|
|
332
|
-
const done = this.tasks.filter(t => t.status === 'done').length;
|
|
333
|
-
const failed = this.tasks.filter(t => t.status === 'failed').length;
|
|
334
|
-
const total = this.tasks.length;
|
|
335
|
-
const summary = failed > 0 ? `${done}/${total} done, ${failed} failed` : `${done}/${total} done`;
|
|
336
|
-
|
|
337
|
-
let buf = `\x1b[2K${C.green}✔${C.reset} ${this.name} ${C.dim}(${elapsed} · ${summary})${C.reset}\n`;
|
|
338
|
-
for (let i = 0; i < this.tasks.length; i++) {
|
|
339
|
-
const t = this.tasks[i];
|
|
340
|
-
const connector = i === 0 ? '⎿' : ' ';
|
|
341
|
-
const icon = TASK_ICONS[t.status] || TASK_ICONS.pending;
|
|
342
|
-
const color = TASK_COLORS[t.status] || TASK_COLORS.pending;
|
|
343
|
-
const desc = t.description.length > 55 ? t.description.substring(0, 52) + '...' : t.description;
|
|
344
|
-
buf += `\x1b[2K ${C.dim}${connector}${C.reset} ${color}${icon}${C.reset} ${C.dim}${desc}${C.reset}\n`;
|
|
345
|
-
}
|
|
346
|
-
process.stderr.write(buf);
|
|
347
|
-
}
|
|
348
|
-
}
|
|
349
|
-
|
|
350
|
-
function setActiveTaskProgress(tp) {
|
|
351
|
-
_activeTaskProgress = tp;
|
|
352
|
-
}
|
|
353
|
-
|
|
354
|
-
function getActiveTaskProgress() {
|
|
355
|
-
return _activeTaskProgress;
|
|
356
|
-
}
|
|
357
|
-
|
|
358
|
-
/**
|
|
359
|
-
* Restore terminal to a clean state (show cursor, clear spinner line).
|
|
360
|
-
* Call this on SIGINT or unexpected exit to avoid broken terminal.
|
|
361
|
-
*/
|
|
362
|
-
function cleanupTerminal() {
|
|
363
|
-
if (_activeTaskProgress) {
|
|
364
|
-
_activeTaskProgress.stop();
|
|
365
|
-
_activeTaskProgress = null;
|
|
366
|
-
}
|
|
367
|
-
// Single write: show cursor + clear line (avoids flicker)
|
|
368
|
-
process.stderr.write('\x1b[?25h\x1b[2K\r');
|
|
369
|
-
}
|
|
370
|
-
|
|
371
|
-
module.exports = { C, Spinner, MultiProgress, TaskProgress, setActiveTaskProgress, getActiveTaskProgress, cleanupTerminal };
|