ikie-cli 0.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/LICENSE +21 -0
- package/README.md +35 -0
- package/dist/agent.d.ts +62 -0
- package/dist/agent.js +634 -0
- package/dist/attachments.d.ts +33 -0
- package/dist/attachments.js +239 -0
- package/dist/auth.d.ts +8 -0
- package/dist/auth.js +89 -0
- package/dist/config.d.ts +41 -0
- package/dist/config.js +69 -0
- package/dist/context.d.ts +13 -0
- package/dist/context.js +126 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +132 -0
- package/dist/memory.d.ts +14 -0
- package/dist/memory.js +71 -0
- package/dist/renderer.d.ts +7 -0
- package/dist/renderer.js +203 -0
- package/dist/repl.d.ts +3 -0
- package/dist/repl.js +948 -0
- package/dist/session.d.ts +21 -0
- package/dist/session.js +85 -0
- package/dist/theme.d.ts +66 -0
- package/dist/theme.js +365 -0
- package/dist/tools.d.ts +5 -0
- package/dist/tools.js +477 -0
- package/package.json +47 -0
package/dist/repl.js
ADDED
|
@@ -0,0 +1,948 @@
|
|
|
1
|
+
import readline from 'readline';
|
|
2
|
+
import { execSync } from 'child_process';
|
|
3
|
+
import { restoreStdinListeners } from './agent.js';
|
|
4
|
+
import { c, PROMPT, CONTINUE_PROMPT, printPromptHeader, drawBanner, infoLine, successLine, errorLine, THEMES, setTheme, stripAnsi, } from './theme.js';
|
|
5
|
+
import { renderMarkdown } from './renderer.js';
|
|
6
|
+
import { loadAllMemory } from './memory.js';
|
|
7
|
+
import { HOME_DIR, saveConfig, DEFAULT_MODEL, FIREWORKS_BASE_URL, IKIE_HOST } from './config.js';
|
|
8
|
+
import { join as pathJoin } from 'path';
|
|
9
|
+
import { deleteSession, listSessions, loadSession, normalizeSessionName, saveSession } from './session.js';
|
|
10
|
+
import { buildUserContent, formatBytes, loadClipboardImageAttachment, loadImageAttachment, hasClipboardImage } from './attachments.js';
|
|
11
|
+
async function fetchModelsFromServer(config) {
|
|
12
|
+
const baseUrl = config.baseURL || FIREWORKS_BASE_URL;
|
|
13
|
+
const apiUrl = baseUrl.includes('/api/v1')
|
|
14
|
+
? baseUrl.replace('/api/v1', '/api/models')
|
|
15
|
+
: `${IKIE_HOST}/api/models`;
|
|
16
|
+
const response = await fetch(apiUrl);
|
|
17
|
+
if (!response.ok)
|
|
18
|
+
throw new Error(`Failed to fetch models (${response.status})`);
|
|
19
|
+
const data = await response.json();
|
|
20
|
+
return data.models ?? [];
|
|
21
|
+
}
|
|
22
|
+
async function fetchAndDisplayModels(config) {
|
|
23
|
+
try {
|
|
24
|
+
console.log(c.muted('Fetching available models...'));
|
|
25
|
+
const models = await fetchModelsFromServer(config);
|
|
26
|
+
if (!models || models.length === 0) {
|
|
27
|
+
console.log(infoLine('No models available.'));
|
|
28
|
+
console.log(infoLine(`Current model: ${config.model}`));
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
console.log(`\n${c.primary.bold('Available Models')}\n`);
|
|
32
|
+
for (const model of models) {
|
|
33
|
+
const defaultLabel = model.is_default ? c.success(' [DEFAULT]') : '';
|
|
34
|
+
const contextInfo = c.muted(`(${(model.context_window / 1024).toFixed(0)}K context)`);
|
|
35
|
+
console.log(` ${c.secondary(model.name.padEnd(24))} ${c.white(model.display_name)}${defaultLabel} ${contextInfo}`);
|
|
36
|
+
console.log(` ${c.dim(model.provider_model_id)}`);
|
|
37
|
+
}
|
|
38
|
+
console.log(`\n${c.muted('To switch models:')}`);
|
|
39
|
+
console.log(` ${c.accent('/model')} ${c.muted('<name>')}`);
|
|
40
|
+
console.log(`\n${c.muted('Current model:')} ${c.white(config.model)}\n`);
|
|
41
|
+
}
|
|
42
|
+
catch (err) {
|
|
43
|
+
console.log(errorLine(`Error fetching models: ${err instanceof Error ? err.message : String(err)}`));
|
|
44
|
+
console.log(infoLine(`Current model: ${config.model}`));
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
const HISTORY_FILE = pathJoin(HOME_DIR, '.repl_history');
|
|
48
|
+
const MAX_HISTORY = 500;
|
|
49
|
+
const SLASH_CMDS = [
|
|
50
|
+
{ name: 'help', desc: 'Show commands' },
|
|
51
|
+
{ name: 'clear', desc: 'Clear conversation' },
|
|
52
|
+
{ name: 'memory', desc: 'View/save memory', subs: [{ name: 'save', desc: 'Save a note' }] },
|
|
53
|
+
{
|
|
54
|
+
name: 'image',
|
|
55
|
+
desc: 'Attach image to next prompt',
|
|
56
|
+
args: '[path|list|clear]',
|
|
57
|
+
subs: [
|
|
58
|
+
{ name: 'paste', desc: 'Attach clipboard image' },
|
|
59
|
+
{ name: 'list', desc: 'List queued images' },
|
|
60
|
+
{ name: 'clear', desc: 'Clear queued images' },
|
|
61
|
+
],
|
|
62
|
+
},
|
|
63
|
+
{
|
|
64
|
+
name: 'session',
|
|
65
|
+
desc: 'Save/load chat sessions',
|
|
66
|
+
subs: [
|
|
67
|
+
{ name: 'list', desc: 'List sessions' },
|
|
68
|
+
{ name: 'save', desc: 'Save current session' },
|
|
69
|
+
{ name: 'load', desc: 'Load a session' },
|
|
70
|
+
{ name: 'new', desc: 'Start fresh session' },
|
|
71
|
+
{ name: 'delete', desc: 'Delete a session' },
|
|
72
|
+
],
|
|
73
|
+
},
|
|
74
|
+
{ name: 'context', desc: 'Show project context' },
|
|
75
|
+
{
|
|
76
|
+
name: 'models',
|
|
77
|
+
desc: 'List available models',
|
|
78
|
+
subs: [
|
|
79
|
+
{ name: 'list', desc: 'List available models' },
|
|
80
|
+
{ name: 'refresh', desc: 'Refresh models from server' },
|
|
81
|
+
],
|
|
82
|
+
},
|
|
83
|
+
{ name: 'model', desc: 'Switch model', args: '<name>' },
|
|
84
|
+
{
|
|
85
|
+
name: 'settings',
|
|
86
|
+
desc: 'View/change settings',
|
|
87
|
+
subs: [
|
|
88
|
+
{ name: 'show', desc: 'Show all settings' },
|
|
89
|
+
{ name: 'model', desc: 'Change default model' },
|
|
90
|
+
{ name: 'reset', desc: 'Reset to defaults' },
|
|
91
|
+
],
|
|
92
|
+
},
|
|
93
|
+
{ name: 'theme', desc: 'Change visual theme', args: '[name]' },
|
|
94
|
+
{ name: 'rpm', desc: 'Set request limit', args: '[number]' },
|
|
95
|
+
{ name: 'tokens', desc: 'Token estimate' },
|
|
96
|
+
{ name: 'exit', desc: 'Exit Ikie' },
|
|
97
|
+
];
|
|
98
|
+
function slashCompleter(line) {
|
|
99
|
+
if (!line.startsWith('/'))
|
|
100
|
+
return [[], line];
|
|
101
|
+
const parts = line.slice(1).split(/\s+/);
|
|
102
|
+
const cmd = parts[0]?.toLowerCase() ?? '';
|
|
103
|
+
if (parts.length >= 2) {
|
|
104
|
+
const match = SLASH_CMDS.find(c => c.name === cmd);
|
|
105
|
+
if (match?.subs) {
|
|
106
|
+
const sub = parts[1].toLowerCase();
|
|
107
|
+
const hits = match.subs.filter(s => s.name.startsWith(sub)).map(s => `/${cmd} ${s.name}`);
|
|
108
|
+
return [hits, line];
|
|
109
|
+
}
|
|
110
|
+
return [[], line];
|
|
111
|
+
}
|
|
112
|
+
const hits = SLASH_CMDS.filter(c => c.name.startsWith(cmd)).map(c => '/' + c.name);
|
|
113
|
+
return [hits, line];
|
|
114
|
+
}
|
|
115
|
+
function printHelp() {
|
|
116
|
+
console.log(`
|
|
117
|
+
${c.primary.bold('Ikie Commands')}
|
|
118
|
+
|
|
119
|
+
${c.warning('/help')} Show this help
|
|
120
|
+
${c.warning('/clear')} Clear conversation history
|
|
121
|
+
${c.warning('/memory')} Show loaded memory
|
|
122
|
+
${c.warning('/memory save')} Save a note
|
|
123
|
+
${c.warning('/image <path>')} Attach image to next prompt
|
|
124
|
+
${c.warning('/image paste')} Attach clipboard image
|
|
125
|
+
${c.warning('/image list')} List queued images
|
|
126
|
+
${c.warning('/image clear')} Clear queued images
|
|
127
|
+
${c.warning('/session list')} List saved sessions
|
|
128
|
+
${c.warning('/session save')} Save current chat
|
|
129
|
+
${c.warning('/session load')} Load saved chat
|
|
130
|
+
${c.warning('/session new')} Start a fresh chat
|
|
131
|
+
${c.warning('/context')} Show current project context
|
|
132
|
+
${c.warning('/models')} List available models
|
|
133
|
+
${c.warning('/model <name>')} Switch AI model
|
|
134
|
+
${c.warning('/settings')} View all settings
|
|
135
|
+
${c.warning('/settings model')} Change default model (persisted)
|
|
136
|
+
${c.warning('/settings temperature')} Set temperature (0-2)
|
|
137
|
+
${c.warning('/settings max-tokens')} Set max tokens
|
|
138
|
+
${c.warning('/settings top-p')} Set top P (0-1)
|
|
139
|
+
${c.warning('/settings top-k')} Set top K
|
|
140
|
+
${c.warning('/settings auto-approve')} Toggle auto-approve
|
|
141
|
+
${c.warning('/settings rpm')} Set requests per minute
|
|
142
|
+
${c.warning('/settings theme')} Set visual theme
|
|
143
|
+
${c.warning('/settings reset')} Reset settings to defaults
|
|
144
|
+
${c.warning('/theme <name>')} Switch visual theme
|
|
145
|
+
${c.warning('/theme')} Pick a theme interactively
|
|
146
|
+
${c.warning('/rpm [number]')} Show or set request limit
|
|
147
|
+
${c.warning('/tokens')} Show conversation token estimate
|
|
148
|
+
${c.warning('/exit')} Exit Ikie
|
|
149
|
+
|
|
150
|
+
${c.primary.bold('Shortcuts')}
|
|
151
|
+
|
|
152
|
+
${c.accent('!')}${c.muted('<command>')} Run shell command directly
|
|
153
|
+
${c.accent('Ctrl+V')} Paste image from clipboard
|
|
154
|
+
${c.accent('Tab')} Complete commands
|
|
155
|
+
${c.accent('Esc')} Cancel current operation
|
|
156
|
+
${c.accent('Ctrl+C')} Cancel or quit on second press
|
|
157
|
+
${c.accent('Ctrl+D')} Exit
|
|
158
|
+
`);
|
|
159
|
+
}
|
|
160
|
+
async function handleSlashCommand(input, agent, config, projectContext, rl, sessionState, imageState) {
|
|
161
|
+
const [cmd, ...args] = input.slice(1).trim().split(/\s+/);
|
|
162
|
+
if (!cmd)
|
|
163
|
+
return true;
|
|
164
|
+
switch (cmd.toLowerCase()) {
|
|
165
|
+
case 'help':
|
|
166
|
+
case '?':
|
|
167
|
+
printHelp();
|
|
168
|
+
return true;
|
|
169
|
+
case 'clear':
|
|
170
|
+
agent.clearConversation();
|
|
171
|
+
console.log(infoLine('Conversation cleared.'));
|
|
172
|
+
return true;
|
|
173
|
+
case 'memory': {
|
|
174
|
+
const sub = args[0];
|
|
175
|
+
if (sub === 'save') {
|
|
176
|
+
process.stdout.write(c.primary(' Note: '));
|
|
177
|
+
const note = await promptLine('');
|
|
178
|
+
if (note.trim()) {
|
|
179
|
+
const scope = args[1] === 'global' ? 'global' : 'project';
|
|
180
|
+
if (scope === 'global') {
|
|
181
|
+
const { appendGlobalMemory } = await import('./memory.js');
|
|
182
|
+
appendGlobalMemory(note);
|
|
183
|
+
}
|
|
184
|
+
else {
|
|
185
|
+
const { appendProjectMemory } = await import('./memory.js');
|
|
186
|
+
appendProjectMemory(note);
|
|
187
|
+
}
|
|
188
|
+
console.log(successLine(`Saved to ${scope} memory.`));
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
else {
|
|
192
|
+
const mem = loadAllMemory();
|
|
193
|
+
if (mem.global || mem.project) {
|
|
194
|
+
if (mem.global) {
|
|
195
|
+
console.log(`\n${c.primary.bold('Global Memory')} ${c.muted('(~/.ikie/memory.md)')}`);
|
|
196
|
+
console.log(renderMarkdown(mem.global));
|
|
197
|
+
}
|
|
198
|
+
if (mem.project) {
|
|
199
|
+
console.log(`\n${c.primary.bold('Project Memory')} ${c.muted('(.ikie/memory.md)')}`);
|
|
200
|
+
console.log(renderMarkdown(mem.project));
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
else {
|
|
204
|
+
console.log(infoLine('No memory saved yet. Use /memory save to add a note.'));
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
return true;
|
|
208
|
+
}
|
|
209
|
+
case 'image': {
|
|
210
|
+
const subOrPath = args.join(' ').trim();
|
|
211
|
+
if (!subOrPath || subOrPath === 'list') {
|
|
212
|
+
if (!imageState.pending.length) {
|
|
213
|
+
console.log(infoLine('No images queued. Use /image <path>.'));
|
|
214
|
+
return true;
|
|
215
|
+
}
|
|
216
|
+
console.log(`\n${c.primary.bold('Queued Images')}`);
|
|
217
|
+
for (const image of imageState.pending) {
|
|
218
|
+
console.log(` ${c.secondary(`[Image #${image.id}]`)} ${c.white(image.name)} ${c.muted(formatBytes(image.bytes))}`);
|
|
219
|
+
console.log(` ${c.dim(image.path)}`);
|
|
220
|
+
}
|
|
221
|
+
return true;
|
|
222
|
+
}
|
|
223
|
+
if (subOrPath === 'clear') {
|
|
224
|
+
const count = imageState.pending.length;
|
|
225
|
+
imageState.pending = [];
|
|
226
|
+
console.log(successLine(`Cleared ${count} queued image${count === 1 ? '' : 's'}.`));
|
|
227
|
+
return true;
|
|
228
|
+
}
|
|
229
|
+
if (subOrPath === 'paste' || subOrPath === 'clipboard') {
|
|
230
|
+
try {
|
|
231
|
+
const image = loadClipboardImageAttachment(imageState.nextId++);
|
|
232
|
+
imageState.pending.push(image);
|
|
233
|
+
console.log(successLine(`Queued [Image #${image.id}] clipboard image (${formatBytes(image.bytes)}). Add text, then press Enter to send.`));
|
|
234
|
+
}
|
|
235
|
+
catch (err) {
|
|
236
|
+
console.log(errorLine(err instanceof Error ? err.message : String(err)));
|
|
237
|
+
}
|
|
238
|
+
return true;
|
|
239
|
+
}
|
|
240
|
+
try {
|
|
241
|
+
const image = loadImageAttachment(subOrPath, imageState.nextId++);
|
|
242
|
+
imageState.pending.push(image);
|
|
243
|
+
console.log(successLine(`Queued [Image #${image.id}] ${image.name} (${formatBytes(image.bytes)}). Add text, then press Enter to send.`));
|
|
244
|
+
}
|
|
245
|
+
catch (err) {
|
|
246
|
+
console.log(errorLine(err instanceof Error ? err.message : String(err)));
|
|
247
|
+
}
|
|
248
|
+
return true;
|
|
249
|
+
}
|
|
250
|
+
case 'session': {
|
|
251
|
+
const sub = (args[0] ?? 'list').toLowerCase();
|
|
252
|
+
const nameArg = args.slice(1).join(' ').trim();
|
|
253
|
+
if (sub === 'list' || sub === 'ls') {
|
|
254
|
+
const sessions = listSessions();
|
|
255
|
+
if (!sessions.length) {
|
|
256
|
+
console.log(infoLine('No saved sessions yet. Use /session save <name>.'));
|
|
257
|
+
return true;
|
|
258
|
+
}
|
|
259
|
+
console.log(`\n${c.primary.bold('Sessions')}`);
|
|
260
|
+
for (const s of sessions.slice(0, 30)) {
|
|
261
|
+
const active = s.name === sessionState.activeName ? c.success('*') : ' ';
|
|
262
|
+
const when = new Date(s.updatedAt).toLocaleString();
|
|
263
|
+
const model = s.model ? ` ${c.muted(s.model.split('/').pop() ?? s.model)}` : '';
|
|
264
|
+
console.log(` ${active} ${c.secondary(s.name.padEnd(24))} ${c.muted(String(s.messageCount).padStart(3) + ' msgs')} ${c.dim(when)}${model}`);
|
|
265
|
+
}
|
|
266
|
+
return true;
|
|
267
|
+
}
|
|
268
|
+
if (sub === 'save') {
|
|
269
|
+
const name = normalizeSessionName(nameArg || sessionState.activeName);
|
|
270
|
+
const saved = saveSession(name, config.model, agent.getConversation());
|
|
271
|
+
sessionState.activeName = saved.name;
|
|
272
|
+
console.log(successLine(`Session saved as "${saved.name}".`));
|
|
273
|
+
return true;
|
|
274
|
+
}
|
|
275
|
+
if (sub === 'load') {
|
|
276
|
+
if (!nameArg) {
|
|
277
|
+
console.log(errorLine('Usage: /session load <name>'));
|
|
278
|
+
return true;
|
|
279
|
+
}
|
|
280
|
+
try {
|
|
281
|
+
const session = loadSession(nameArg);
|
|
282
|
+
agent.setConversation(session.messages);
|
|
283
|
+
sessionState.activeName = session.name;
|
|
284
|
+
if (session.model)
|
|
285
|
+
config.model = session.model;
|
|
286
|
+
console.log(successLine(`Loaded session "${session.name}" (${session.messages.length} messages).`));
|
|
287
|
+
}
|
|
288
|
+
catch (err) {
|
|
289
|
+
console.log(errorLine(err instanceof Error ? err.message : String(err)));
|
|
290
|
+
}
|
|
291
|
+
return true;
|
|
292
|
+
}
|
|
293
|
+
if (sub === 'new') {
|
|
294
|
+
agent.clearConversation();
|
|
295
|
+
sessionState.activeName = nameArg ? normalizeSessionName(nameArg) : undefined;
|
|
296
|
+
console.log(successLine(sessionState.activeName ? `Started new session "${sessionState.activeName}".` : 'Started a fresh session.'));
|
|
297
|
+
return true;
|
|
298
|
+
}
|
|
299
|
+
if (sub === 'delete' || sub === 'rm') {
|
|
300
|
+
if (!nameArg) {
|
|
301
|
+
console.log(errorLine('Usage: /session delete <name>'));
|
|
302
|
+
return true;
|
|
303
|
+
}
|
|
304
|
+
try {
|
|
305
|
+
const name = normalizeSessionName(nameArg);
|
|
306
|
+
deleteSession(name);
|
|
307
|
+
if (sessionState.activeName === name)
|
|
308
|
+
sessionState.activeName = undefined;
|
|
309
|
+
console.log(successLine(`Deleted session "${name}".`));
|
|
310
|
+
}
|
|
311
|
+
catch (err) {
|
|
312
|
+
console.log(errorLine(err instanceof Error ? err.message : String(err)));
|
|
313
|
+
}
|
|
314
|
+
return true;
|
|
315
|
+
}
|
|
316
|
+
console.log(errorLine('Usage: /session list|save [name]|load <name>|new [name]|delete <name>'));
|
|
317
|
+
return true;
|
|
318
|
+
}
|
|
319
|
+
case 'context':
|
|
320
|
+
console.log(`\n${c.primary.bold('Project Context')}\n`);
|
|
321
|
+
console.log(renderMarkdown(projectContext));
|
|
322
|
+
return true;
|
|
323
|
+
case 'models': {
|
|
324
|
+
const sub = args[0]?.toLowerCase();
|
|
325
|
+
if (sub === 'refresh' || !sub || sub === 'list') {
|
|
326
|
+
await fetchAndDisplayModels(config);
|
|
327
|
+
}
|
|
328
|
+
return true;
|
|
329
|
+
}
|
|
330
|
+
case 'model': {
|
|
331
|
+
const model = args[0];
|
|
332
|
+
if (!model) {
|
|
333
|
+
await selectModelInteractively(rl, config);
|
|
334
|
+
}
|
|
335
|
+
else {
|
|
336
|
+
config.model = model;
|
|
337
|
+
saveConfig({ model });
|
|
338
|
+
console.log(successLine(`Default model set to: ${model}`));
|
|
339
|
+
console.log(infoLine('Persisted to settings — will be used for all new sessions.'));
|
|
340
|
+
}
|
|
341
|
+
return true;
|
|
342
|
+
}
|
|
343
|
+
case 'settings': {
|
|
344
|
+
const sub = (args[0] ?? 'show').toLowerCase();
|
|
345
|
+
const value = args.slice(1).join(' ').trim();
|
|
346
|
+
if (sub === 'show' || sub === 'list') {
|
|
347
|
+
console.log(`\n${c.primary.bold('Current Settings')}\n`);
|
|
348
|
+
console.log(` ${c.secondary('Model:')} ${c.white(config.model)}`);
|
|
349
|
+
console.log(` ${c.secondary('Max Tokens:')} ${c.white(config.maxTokens.toLocaleString())}`);
|
|
350
|
+
console.log(` ${c.secondary('Temperature:')} ${c.white(config.temperature)}`);
|
|
351
|
+
console.log(` ${c.secondary('Top P:')} ${c.white(config.topP)}`);
|
|
352
|
+
console.log(` ${c.secondary('Top K:')} ${c.white(config.topK)}`);
|
|
353
|
+
console.log(` ${c.secondary('Auto Approve:')} ${c.white(config.autoApprove ? 'Yes' : 'No')}`);
|
|
354
|
+
console.log(` ${c.secondary('Requests/min:')} ${c.white(config.requestsPerMinute)}`);
|
|
355
|
+
console.log(` ${c.secondary('Theme:')} ${c.white(config.theme)}`);
|
|
356
|
+
console.log(` ${c.secondary('Base URL:')} ${c.white(config.baseURL ?? FIREWORKS_BASE_URL)}`);
|
|
357
|
+
console.log(`\n ${c.muted('Use')} ${c.warning('/settings <option> <value>')} ${c.muted('to change settings')}`);
|
|
358
|
+
console.log(` ${c.muted('Options:')} model, temperature, max-tokens, top-p, top-k, auto-approve, rpm, theme`);
|
|
359
|
+
return true;
|
|
360
|
+
}
|
|
361
|
+
if (sub === 'model') {
|
|
362
|
+
if (!value) {
|
|
363
|
+
console.log(errorLine('Usage: /settings model <model-name>'));
|
|
364
|
+
console.log(`\n${c.muted(' Examples:')}`);
|
|
365
|
+
console.log(` ${c.accent('accounts/fireworks/models/kimi-k2p7-code')}`);
|
|
366
|
+
console.log(` ${c.accent('accounts/fireworks/models/llama-v3p3-70b-instruct')}`);
|
|
367
|
+
console.log(`\n ${c.muted('Current:')} ${c.white(config.model)}`);
|
|
368
|
+
console.log(` ${c.muted('Default:')} ${c.dim(DEFAULT_MODEL)}`);
|
|
369
|
+
return true;
|
|
370
|
+
}
|
|
371
|
+
config.model = value;
|
|
372
|
+
saveConfig({ model: value });
|
|
373
|
+
console.log(successLine(`Default model changed to: ${value}`));
|
|
374
|
+
return true;
|
|
375
|
+
}
|
|
376
|
+
if (sub === 'temperature') {
|
|
377
|
+
const temp = parseFloat(value);
|
|
378
|
+
if (isNaN(temp) || temp < 0 || temp > 2) {
|
|
379
|
+
console.log(errorLine('Temperature must be a number between 0 and 2'));
|
|
380
|
+
return true;
|
|
381
|
+
}
|
|
382
|
+
config.temperature = temp;
|
|
383
|
+
saveConfig({ temperature: temp });
|
|
384
|
+
console.log(successLine(`Temperature set to ${temp}`));
|
|
385
|
+
return true;
|
|
386
|
+
}
|
|
387
|
+
if (sub === 'max-tokens') {
|
|
388
|
+
const tokens = parseInt(value, 10);
|
|
389
|
+
if (isNaN(tokens) || tokens < 1) {
|
|
390
|
+
console.log(errorLine('Max tokens must be a positive number'));
|
|
391
|
+
return true;
|
|
392
|
+
}
|
|
393
|
+
config.maxTokens = tokens;
|
|
394
|
+
saveConfig({ maxTokens: tokens });
|
|
395
|
+
console.log(successLine(`Max tokens set to ${tokens.toLocaleString()}`));
|
|
396
|
+
return true;
|
|
397
|
+
}
|
|
398
|
+
if (sub === 'top-p') {
|
|
399
|
+
const topP = parseFloat(value);
|
|
400
|
+
if (isNaN(topP) || topP < 0 || topP > 1) {
|
|
401
|
+
console.log(errorLine('Top P must be a number between 0 and 1'));
|
|
402
|
+
return true;
|
|
403
|
+
}
|
|
404
|
+
config.topP = topP;
|
|
405
|
+
saveConfig({ topP });
|
|
406
|
+
console.log(successLine(`Top P set to ${topP}`));
|
|
407
|
+
return true;
|
|
408
|
+
}
|
|
409
|
+
if (sub === 'top-k') {
|
|
410
|
+
const topK = parseInt(value, 10);
|
|
411
|
+
if (isNaN(topK) || topK < 1) {
|
|
412
|
+
console.log(errorLine('Top K must be a positive number'));
|
|
413
|
+
return true;
|
|
414
|
+
}
|
|
415
|
+
config.topK = topK;
|
|
416
|
+
saveConfig({ topK });
|
|
417
|
+
console.log(successLine(`Top K set to ${topK}`));
|
|
418
|
+
return true;
|
|
419
|
+
}
|
|
420
|
+
if (sub === 'auto-approve') {
|
|
421
|
+
const on = value === 'true' || value === 'on' || value === 'yes' || value === '1';
|
|
422
|
+
config.autoApprove = on;
|
|
423
|
+
saveConfig({ autoApprove: on });
|
|
424
|
+
console.log(successLine(`Auto approve ${on ? 'enabled' : 'disabled'}`));
|
|
425
|
+
return true;
|
|
426
|
+
}
|
|
427
|
+
if (sub === 'rpm') {
|
|
428
|
+
const rpm = parseInt(value, 10);
|
|
429
|
+
if (isNaN(rpm) || rpm < 1) {
|
|
430
|
+
console.log(errorLine('RPM must be a positive number'));
|
|
431
|
+
return true;
|
|
432
|
+
}
|
|
433
|
+
config.requestsPerMinute = rpm;
|
|
434
|
+
saveConfig({ requestsPerMinute: rpm });
|
|
435
|
+
console.log(successLine(`Request limit set to ${rpm} rpm`));
|
|
436
|
+
return true;
|
|
437
|
+
}
|
|
438
|
+
if (sub === 'theme') {
|
|
439
|
+
if (!value) {
|
|
440
|
+
await selectThemeInteractively(rl, config);
|
|
441
|
+
}
|
|
442
|
+
else if (setTheme(value)) {
|
|
443
|
+
config.theme = value;
|
|
444
|
+
saveConfig({ theme: value });
|
|
445
|
+
drawBanner(config.model);
|
|
446
|
+
console.log(successLine(`Theme switched to "${value}".`));
|
|
447
|
+
}
|
|
448
|
+
else {
|
|
449
|
+
console.log(errorLine(`Unknown theme "${value}". Available: ${Object.keys(THEMES).join(', ')}`));
|
|
450
|
+
}
|
|
451
|
+
return true;
|
|
452
|
+
}
|
|
453
|
+
if (sub === 'reset') {
|
|
454
|
+
config.model = DEFAULT_MODEL;
|
|
455
|
+
config.maxTokens = 32768;
|
|
456
|
+
config.temperature = 0.6;
|
|
457
|
+
config.topP = 0.95;
|
|
458
|
+
config.topK = 40;
|
|
459
|
+
config.autoApprove = false;
|
|
460
|
+
config.requestsPerMinute = 10;
|
|
461
|
+
config.theme = 'nebula';
|
|
462
|
+
saveConfig({
|
|
463
|
+
model: DEFAULT_MODEL,
|
|
464
|
+
maxTokens: 32768,
|
|
465
|
+
temperature: 0.6,
|
|
466
|
+
topP: 0.95,
|
|
467
|
+
topK: 40,
|
|
468
|
+
autoApprove: false,
|
|
469
|
+
requestsPerMinute: 10,
|
|
470
|
+
theme: 'nebula',
|
|
471
|
+
});
|
|
472
|
+
drawBanner(config.model);
|
|
473
|
+
console.log(successLine('Settings reset to defaults'));
|
|
474
|
+
return true;
|
|
475
|
+
}
|
|
476
|
+
console.log(errorLine('Usage: /settings [show|model|temperature|max-tokens|top-p|top-k|auto-approve|rpm|theme|reset]'));
|
|
477
|
+
return true;
|
|
478
|
+
}
|
|
479
|
+
case 'theme': {
|
|
480
|
+
const themeName = args[0]?.toLowerCase();
|
|
481
|
+
if (!themeName) {
|
|
482
|
+
await selectThemeInteractively(rl, config);
|
|
483
|
+
}
|
|
484
|
+
else if (setTheme(themeName)) {
|
|
485
|
+
config.theme = themeName;
|
|
486
|
+
drawBanner(config.model);
|
|
487
|
+
console.log(successLine(`Theme switched to "${themeName}".`));
|
|
488
|
+
}
|
|
489
|
+
else {
|
|
490
|
+
console.log(errorLine(`Unknown theme "${themeName}". Available themes: ${Object.keys(THEMES).join(', ')}`));
|
|
491
|
+
}
|
|
492
|
+
return true;
|
|
493
|
+
}
|
|
494
|
+
case 'rpm': {
|
|
495
|
+
const value = args[0];
|
|
496
|
+
if (!value) {
|
|
497
|
+
console.log(infoLine(`Request limit: ${config.requestsPerMinute} rpm`));
|
|
498
|
+
return true;
|
|
499
|
+
}
|
|
500
|
+
const rpm = Number(value);
|
|
501
|
+
if (!Number.isFinite(rpm) || rpm < 1) {
|
|
502
|
+
console.log(errorLine('Usage: /rpm <number greater than 0>'));
|
|
503
|
+
return true;
|
|
504
|
+
}
|
|
505
|
+
config.requestsPerMinute = Math.floor(rpm);
|
|
506
|
+
console.log(successLine(`Request limit set to ${config.requestsPerMinute} rpm.`));
|
|
507
|
+
return true;
|
|
508
|
+
}
|
|
509
|
+
case 'tokens': {
|
|
510
|
+
const msgs = agent.getConversation();
|
|
511
|
+
const approxTokens = JSON.stringify(msgs).length / 4;
|
|
512
|
+
console.log(infoLine(`~${Math.round(approxTokens).toLocaleString()} tokens in conversation`));
|
|
513
|
+
return true;
|
|
514
|
+
}
|
|
515
|
+
case 'exit':
|
|
516
|
+
case 'quit':
|
|
517
|
+
case 'q':
|
|
518
|
+
printGoodbye(sessionState, agent, config);
|
|
519
|
+
process.exit(0);
|
|
520
|
+
default:
|
|
521
|
+
console.log(errorLine(`Unknown command: /${cmd}. Type /help for help.`));
|
|
522
|
+
return true;
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
function promptLine(prompt) {
|
|
526
|
+
return new Promise((resolve) => {
|
|
527
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
528
|
+
rl.question(prompt, (answer) => {
|
|
529
|
+
rl.close();
|
|
530
|
+
resolve(answer);
|
|
531
|
+
});
|
|
532
|
+
});
|
|
533
|
+
}
|
|
534
|
+
function printGoodbye(sessionState, agent, config) {
|
|
535
|
+
const msgs = agent.getConversation();
|
|
536
|
+
const hasConversation = msgs.length > 0;
|
|
537
|
+
// Auto-save if there's an active session and we have config
|
|
538
|
+
if (sessionState.activeName && hasConversation && config) {
|
|
539
|
+
try {
|
|
540
|
+
saveSession(sessionState.activeName, config.model, msgs);
|
|
541
|
+
}
|
|
542
|
+
catch (err) {
|
|
543
|
+
// Silently fail if save doesn't work
|
|
544
|
+
}
|
|
545
|
+
}
|
|
546
|
+
console.log(`\n${c.primary.bold('Goodbye!')}\n`);
|
|
547
|
+
if (sessionState.activeName) {
|
|
548
|
+
console.log(` ${c.muted('Your session')} ${c.secondary.bold(sessionState.activeName)} ${c.muted('has been saved.')}`);
|
|
549
|
+
console.log(`\n ${c.muted('To continue:')}`);
|
|
550
|
+
console.log(` ${c.accent('ikie')} ${c.warning(`/session load ${sessionState.activeName}`)}\n`);
|
|
551
|
+
}
|
|
552
|
+
else if (hasConversation) {
|
|
553
|
+
console.log(` ${c.warning('\u26A0')} ${c.muted('Your current conversation was not saved.')}`);
|
|
554
|
+
console.log(`\n ${c.muted('To save next time:')}`);
|
|
555
|
+
console.log(` ${c.warning('/session save')} ${c.muted('<name>')}\n`);
|
|
556
|
+
}
|
|
557
|
+
console.log(` ${c.muted('Happy coding!')}\n`);
|
|
558
|
+
}
|
|
559
|
+
function formatElapsed(ms) {
|
|
560
|
+
if (ms < 1000)
|
|
561
|
+
return `${ms}ms`;
|
|
562
|
+
const seconds = ms / 1000;
|
|
563
|
+
if (seconds < 60)
|
|
564
|
+
return `${seconds.toFixed(1)}s`;
|
|
565
|
+
const minutes = Math.floor(seconds / 60);
|
|
566
|
+
const rest = Math.floor(seconds % 60).toString().padStart(2, '0');
|
|
567
|
+
return `${minutes}:${rest}`;
|
|
568
|
+
}
|
|
569
|
+
function printRightStatus(message) {
|
|
570
|
+
const cols = process.stdout.columns ?? 80;
|
|
571
|
+
const width = stripAnsi(message).length;
|
|
572
|
+
process.stdout.write(`${' '.repeat(Math.max(0, cols - width - 1))}${message}\n`);
|
|
573
|
+
}
|
|
574
|
+
function formatTaskTimeline(agent, elapsed, state) {
|
|
575
|
+
const stats = agent.getLastTurnStats();
|
|
576
|
+
const modelLabel = `${stats.modelCalls} model`;
|
|
577
|
+
const toolLabel = `${stats.toolCalls} tools`;
|
|
578
|
+
const filesLabel = stats.filesChanged > 0 ? ` · ${stats.filesChanged} files` : '';
|
|
579
|
+
const tail = state === 'cancelled' ? `cancelled after ${elapsed}` : `${state} in ${elapsed}`;
|
|
580
|
+
return `${modelLabel} · ${toolLabel}${filesLabel} · ${tail}`;
|
|
581
|
+
}
|
|
582
|
+
function selectModelInteractively(rl, config) {
|
|
583
|
+
return new Promise((resolve) => {
|
|
584
|
+
const models = [];
|
|
585
|
+
const prompt = () => {
|
|
586
|
+
rl.question(c.muted('Enter number (or 0 to cancel): '), (answer) => {
|
|
587
|
+
const num = parseInt(answer, 10);
|
|
588
|
+
if (isNaN(num) || num < 0 || num > models.length) {
|
|
589
|
+
prompt();
|
|
590
|
+
return;
|
|
591
|
+
}
|
|
592
|
+
if (num === 0) {
|
|
593
|
+
console.log(infoLine('Model selection cancelled.'));
|
|
594
|
+
resolve();
|
|
595
|
+
return;
|
|
596
|
+
}
|
|
597
|
+
const chosen = models[num - 1];
|
|
598
|
+
config.model = chosen.provider_model_id;
|
|
599
|
+
console.log(successLine(`Switched to ${chosen.name} — ${chosen.display_name}`));
|
|
600
|
+
resolve();
|
|
601
|
+
});
|
|
602
|
+
};
|
|
603
|
+
console.log(`\n ${c.accent.bold('Select a Model')}`);
|
|
604
|
+
fetchModelsFromServer(config).then((fetched) => {
|
|
605
|
+
models.push(...fetched);
|
|
606
|
+
if (models.length === 0) {
|
|
607
|
+
console.log(` ${c.error('No models available.')}`);
|
|
608
|
+
resolve();
|
|
609
|
+
return;
|
|
610
|
+
}
|
|
611
|
+
models.forEach((m, i) => {
|
|
612
|
+
const num = c.primary((i + 1).toString().padStart(2));
|
|
613
|
+
const def = m.is_default ? ` ${c.success('[DEFAULT]')}` : '';
|
|
614
|
+
console.log(` ${num}) ${c.bold(m.name.padEnd(24))} ${c.dim(m.display_name)} ${c.muted(`(${(m.context_window / 1024).toFixed(0)}K)`)}${def}`);
|
|
615
|
+
});
|
|
616
|
+
console.log(` ${c.muted(' 0)')} Cancel`);
|
|
617
|
+
prompt();
|
|
618
|
+
}).catch(() => {
|
|
619
|
+
console.log(` ${c.error('Failed to load models.')}`);
|
|
620
|
+
resolve();
|
|
621
|
+
});
|
|
622
|
+
});
|
|
623
|
+
}
|
|
624
|
+
function selectThemeInteractively(rl, config) {
|
|
625
|
+
return new Promise((resolve) => {
|
|
626
|
+
const themesList = Object.keys(THEMES);
|
|
627
|
+
const currentIndex = themesList.indexOf(config.theme);
|
|
628
|
+
let lastIndex = currentIndex === -1 ? 0 : currentIndex;
|
|
629
|
+
const prompt = () => {
|
|
630
|
+
rl.question(c.muted('Enter number (or 0 to cancel): '), (answer) => {
|
|
631
|
+
const num = parseInt(answer, 10);
|
|
632
|
+
if (isNaN(num) || num < 0 || num > themesList.length) {
|
|
633
|
+
prompt();
|
|
634
|
+
return;
|
|
635
|
+
}
|
|
636
|
+
if (num === 0) {
|
|
637
|
+
console.log(infoLine('Theme selection cancelled.'));
|
|
638
|
+
resolve();
|
|
639
|
+
return;
|
|
640
|
+
}
|
|
641
|
+
const chosen = themesList[num - 1];
|
|
642
|
+
setTheme(chosen);
|
|
643
|
+
config.theme = chosen;
|
|
644
|
+
drawBanner(config.model);
|
|
645
|
+
console.log(successLine(`Theme switched to "${chosen}".`));
|
|
646
|
+
resolve();
|
|
647
|
+
});
|
|
648
|
+
};
|
|
649
|
+
console.log(`\n ${c.accent.bold('Select a Theme')}`);
|
|
650
|
+
themesList.forEach((name, i) => {
|
|
651
|
+
const theme = THEMES[name];
|
|
652
|
+
const num = c.primary((i + 1).toString().padStart(2));
|
|
653
|
+
const active = i === currentIndex ? ` ${c.success('[ACTIVE]')}` : '';
|
|
654
|
+
console.log(` ${num}) ${c.bold(name.padEnd(12))} ${c.muted('-')} ${c.dim(theme.description)}${active}`);
|
|
655
|
+
});
|
|
656
|
+
console.log(` ${c.muted(' 0)')} Cancel`);
|
|
657
|
+
prompt();
|
|
658
|
+
});
|
|
659
|
+
}
|
|
660
|
+
export async function startREPL(agent, config, projectContext, oneShot) {
|
|
661
|
+
void HISTORY_FILE;
|
|
662
|
+
if (oneShot) {
|
|
663
|
+
try {
|
|
664
|
+
await agent.send(oneShot, { autoApprove: config.autoApprove });
|
|
665
|
+
}
|
|
666
|
+
catch (err) {
|
|
667
|
+
console.error(errorLine(`Agent error: ${err}`));
|
|
668
|
+
process.exit(1);
|
|
669
|
+
}
|
|
670
|
+
return;
|
|
671
|
+
}
|
|
672
|
+
drawBanner(config.model);
|
|
673
|
+
const rl = readline.createInterface({
|
|
674
|
+
input: process.stdin,
|
|
675
|
+
output: process.stdout,
|
|
676
|
+
terminal: true,
|
|
677
|
+
historySize: MAX_HISTORY,
|
|
678
|
+
prompt: PROMPT,
|
|
679
|
+
completer: slashCompleter,
|
|
680
|
+
});
|
|
681
|
+
let multilineBuffer = '';
|
|
682
|
+
let busy = false;
|
|
683
|
+
let ctrlCCount = 0;
|
|
684
|
+
const sessionState = {};
|
|
685
|
+
const imageState = { nextId: 1, pending: [] };
|
|
686
|
+
let pasteSeq = 0;
|
|
687
|
+
let pasteMode = false;
|
|
688
|
+
let pasteBuffer = '';
|
|
689
|
+
const pastedBlocks = new Map();
|
|
690
|
+
const readlineDataListeners = process.stdin.rawListeners('data').slice();
|
|
691
|
+
for (const fn of readlineDataListeners) {
|
|
692
|
+
process.stdin.removeListener('data', fn);
|
|
693
|
+
}
|
|
694
|
+
const forwardToReadline = (data) => {
|
|
695
|
+
for (const fn of readlineDataListeners) {
|
|
696
|
+
fn.call(process.stdin, data);
|
|
697
|
+
}
|
|
698
|
+
};
|
|
699
|
+
const pasteSummary = (content) => {
|
|
700
|
+
const normalized = content.replace(/\r\n/g, '\n').replace(/\r/g, '\n');
|
|
701
|
+
const lines = normalized ? normalized.split('\n').length : 0;
|
|
702
|
+
if (lines > 1)
|
|
703
|
+
return `~${lines} lines`;
|
|
704
|
+
return `${normalized.length} chars`;
|
|
705
|
+
};
|
|
706
|
+
const insertPasteBlock = (content) => {
|
|
707
|
+
const normalized = content.replace(/\r\n/g, '\n').replace(/\r/g, '\n');
|
|
708
|
+
if (!normalized)
|
|
709
|
+
return;
|
|
710
|
+
const token = `[Pasted #${++pasteSeq}: ${pasteSummary(normalized)}]`;
|
|
711
|
+
pastedBlocks.set(token, normalized);
|
|
712
|
+
rl.write(token);
|
|
713
|
+
};
|
|
714
|
+
const routeInputData = (data) => {
|
|
715
|
+
const text = data.toString('utf8');
|
|
716
|
+
// Check for Ctrl+V (0x16) - try to paste image from clipboard
|
|
717
|
+
if (data.length === 1 && data[0] === 0x16) {
|
|
718
|
+
// Check if clipboard has an image
|
|
719
|
+
if (hasClipboardImage()) {
|
|
720
|
+
try {
|
|
721
|
+
const image = loadClipboardImageAttachment(imageState.nextId++);
|
|
722
|
+
imageState.pending.push(image);
|
|
723
|
+
// Show feedback
|
|
724
|
+
process.stdout.write(`\r\x1b[2K${rl.getPrompt()}`);
|
|
725
|
+
const line = rl.line || '';
|
|
726
|
+
process.stdout.write(line);
|
|
727
|
+
process.stdout.write(`\n ${c.success('✓')} ${c.muted('Image pasted from clipboard')} ${c.secondary(`[Image #${image.id}]`)} ${c.dim(image.name)} ${c.muted(formatBytes(image.bytes))}\n`);
|
|
728
|
+
process.stdout.write(rl.getPrompt() + line);
|
|
729
|
+
return;
|
|
730
|
+
}
|
|
731
|
+
catch (err) {
|
|
732
|
+
// Silently fall through to normal paste if clipboard image load fails
|
|
733
|
+
// This allows Ctrl+V to still work for text paste
|
|
734
|
+
}
|
|
735
|
+
}
|
|
736
|
+
// If no image in clipboard, forward Ctrl+V to readline for text paste
|
|
737
|
+
forwardToReadline(data);
|
|
738
|
+
return;
|
|
739
|
+
}
|
|
740
|
+
if (!pasteMode && !text.includes('\x1b[200~') && /[\r\n]/.test(text.trim()) && text.length > 3) {
|
|
741
|
+
insertPasteBlock(text);
|
|
742
|
+
return;
|
|
743
|
+
}
|
|
744
|
+
let rest = text;
|
|
745
|
+
while (rest) {
|
|
746
|
+
if (pasteMode) {
|
|
747
|
+
const end = rest.indexOf('\x1b[201~');
|
|
748
|
+
if (end === -1) {
|
|
749
|
+
pasteBuffer += rest;
|
|
750
|
+
return;
|
|
751
|
+
}
|
|
752
|
+
pasteBuffer += rest.slice(0, end);
|
|
753
|
+
insertPasteBlock(pasteBuffer);
|
|
754
|
+
pasteBuffer = '';
|
|
755
|
+
pasteMode = false;
|
|
756
|
+
rest = rest.slice(end + '\x1b[201~'.length);
|
|
757
|
+
continue;
|
|
758
|
+
}
|
|
759
|
+
const start = rest.indexOf('\x1b[200~');
|
|
760
|
+
if (start === -1) {
|
|
761
|
+
forwardToReadline(Buffer.from(rest, 'utf8'));
|
|
762
|
+
return;
|
|
763
|
+
}
|
|
764
|
+
if (start > 0) {
|
|
765
|
+
forwardToReadline(Buffer.from(rest.slice(0, start), 'utf8'));
|
|
766
|
+
}
|
|
767
|
+
pasteMode = true;
|
|
768
|
+
rest = rest.slice(start + '\x1b[200~'.length);
|
|
769
|
+
}
|
|
770
|
+
};
|
|
771
|
+
process.stdin.on('data', routeInputData);
|
|
772
|
+
const expandPastedBlocks = (input) => {
|
|
773
|
+
let expanded = input;
|
|
774
|
+
const used = [];
|
|
775
|
+
for (const [token, content] of pastedBlocks) {
|
|
776
|
+
if (!expanded.includes(token))
|
|
777
|
+
continue;
|
|
778
|
+
used.push(token);
|
|
779
|
+
expanded = expanded.split(token).join(`[pasted content ${token}]`);
|
|
780
|
+
}
|
|
781
|
+
if (!used.length)
|
|
782
|
+
return input;
|
|
783
|
+
const attachments = used.map((token, i) => {
|
|
784
|
+
const content = pastedBlocks.get(token) ?? '';
|
|
785
|
+
return `Pasted content ${i + 1} (${token}):\n${content}`;
|
|
786
|
+
});
|
|
787
|
+
for (const token of used)
|
|
788
|
+
pastedBlocks.delete(token);
|
|
789
|
+
return `${expanded}\n\n${attachments.join('\n\n')}`;
|
|
790
|
+
};
|
|
791
|
+
const showPrompt = () => {
|
|
792
|
+
if (multilineBuffer) {
|
|
793
|
+
rl.setPrompt(CONTINUE_PROMPT);
|
|
794
|
+
}
|
|
795
|
+
else {
|
|
796
|
+
printPromptHeader();
|
|
797
|
+
if (imageState.pending.length) {
|
|
798
|
+
const labels = imageState.pending.map(image => `[Image #${image.id}]`).join(' ');
|
|
799
|
+
process.stdout.write(`${c.muted(' attached')} ${c.secondary(labels)}\n`);
|
|
800
|
+
}
|
|
801
|
+
rl.setPrompt(PROMPT);
|
|
802
|
+
}
|
|
803
|
+
rl.prompt();
|
|
804
|
+
};
|
|
805
|
+
rl.on('close', () => {
|
|
806
|
+
printGoodbye(sessionState, agent, config);
|
|
807
|
+
process.exit(0);
|
|
808
|
+
});
|
|
809
|
+
rl.on('SIGINT', () => {
|
|
810
|
+
if (busy)
|
|
811
|
+
return;
|
|
812
|
+
ctrlCCount++;
|
|
813
|
+
if (ctrlCCount >= 2) {
|
|
814
|
+
rl.close();
|
|
815
|
+
return;
|
|
816
|
+
}
|
|
817
|
+
multilineBuffer = '';
|
|
818
|
+
process.stdout.write(`\n${c.muted(' (press Ctrl+C again or type /exit to quit)')}\n`);
|
|
819
|
+
setTimeout(() => { ctrlCCount = 0; }, 2000);
|
|
820
|
+
showPrompt();
|
|
821
|
+
});
|
|
822
|
+
const handleLine = async (raw) => {
|
|
823
|
+
const line = raw.trim();
|
|
824
|
+
if (!line) {
|
|
825
|
+
showPrompt();
|
|
826
|
+
return;
|
|
827
|
+
}
|
|
828
|
+
if (line.endsWith('\\')) {
|
|
829
|
+
multilineBuffer += (multilineBuffer ? '\n' : '') + line.slice(0, -1);
|
|
830
|
+
showPrompt();
|
|
831
|
+
return;
|
|
832
|
+
}
|
|
833
|
+
const fullInput = multilineBuffer ? multilineBuffer + '\n' + line : line;
|
|
834
|
+
multilineBuffer = '';
|
|
835
|
+
if (fullInput.startsWith('!')) {
|
|
836
|
+
const cmd = fullInput.slice(1).trim();
|
|
837
|
+
if (cmd) {
|
|
838
|
+
try {
|
|
839
|
+
process.stdout.write(execSync(cmd, { encoding: 'utf-8' }));
|
|
840
|
+
}
|
|
841
|
+
catch (err) {
|
|
842
|
+
const e = err;
|
|
843
|
+
if (e.stdout)
|
|
844
|
+
process.stdout.write(e.stdout);
|
|
845
|
+
if (e.stderr)
|
|
846
|
+
process.stdout.write(e.stderr);
|
|
847
|
+
else
|
|
848
|
+
console.error(errorLine(e.message ?? 'Command failed'));
|
|
849
|
+
}
|
|
850
|
+
process.stdout.write('\n');
|
|
851
|
+
}
|
|
852
|
+
showPrompt();
|
|
853
|
+
return;
|
|
854
|
+
}
|
|
855
|
+
if (fullInput.startsWith('/')) {
|
|
856
|
+
await handleSlashCommand(fullInput, agent, config, projectContext, rl, sessionState, imageState);
|
|
857
|
+
process.stdout.write('\n');
|
|
858
|
+
showPrompt();
|
|
859
|
+
return;
|
|
860
|
+
}
|
|
861
|
+
const agentInput = expandPastedBlocks(fullInput);
|
|
862
|
+
const imagesForTurn = [...imageState.pending];
|
|
863
|
+
const userContent = buildUserContent(agentInput, imagesForTurn);
|
|
864
|
+
imageState.pending = [];
|
|
865
|
+
let restoredImages = false;
|
|
866
|
+
process.stdout.write('\r\x1b[2K');
|
|
867
|
+
process.stdout.write(`${c.muted(' Working. Press Esc to interrupt.')}\n`);
|
|
868
|
+
busy = true;
|
|
869
|
+
rl.pause();
|
|
870
|
+
const taskStartedAt = Date.now();
|
|
871
|
+
let taskFailed = false;
|
|
872
|
+
const abortController = new AbortController();
|
|
873
|
+
const savedDataListeners = process.stdin.rawListeners('data').slice();
|
|
874
|
+
const savedKeypressListeners = process.stdin.rawListeners('keypress').slice();
|
|
875
|
+
process.stdin.removeAllListeners('data');
|
|
876
|
+
process.stdin.removeAllListeners('keypress');
|
|
877
|
+
if (process.stdin.isTTY)
|
|
878
|
+
process.stdin.setRawMode(true);
|
|
879
|
+
process.stdin.resume();
|
|
880
|
+
const cancelHandler = (data) => {
|
|
881
|
+
const b = data[0];
|
|
882
|
+
if (b === 0x1b && data.length === 1) {
|
|
883
|
+
if (!abortController.signal.aborted) {
|
|
884
|
+
abortController.abort();
|
|
885
|
+
process.stdout.write(`\n${c.warning(' Cancelled')}\n`);
|
|
886
|
+
}
|
|
887
|
+
}
|
|
888
|
+
else if (b === 0x03) {
|
|
889
|
+
ctrlCCount++;
|
|
890
|
+
if (ctrlCCount >= 2)
|
|
891
|
+
process.exit(0);
|
|
892
|
+
if (!abortController.signal.aborted) {
|
|
893
|
+
abortController.abort();
|
|
894
|
+
process.stdout.write(`\n${c.warning(' Cancelled')}\n`);
|
|
895
|
+
}
|
|
896
|
+
setTimeout(() => { ctrlCCount = 0; }, 2000);
|
|
897
|
+
}
|
|
898
|
+
};
|
|
899
|
+
process.stdin.on('data', cancelHandler);
|
|
900
|
+
try {
|
|
901
|
+
await agent.send(userContent, {
|
|
902
|
+
autoApprove: config.autoApprove,
|
|
903
|
+
signal: abortController.signal,
|
|
904
|
+
startedAt: taskStartedAt,
|
|
905
|
+
});
|
|
906
|
+
if (sessionState.activeName && !abortController.signal.aborted) {
|
|
907
|
+
saveSession(sessionState.activeName, config.model, agent.getConversation());
|
|
908
|
+
}
|
|
909
|
+
}
|
|
910
|
+
catch (err) {
|
|
911
|
+
if (!abortController.signal.aborted) {
|
|
912
|
+
taskFailed = true;
|
|
913
|
+
if (imagesForTurn.length) {
|
|
914
|
+
imageState.pending = [...imagesForTurn, ...imageState.pending];
|
|
915
|
+
restoredImages = true;
|
|
916
|
+
}
|
|
917
|
+
const e = err;
|
|
918
|
+
console.error(`\n${errorLine(`Error: ${e.message ?? String(err)}`)}`);
|
|
919
|
+
}
|
|
920
|
+
}
|
|
921
|
+
finally {
|
|
922
|
+
if (abortController.signal.aborted && imagesForTurn.length && !restoredImages) {
|
|
923
|
+
imageState.pending = [...imagesForTurn, ...imageState.pending];
|
|
924
|
+
}
|
|
925
|
+
const elapsed = formatElapsed(Date.now() - taskStartedAt);
|
|
926
|
+
process.stdin.removeListener('data', cancelHandler);
|
|
927
|
+
if (process.stdin.isTTY)
|
|
928
|
+
process.stdin.setRawMode(false);
|
|
929
|
+
process.stdin.pause();
|
|
930
|
+
restoreStdinListeners(savedDataListeners, savedKeypressListeners);
|
|
931
|
+
busy = false;
|
|
932
|
+
ctrlCCount = 0;
|
|
933
|
+
rl.resume();
|
|
934
|
+
const status = abortController.signal.aborted
|
|
935
|
+
? c.warning(formatTaskTimeline(agent, elapsed, 'cancelled'))
|
|
936
|
+
: taskFailed
|
|
937
|
+
? c.error(formatTaskTimeline(agent, elapsed, 'failed'))
|
|
938
|
+
: c.muted(formatTaskTimeline(agent, elapsed, 'done'));
|
|
939
|
+
printRightStatus(status);
|
|
940
|
+
showPrompt();
|
|
941
|
+
}
|
|
942
|
+
};
|
|
943
|
+
let chain = Promise.resolve();
|
|
944
|
+
rl.on('line', (raw) => {
|
|
945
|
+
chain = chain.then(() => handleLine(raw));
|
|
946
|
+
});
|
|
947
|
+
showPrompt();
|
|
948
|
+
}
|