@aragon-agent/cli 0.6.11 → 0.6.12
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/CHANGELOG.md +10 -1
- package/README.md +24 -13
- package/dist/agent/controller.d.ts +9 -2
- package/dist/agent/controller.js +29 -39
- package/dist/agent/queued-messages.d.ts +8 -0
- package/dist/agent/queued-messages.js +28 -0
- package/dist/agent/reducer.d.ts +8 -1
- package/dist/agent/reducer.js +23 -34
- package/dist/commands/builtins.js +21 -7
- package/dist/input/enter-sequences.d.ts +11 -10
- package/dist/input/enter-sequences.js +29 -27
- package/dist/input/stdin-filter.js +90 -26
- package/dist/logging/install.d.ts +3 -0
- package/dist/session/persist.d.ts +6 -7
- package/dist/session/persist.js +6 -9
- package/dist/session/store.d.ts +4 -1
- package/dist/session/validate-session.d.ts +3 -0
- package/dist/session/validate-session.js +207 -0
- package/dist/ui/App.js +55 -28
- package/dist/ui/Composer.d.ts +2 -1
- package/dist/ui/PromptInput.d.ts +2 -1
- package/dist/ui/PromptInput.js +50 -71
- package/dist/ui/QueueStatusRow.d.ts +22 -0
- package/dist/ui/QueueStatusRow.js +51 -0
- package/dist/ui/StatusBar.d.ts +2 -0
- package/dist/ui/StatusBar.js +30 -0
- package/dist/ui/composer-input.d.ts +28 -0
- package/dist/ui/composer-input.js +46 -0
- package/dist/ui/editor-reducer.d.ts +5 -0
- package/dist/ui/editor-reducer.js +2 -0
- package/dist/ui/enter-frames.d.ts +15 -31
- package/dist/ui/enter-frames.js +14 -33
- package/dist/ui/overlays/HelpOverlay.js +8 -0
- package/dist/ui/selection/selection-controller.js +57 -20
- package/dist/ui/transcript-text.js +20 -12
- package/package.json +2 -2
|
@@ -1,37 +1,21 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
* - a bare `\n` -- Ctrl+J, which Ink parses to `name: 'enter'` with
|
|
13
|
-
* `input === '\n'` and NO `key.return`, so it reaches this branch as an
|
|
14
|
-
* ordinary (control) character that must still mean "newline".
|
|
15
|
-
*
|
|
16
|
-
* I-12, THE BROADCAST OBLIGATION, APPLIES TO THESE FRAMES TOO. `useInput`
|
|
17
|
-
* re-emits every chunk to every mounted handler, `stripPasteFrames` does not
|
|
18
|
-
* know this frame, and a NUL appended into `SettingsScreen`'s API-key field
|
|
19
|
-
* renders as nothing and silently breaks authentication -- the exact accident
|
|
20
|
-
* class that function's own doc records. `stripEnterFrames` is the equivalent
|
|
21
|
-
* guard for overlays with no paste model of their own.
|
|
22
|
-
*/
|
|
23
|
-
import { type FrameSegment } from './paste-frames.js';
|
|
1
|
+
export type ComposerFrame = {
|
|
2
|
+
kind: 'text';
|
|
3
|
+
text: string;
|
|
4
|
+
} | {
|
|
5
|
+
kind: 'paste';
|
|
6
|
+
text: string;
|
|
7
|
+
} | {
|
|
8
|
+
kind: 'newline';
|
|
9
|
+
} | {
|
|
10
|
+
kind: 'submit';
|
|
11
|
+
};
|
|
24
12
|
/** True when this `input` carries at least one Enter frame -- the branch guard. */
|
|
25
13
|
export declare function hasEnterFrame(input: string): boolean;
|
|
26
14
|
/**
|
|
27
|
-
*
|
|
28
|
-
*
|
|
29
|
-
* Text-only by construction (paste frames are `mergeWithPasteRuns`'s job), so
|
|
30
|
-
* the paste id never has to exist here -- `PromptInput` allocates ids for paste
|
|
31
|
-
* segments while building the reducer's action, exactly as the paste branch
|
|
32
|
-
* does (P1-5: the reducer stays pure and this stays a function of its input).
|
|
15
|
+
* Decode paste regions before interpreting CR/LF or newline frames as keys.
|
|
16
|
+
* Paste IDs remain the caller's responsibility.
|
|
33
17
|
*/
|
|
34
|
-
export declare function splitEnterFrames(input: string):
|
|
18
|
+
export declare function splitEnterFrames(input: string): ComposerFrame[];
|
|
35
19
|
/**
|
|
36
20
|
* Merge newline runs with the paste frames that share the same `input`.
|
|
37
21
|
*
|
|
@@ -48,7 +32,7 @@ export declare function splitEnterFrames(input: string): FrameSegment[];
|
|
|
48
32
|
* Paste segments carry NO id here (P1-5); the caller assigns one per segment
|
|
49
33
|
* only when it builds the reducer's action.
|
|
50
34
|
*/
|
|
51
|
-
export declare function mergeWithPasteRuns(input: string, runs:
|
|
35
|
+
export declare function mergeWithPasteRuns(input: string, runs: ComposerFrame[]): ComposerFrame[];
|
|
52
36
|
/**
|
|
53
37
|
* Remove every Enter frame for a consumer that has no newline model of its
|
|
54
38
|
* own, replacing each frame with `'\n'` (tui-shift-enter-copy-queue 3.5, the
|
package/dist/ui/enter-frames.js
CHANGED
|
@@ -1,25 +1,3 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Reading Enter frames back out of an Ink `input` string
|
|
3
|
-
* (tui-shift-enter-copy-queue 3.5) -- pure.
|
|
4
|
-
*
|
|
5
|
-
* The stdin filter rewrites a modified Enter into `ENTER_NEWLINE_FRAME` inline,
|
|
6
|
-
* in stream order, exactly the way it frames a paste. This module is the only
|
|
7
|
-
* place that takes those frames apart again, and it handles BOTH newline
|
|
8
|
-
* sources the composer can see in one `input` string:
|
|
9
|
-
*
|
|
10
|
-
* - `ENTER_NEWLINE_FRAME` -- Shift+Enter / Alt+Enter / CSI-u Enter, translated
|
|
11
|
-
* by the filter; and
|
|
12
|
-
* - a bare `\n` -- Ctrl+J, which Ink parses to `name: 'enter'` with
|
|
13
|
-
* `input === '\n'` and NO `key.return`, so it reaches this branch as an
|
|
14
|
-
* ordinary (control) character that must still mean "newline".
|
|
15
|
-
*
|
|
16
|
-
* I-12, THE BROADCAST OBLIGATION, APPLIES TO THESE FRAMES TOO. `useInput`
|
|
17
|
-
* re-emits every chunk to every mounted handler, `stripPasteFrames` does not
|
|
18
|
-
* know this frame, and a NUL appended into `SettingsScreen`'s API-key field
|
|
19
|
-
* renders as nothing and silently breaks authentication -- the exact accident
|
|
20
|
-
* class that function's own doc records. `stripEnterFrames` is the equivalent
|
|
21
|
-
* guard for overlays with no paste model of their own.
|
|
22
|
-
*/
|
|
23
1
|
import { ENTER_NEWLINE_FRAME, PASTE_CLOSE, PASTE_OPEN } from '../input/limits.js';
|
|
24
2
|
import { sanitisePaste } from '../input/paste-parse.js';
|
|
25
3
|
import { sanitiseTyped } from './paste-frames.js';
|
|
@@ -28,10 +6,10 @@ export function hasEnterFrame(input) {
|
|
|
28
6
|
return input.includes(ENTER_NEWLINE_FRAME);
|
|
29
7
|
}
|
|
30
8
|
/**
|
|
31
|
-
* Split one
|
|
9
|
+
* Split one paste-free span into ordered text, newline and submit intents.
|
|
32
10
|
*
|
|
33
11
|
* Every newline source -- frame or bare `\n` -- yields ONE
|
|
34
|
-
* `{ kind: '
|
|
12
|
+
* `{ kind: 'newline' }` segment, so the editor's existing `input`
|
|
35
13
|
* action inserts it through the same path typing uses and the draft's row
|
|
36
14
|
* budget applies unchanged. Non-newline text is accumulated and passed through
|
|
37
15
|
* `sanitiseTyped` (I-14): a chunk that carries a frame can also carry the
|
|
@@ -51,13 +29,19 @@ function splitEnterSpan(span) {
|
|
|
51
29
|
while (i < span.length) {
|
|
52
30
|
if (span.startsWith(ENTER_NEWLINE_FRAME, i)) {
|
|
53
31
|
pushText();
|
|
54
|
-
segments.push({ kind: '
|
|
32
|
+
segments.push({ kind: 'newline' });
|
|
55
33
|
i += ENTER_NEWLINE_FRAME.length;
|
|
56
34
|
continue;
|
|
57
35
|
}
|
|
36
|
+
if (span[i] === '\r') {
|
|
37
|
+
pushText();
|
|
38
|
+
segments.push({ kind: 'submit' });
|
|
39
|
+
i += 1;
|
|
40
|
+
continue;
|
|
41
|
+
}
|
|
58
42
|
if (span[i] === '\n') {
|
|
59
43
|
pushText();
|
|
60
|
-
segments.push({ kind: '
|
|
44
|
+
segments.push({ kind: 'newline' });
|
|
61
45
|
i += 1;
|
|
62
46
|
continue;
|
|
63
47
|
}
|
|
@@ -68,15 +52,12 @@ function splitEnterSpan(span) {
|
|
|
68
52
|
return segments;
|
|
69
53
|
}
|
|
70
54
|
/**
|
|
71
|
-
*
|
|
72
|
-
*
|
|
73
|
-
* Text-only by construction (paste frames are `mergeWithPasteRuns`'s job), so
|
|
74
|
-
* the paste id never has to exist here -- `PromptInput` allocates ids for paste
|
|
75
|
-
* segments while building the reducer's action, exactly as the paste branch
|
|
76
|
-
* does (P1-5: the reducer stays pure and this stays a function of its input).
|
|
55
|
+
* Decode paste regions before interpreting CR/LF or newline frames as keys.
|
|
56
|
+
* Paste IDs remain the caller's responsibility.
|
|
77
57
|
*/
|
|
78
58
|
export function splitEnterFrames(input) {
|
|
79
|
-
return
|
|
59
|
+
return input.includes(PASTE_OPEN)
|
|
60
|
+
? mergeWithPasteRuns(input, []) : splitEnterSpan(input);
|
|
80
61
|
}
|
|
81
62
|
/**
|
|
82
63
|
* Merge newline runs with the paste frames that share the same `input`.
|
|
@@ -6,6 +6,7 @@ function keyRows(times) {
|
|
|
6
6
|
return [
|
|
7
7
|
['Enter', 'Submit (idle) / queue steering (running); re-pins to the newest output'],
|
|
8
8
|
['Alt+Enter / Shift+Enter', 'Insert newline'],
|
|
9
|
+
['Ctrl+J', 'Newline fallback; /terminal-setup explains Shift+Enter binding'],
|
|
9
10
|
['Esc', 'Close menu / overlay'],
|
|
10
11
|
['Esc twice', 'Interrupt run (within 1.5s)'],
|
|
11
12
|
['Esc again', 'Force-stop after requesting interruption'],
|
|
@@ -30,6 +31,12 @@ function keyRows(times) {
|
|
|
30
31
|
['Run status row', 'Spinner and queue / interrupt keys, above the input during a run'],
|
|
31
32
|
['Drag text (left button)', 'Select text on screen; the highlight waits'],
|
|
32
33
|
['Ctrl+C (with a selection)', 'Copy the pending selection - does not arm exit'],
|
|
34
|
+
['Native terminal selection', 'Copy-on-select is controlled by your terminal settings'],
|
|
35
|
+
['Queue', 'Waits for a message receipt, not turn start; paused means not running'],
|
|
36
|
+
['P>B / B>P', 'A pending mode switch; P=plan, B=build'],
|
|
37
|
+
['E2 / E!', 'Esc twice to interrupt / Esc again to force-stop'],
|
|
38
|
+
['S+ / T+', 'Services / TODO present; /bg and /todo status show counts'],
|
|
39
|
+
['Queue(p) / (+99+)', 'Paused queue / at least 100 additional messages'],
|
|
33
40
|
['Up / Down', 'Prompt history (empty input)'],
|
|
34
41
|
// Pasting is the one affordance here with no key of its own, so the only
|
|
35
42
|
// place a user can learn what happened to their 218 lines is this row.
|
|
@@ -58,6 +65,7 @@ const COMMANDS = [
|
|
|
58
65
|
['/resume [file]', 'Load a saved session'],
|
|
59
66
|
['/copy', 'Copy the last answer to the clipboard (OSC 52, so it works over ssh)'],
|
|
60
67
|
['/mouse [on|off]', 'Release the mouse to your terminal, or take it back'],
|
|
68
|
+
['/terminal-setup', 'Show newline key bindings without changing terminal settings'],
|
|
61
69
|
['/exit', 'Exit'],
|
|
62
70
|
];
|
|
63
71
|
const SKILL_ROWS = [
|
|
@@ -27,9 +27,10 @@
|
|
|
27
27
|
* same `decorate` call. What you see is what you get by construction, rather
|
|
28
28
|
* than by a staleness check that can be wrong.
|
|
29
29
|
*/
|
|
30
|
+
import stripAnsi from 'strip-ansi';
|
|
30
31
|
import { createScreenMirror } from './screen-mirror.js';
|
|
31
32
|
import { paintRow, selectionOpen } from './highlight.js';
|
|
32
|
-
import { isEmpty, normalize, rowSpan, selectedText, } from './selection.js';
|
|
33
|
+
import { isEmpty, normalize, rowSpan, selectedText, sliceColumns, } from './selection.js';
|
|
33
34
|
/**
|
|
34
35
|
* Motion coalescing window. The same 16 ms and the same reasoning as
|
|
35
36
|
* `use-wheel-routing.ts`: this one is in the direct path of a hand gesture, so
|
|
@@ -67,10 +68,28 @@ export function createSelectionController(options) {
|
|
|
67
68
|
let disposed = false;
|
|
68
69
|
/** Guards `decorate` against being re-entered from inside a repaint. */
|
|
69
70
|
let painting = false;
|
|
71
|
+
let notifiedHold = false;
|
|
72
|
+
let holdNotificationPending = false;
|
|
73
|
+
const notifyHold = () => {
|
|
74
|
+
if (disposed || notifiedHold === holdActive)
|
|
75
|
+
return;
|
|
76
|
+
notifiedHold = holdActive;
|
|
77
|
+
for (const listener of [...holdListeners])
|
|
78
|
+
listener(holdActive);
|
|
79
|
+
};
|
|
70
80
|
const emitHold = (hold) => {
|
|
71
81
|
holdActive = hold;
|
|
72
|
-
|
|
73
|
-
|
|
82
|
+
if (!painting) {
|
|
83
|
+
notifyHold();
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
if (holdNotificationPending)
|
|
87
|
+
return;
|
|
88
|
+
holdNotificationPending = true;
|
|
89
|
+
queueMicrotask(() => {
|
|
90
|
+
holdNotificationPending = false;
|
|
91
|
+
notifyHold();
|
|
92
|
+
});
|
|
74
93
|
};
|
|
75
94
|
const clearFlushTimer = () => {
|
|
76
95
|
if (!flushTimer)
|
|
@@ -97,13 +116,7 @@ export function createSelectionController(options) {
|
|
|
97
116
|
holdWatchdog = null;
|
|
98
117
|
if (!dragging)
|
|
99
118
|
return;
|
|
100
|
-
|
|
101
|
-
// still want to copy it, and throwing it away would turn a terminal quirk
|
|
102
|
-
// into lost work.
|
|
103
|
-
dragging = false;
|
|
104
|
-
pendingFocus = null;
|
|
105
|
-
clearFlushTimer();
|
|
106
|
-
emitHold(false);
|
|
119
|
+
clear();
|
|
107
120
|
}, holdMaxMs);
|
|
108
121
|
holdWatchdog.unref?.();
|
|
109
122
|
};
|
|
@@ -122,7 +135,7 @@ export function createSelectionController(options) {
|
|
|
122
135
|
if (!painted && wanted)
|
|
123
136
|
options.requestRedraw();
|
|
124
137
|
};
|
|
125
|
-
const
|
|
138
|
+
const clearState = () => {
|
|
126
139
|
// Nothing to undo: no timers, no repaint, no redraw. This is what keeps
|
|
127
140
|
// `onInvalidate` — which fires on every resize and every foreign write —
|
|
128
141
|
// free for a session that has never selected anything.
|
|
@@ -139,7 +152,12 @@ export function createSelectionController(options) {
|
|
|
139
152
|
// exit for two states, not one.
|
|
140
153
|
if (holdActive)
|
|
141
154
|
emitHold(false);
|
|
142
|
-
|
|
155
|
+
};
|
|
156
|
+
const clear = () => {
|
|
157
|
+
const hadSelection = sel !== null || holdActive;
|
|
158
|
+
clearState();
|
|
159
|
+
if (hadSelection)
|
|
160
|
+
paint(false);
|
|
143
161
|
};
|
|
144
162
|
const cellOf = (event) => {
|
|
145
163
|
// SGR reports are 1-based, frame line `i` is terminal row `i + 1`. A report
|
|
@@ -200,8 +218,7 @@ export function createSelectionController(options) {
|
|
|
200
218
|
// a selection — N1 already declines auto-scroll-while-dragging. The notch
|
|
201
219
|
// then scrolls normally, because the wheel router is a separate subscriber
|
|
202
220
|
// on the same channel.
|
|
203
|
-
|
|
204
|
-
clear();
|
|
221
|
+
clear();
|
|
205
222
|
return;
|
|
206
223
|
}
|
|
207
224
|
if (event.kind === 'press') {
|
|
@@ -213,6 +230,7 @@ export function createSelectionController(options) {
|
|
|
213
230
|
return;
|
|
214
231
|
}
|
|
215
232
|
const cell = cellOf(event);
|
|
233
|
+
settled = false;
|
|
216
234
|
sel = { anchor: cell, focus: cell };
|
|
217
235
|
pendingFocus = null;
|
|
218
236
|
clearFlushTimer();
|
|
@@ -248,16 +266,35 @@ export function createSelectionController(options) {
|
|
|
248
266
|
finishSelection();
|
|
249
267
|
};
|
|
250
268
|
const unsubscribe = options.source.subscribe(onMouse);
|
|
269
|
+
const selectedCellsChanged = (lines) => {
|
|
270
|
+
if (!settled || !sel)
|
|
271
|
+
return false;
|
|
272
|
+
const normal = normalize(sel);
|
|
273
|
+
for (let row = normal.start.row; row <= normal.end.row; row += 1) {
|
|
274
|
+
const span = rowSpan(normal, row);
|
|
275
|
+
if (!span)
|
|
276
|
+
continue;
|
|
277
|
+
const before = sliceColumns(stripAnsi(mirror.raw[row] ?? ''), span.from, span.to);
|
|
278
|
+
const after = sliceColumns(stripAnsi(lines[row] ?? ''), span.from, span.to);
|
|
279
|
+
if (before !== after)
|
|
280
|
+
return true;
|
|
281
|
+
}
|
|
282
|
+
return false;
|
|
283
|
+
};
|
|
251
284
|
return {
|
|
252
285
|
decorate(lines) {
|
|
253
|
-
|
|
254
|
-
if (painting || sel === null)
|
|
255
|
-
return lines;
|
|
256
|
-
const normal = normalize(sel);
|
|
257
|
-
if (isEmpty(normal))
|
|
286
|
+
if (painting || disposed)
|
|
258
287
|
return lines;
|
|
259
288
|
painting = true;
|
|
260
289
|
try {
|
|
290
|
+
if (selectedCellsChanged(lines))
|
|
291
|
+
clearState();
|
|
292
|
+
mirror.set(lines);
|
|
293
|
+
if (sel === null)
|
|
294
|
+
return lines;
|
|
295
|
+
const normal = normalize(sel);
|
|
296
|
+
if (isEmpty(normal))
|
|
297
|
+
return lines;
|
|
261
298
|
const open = selectionOpen(options.theme(), options.caps);
|
|
262
299
|
const cols = Math.max(1, options.cols());
|
|
263
300
|
const out = lines.slice();
|
|
@@ -317,11 +354,11 @@ export function createSelectionController(options) {
|
|
|
317
354
|
dispose() {
|
|
318
355
|
if (disposed)
|
|
319
356
|
return;
|
|
320
|
-
disposed = true;
|
|
321
357
|
// 4.2.3: a pending selection holds the viewport; teardown must let go
|
|
322
358
|
// of it explicitly, exactly like `clear()` does.
|
|
323
359
|
if (holdActive)
|
|
324
360
|
emitHold(false);
|
|
361
|
+
disposed = true;
|
|
325
362
|
unsubscribe();
|
|
326
363
|
clearFlushTimer();
|
|
327
364
|
clearWatchdog();
|
|
@@ -73,6 +73,8 @@ function serviceTextGlyph(status, exitCode, glyphs) {
|
|
|
73
73
|
}
|
|
74
74
|
function renderEntry(entry, glyphs) {
|
|
75
75
|
switch (entry.kind) {
|
|
76
|
+
case 'queued':
|
|
77
|
+
return block('Queued but never sent:', entry.text);
|
|
76
78
|
case 'user':
|
|
77
79
|
return block(glyphs.user, entry.text);
|
|
78
80
|
case 'assistant': {
|
|
@@ -211,20 +213,26 @@ export function renderTranscriptText(entries, opts) {
|
|
|
211
213
|
lines.push(`${g.ellipsis} ${dropped} earlier ${dropped === 1 ? 'entry' : 'entries'} dropped by ` +
|
|
212
214
|
`transcriptRetain ${g.midDot} raise it or /save sooner`);
|
|
213
215
|
}
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
216
|
+
const historyCount = entries.filter((entry) => entry.kind !== 'queued').length;
|
|
217
|
+
const omitted = max > 1 ? Math.max(0, historyCount - max) : 0;
|
|
218
|
+
const head = Math.ceil(max / 2);
|
|
219
|
+
const tailStart = historyCount - (max - head);
|
|
220
|
+
let historyIndex = 0;
|
|
221
|
+
let reported = false;
|
|
222
|
+
for (const entry of entries) {
|
|
223
|
+
if (entry.kind === 'queued') {
|
|
222
224
|
lines.push(...renderEntry(entry, g));
|
|
225
|
+
continue;
|
|
223
226
|
}
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
227
|
+
const index = historyIndex++;
|
|
228
|
+
if (omitted > 0 && index >= head && index < tailStart) {
|
|
229
|
+
if (!reported) {
|
|
230
|
+
lines.push(`${g.ellipsis} ${omitted} entries omitted ${g.midDot} use /save before exiting`);
|
|
231
|
+
reported = true;
|
|
232
|
+
}
|
|
233
|
+
continue;
|
|
234
|
+
}
|
|
235
|
+
lines.push(...renderEntry(entry, g));
|
|
228
236
|
}
|
|
229
237
|
return `${lines.join('\n')}\n`;
|
|
230
238
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@aragon-agent/cli",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.12",
|
|
4
4
|
"description": "AragonAgent CLI — a Claude-Code / Codex-style interactive terminal UI (TUI) on top of the @aragon-agent/core engine. Installable and runnable via npm / npx.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "AragonAgent contributors",
|
|
@@ -56,7 +56,7 @@
|
|
|
56
56
|
"prepublishOnly": "npm run build"
|
|
57
57
|
},
|
|
58
58
|
"dependencies": {
|
|
59
|
-
"@aragon-agent/core": "^0.2.
|
|
59
|
+
"@aragon-agent/core": "^0.2.24",
|
|
60
60
|
"chalk": "^5.6.2",
|
|
61
61
|
"cli-highlight": "^2.0.0",
|
|
62
62
|
"commander": "^12.1.0",
|