cloving 0.2.1 ā 0.3.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 +1 -1
- package/dist/cloving_gpt/adapters/claude.js +7 -4
- package/dist/cloving_gpt/adapters/gemini.js +2 -1
- package/dist/cloving_gpt/adapters/mistral.js +1 -1
- package/dist/cloving_gpt/adapters/openai.js +1 -1
- package/dist/cloving_gpt/index.js +16 -7
- package/dist/commands/generate/code.js +3 -13
- package/dist/commands/generate/commit.js +3 -43
- package/dist/commands/generate/init.js +15 -0
- package/dist/commands/generate/review.js +1 -5
- package/dist/commands/generate/shell.js +3 -103
- package/dist/commands/generate/unit_tests.js +5 -51
- package/dist/commands/init.js +62 -53
- package/dist/file2.js +4 -0
- package/dist/managers/ChatManager.js +438 -128
- package/dist/managers/CodeManager.js +66 -185
- package/dist/managers/CommitManager.js +55 -0
- package/dist/managers/ProxyManager.js +2 -2
- package/dist/managers/ReviewManager.js +28 -23
- package/dist/managers/ShellManager.js +106 -0
- package/dist/newfile.js +2 -0
- package/dist/package.json +11 -10
- package/dist/utils/command_utils.js +195 -20
- package/dist/utils/string_utils.js +222 -60
- package/package.json +11 -10
|
@@ -4,18 +4,34 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
4
4
|
};
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
6
|
const copy_paste_1 = __importDefault(require("copy-paste"));
|
|
7
|
-
const path_1 = __importDefault(require("path"));
|
|
8
|
-
const fs_1 = __importDefault(require("fs"));
|
|
9
7
|
const readline_1 = __importDefault(require("readline"));
|
|
10
8
|
const child_process_1 = require("child_process");
|
|
9
|
+
const colors_1 = __importDefault(require("colors"));
|
|
10
|
+
const CommitManager_1 = __importDefault(require("./CommitManager"));
|
|
11
11
|
const ChunkManager_1 = __importDefault(require("./ChunkManager"));
|
|
12
12
|
const ReviewManager_1 = __importDefault(require("./ReviewManager"));
|
|
13
13
|
const cloving_gpt_1 = __importDefault(require("../cloving_gpt"));
|
|
14
|
-
const git_utils_1 = require("../utils/git_utils");
|
|
15
14
|
const string_utils_1 = require("../utils/string_utils");
|
|
15
|
+
const config_utils_1 = require("../utils/config_utils");
|
|
16
16
|
const command_utils_1 = require("../utils/command_utils");
|
|
17
|
-
const
|
|
17
|
+
const specialCommands = [
|
|
18
|
+
'save',
|
|
19
|
+
'commit',
|
|
20
|
+
'copy',
|
|
21
|
+
'review',
|
|
22
|
+
'add <file-path>',
|
|
23
|
+
'find <file-name>',
|
|
24
|
+
'rm <pattern>',
|
|
25
|
+
'ls <pattern>',
|
|
26
|
+
'git <command>',
|
|
27
|
+
'help',
|
|
28
|
+
'exit'
|
|
29
|
+
];
|
|
18
30
|
class ChatManager {
|
|
31
|
+
/**
|
|
32
|
+
* Creates an instance of ChatManager.
|
|
33
|
+
* @param {ClovingGPTOptions} options - Configuration options for the ChatManager.
|
|
34
|
+
*/
|
|
19
35
|
constructor(options) {
|
|
20
36
|
this.options = options;
|
|
21
37
|
this.chatHistory = [];
|
|
@@ -32,78 +48,134 @@ class ChatManager {
|
|
|
32
48
|
this.rl = readline_1.default.createInterface({
|
|
33
49
|
input: process.stdin,
|
|
34
50
|
output: process.stdout,
|
|
35
|
-
prompt: 'cloving> ',
|
|
51
|
+
prompt: colors_1.default.green.bold('cloving> '),
|
|
36
52
|
historySize: 1000,
|
|
37
53
|
});
|
|
38
54
|
this.chunkManager = new ChunkManager_1.default();
|
|
39
55
|
}
|
|
56
|
+
/**
|
|
57
|
+
* Initializes the ChatManager by loading context files, displaying a welcome message,
|
|
58
|
+
* and setting up event listeners.
|
|
59
|
+
* @returns {Promise<void>}
|
|
60
|
+
*/
|
|
40
61
|
async initialize() {
|
|
41
|
-
|
|
62
|
+
await this.loadContextFiles();
|
|
63
|
+
console.log(`\nš š š ${colors_1.default.bold('Welcome to Cloving REPL')} š š š\n`);
|
|
64
|
+
this.displayAvailableCommands();
|
|
65
|
+
console.log('\nWhat would you like to do?');
|
|
42
66
|
this.rl.prompt();
|
|
43
67
|
this.setupEventListeners();
|
|
44
|
-
await this.loadContextFiles();
|
|
45
68
|
}
|
|
69
|
+
displayAvailableCommands() {
|
|
70
|
+
console.log(`Type a freeform request or question to interact with your Cloving AI pair programmer.\n`);
|
|
71
|
+
console.log('Available special commands:');
|
|
72
|
+
console.log(` - ${colors_1.default.yellow(colors_1.default.bold('save'))} Save all the changes from the last response to files`);
|
|
73
|
+
console.log(` - ${colors_1.default.yellow(colors_1.default.bold('commit'))} Commit the changes to git with an AI-generated message that you can edit`);
|
|
74
|
+
console.log(` - ${colors_1.default.yellow(colors_1.default.bold('copy'))} Copy the last response to clipboard`);
|
|
75
|
+
console.log(` - ${colors_1.default.yellow(colors_1.default.bold('review'))} Start a code review`);
|
|
76
|
+
console.log(` - ${colors_1.default.yellow(colors_1.default.bold('find <file-name>'))} Find and add files matching the name to the chat context (supports * for glob matching)`);
|
|
77
|
+
console.log(` - ${colors_1.default.yellow(colors_1.default.bold('add <file-path>'))} Add a file to the chat context (supports * for glob matching)`);
|
|
78
|
+
console.log(` - ${colors_1.default.yellow(colors_1.default.bold('rm <pattern>'))} Remove files from the chat context (supports * for glob matching)`);
|
|
79
|
+
console.log(` - ${colors_1.default.yellow(colors_1.default.bold('ls <pattern>'))} List files in the chat context (supports * for glob matching)`);
|
|
80
|
+
console.log(` - ${colors_1.default.yellow(colors_1.default.bold('git <command>'))} Run a git command`);
|
|
81
|
+
console.log(` - ${colors_1.default.yellow(colors_1.default.bold('help'))} Display this help message`);
|
|
82
|
+
console.log(` - ${colors_1.default.yellow(colors_1.default.bold('exit'))} Quit this session`);
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* Sets up event listeners for the readline interface and process input.
|
|
86
|
+
* @private
|
|
87
|
+
*/
|
|
46
88
|
setupEventListeners() {
|
|
47
89
|
this.rl.on('line', this.handleLine.bind(this));
|
|
48
90
|
this.rl.on('close', this.handleClose.bind(this));
|
|
49
91
|
process.stdin.on('keypress', this.handleKeypress.bind(this));
|
|
50
92
|
}
|
|
93
|
+
/**
|
|
94
|
+
* Loads context files specified in the options or defaults to the primary language's directory.
|
|
95
|
+
* @private
|
|
96
|
+
* @returns {Promise<void>}
|
|
97
|
+
*/
|
|
51
98
|
async loadContextFiles() {
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
else {
|
|
61
|
-
expandedFiles.push(path_1.default.relative(process.cwd(), filePath));
|
|
62
|
-
}
|
|
99
|
+
const config = (0, config_utils_1.getClovingConfig)();
|
|
100
|
+
const primaryLanguage = config.languages.find(lang => lang.primary);
|
|
101
|
+
const defaultDirectory = primaryLanguage ? primaryLanguage.directory : '.';
|
|
102
|
+
const testingDirectories = config.testingFrameworks?.map(framework => framework.directory) || [];
|
|
103
|
+
const testingDirectory = testingDirectories[0];
|
|
104
|
+
let files = this.options.files || [defaultDirectory, testingDirectory].filter(Boolean);
|
|
105
|
+
if (files.length > 0) {
|
|
106
|
+
console.log(`\nBuilding chat session context...\n`);
|
|
63
107
|
}
|
|
64
|
-
files = expandedFiles;
|
|
65
108
|
for (const file of files) {
|
|
66
|
-
|
|
67
|
-
if (
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
109
|
+
// Skip if the file is not a string
|
|
110
|
+
if (!file)
|
|
111
|
+
continue;
|
|
112
|
+
const previousCount = Object.keys(this.contextFiles).length;
|
|
113
|
+
this.contextFiles = await (0, command_utils_1.addFileOrDirectoryToContext)(file, this.contextFiles, this.options);
|
|
114
|
+
const newCount = Object.keys(this.contextFiles).length;
|
|
115
|
+
const addedCount = newCount - previousCount;
|
|
116
|
+
const totalTokens = this.calculateTotalTokens();
|
|
117
|
+
console.log(colors_1.default.cyan(`š Loaded context from: ${colors_1.default.bold(file)}`));
|
|
118
|
+
console.log(colors_1.default.green(` ā
Added ${addedCount} file(s) to context`));
|
|
119
|
+
console.log(colors_1.default.yellow(` š Total tokens in context: ${totalTokens.toLocaleString()}\n`));
|
|
71
120
|
}
|
|
72
121
|
}
|
|
122
|
+
calculateTotalTokens() {
|
|
123
|
+
return Object.values(this.contextFiles).reduce((total, content) => {
|
|
124
|
+
return total + Math.ceil(content.length / 4);
|
|
125
|
+
}, 0);
|
|
126
|
+
}
|
|
127
|
+
/**
|
|
128
|
+
* Handles each line of input from the user.
|
|
129
|
+
* @private
|
|
130
|
+
* @param {string} line - The input line from the user.
|
|
131
|
+
* @returns {Promise<void>}
|
|
132
|
+
*/
|
|
73
133
|
async handleLine(line) {
|
|
74
134
|
if (this.isProcessing) {
|
|
75
135
|
return;
|
|
76
136
|
}
|
|
137
|
+
const trimmedLine = line.trim();
|
|
138
|
+
if (this.handleMultilineInput(trimmedLine)) {
|
|
139
|
+
return;
|
|
140
|
+
}
|
|
141
|
+
if (trimmedLine === '') {
|
|
142
|
+
this.displayPrompt();
|
|
143
|
+
return;
|
|
144
|
+
}
|
|
145
|
+
this.updateCommandHistory(trimmedLine);
|
|
146
|
+
if (this.handleExitCommand(trimmedLine)) {
|
|
147
|
+
return;
|
|
148
|
+
}
|
|
149
|
+
await this.handleCommand(trimmedLine);
|
|
150
|
+
}
|
|
151
|
+
handleMultilineInput(line) {
|
|
77
152
|
if (this.isMultilineMode) {
|
|
78
|
-
if (line
|
|
153
|
+
if (line === '```') {
|
|
79
154
|
this.isMultilineMode = false;
|
|
80
|
-
|
|
155
|
+
this.handleCommand(this.multilineInput);
|
|
81
156
|
this.multilineInput = '';
|
|
82
157
|
}
|
|
83
158
|
else {
|
|
84
159
|
this.multilineInput += line + '\n';
|
|
85
160
|
this.rl.prompt();
|
|
86
|
-
return;
|
|
87
161
|
}
|
|
162
|
+
return true;
|
|
88
163
|
}
|
|
89
|
-
else if (line
|
|
164
|
+
else if (line === '```') {
|
|
90
165
|
this.isMultilineMode = true;
|
|
91
166
|
this.multilineInput = '';
|
|
92
167
|
console.log('Entering multiline mode. Type ``` on a new line to end.\n');
|
|
93
168
|
this.rl.prompt();
|
|
94
|
-
return;
|
|
95
|
-
}
|
|
96
|
-
const trimmedLine = line.trim();
|
|
97
|
-
if (trimmedLine === '') {
|
|
98
|
-
this.rl.prompt();
|
|
99
|
-
return;
|
|
169
|
+
return true;
|
|
100
170
|
}
|
|
101
|
-
|
|
102
|
-
|
|
171
|
+
return false;
|
|
172
|
+
}
|
|
173
|
+
handleExitCommand(command) {
|
|
174
|
+
if (command.toLowerCase() === 'exit') {
|
|
103
175
|
this.rl.close();
|
|
104
|
-
return;
|
|
176
|
+
return true;
|
|
105
177
|
}
|
|
106
|
-
|
|
178
|
+
return false;
|
|
107
179
|
}
|
|
108
180
|
updateCommandHistory(command) {
|
|
109
181
|
if (this.commandHistory[0] !== command) {
|
|
@@ -114,8 +186,33 @@ class ChatManager {
|
|
|
114
186
|
}
|
|
115
187
|
this.historyIndex = -1;
|
|
116
188
|
}
|
|
189
|
+
/**
|
|
190
|
+
* Handles various commands entered by the user.
|
|
191
|
+
* This function acts as a command router, delegating to specific handlers based on the input.
|
|
192
|
+
*
|
|
193
|
+
* @private
|
|
194
|
+
* @param {string} command - The command entered by the user.
|
|
195
|
+
* @returns {Promise<void>}
|
|
196
|
+
*
|
|
197
|
+
* @description
|
|
198
|
+
* Supported commands:
|
|
199
|
+
* - 'copy': Copies the last response to clipboard.
|
|
200
|
+
* - 'save': Saves changes to files.
|
|
201
|
+
* - 'commit': Commits changes to git with an AI-generated message.
|
|
202
|
+
* - 'ls': Lists files in the context (equivalent to 'ls *').
|
|
203
|
+
* - 'rm': Removes all files from the context (equivalent to 'rm *').
|
|
204
|
+
* - 'review': Starts a code review.
|
|
205
|
+
* - 'add <file-path>': Adds a file to the context.
|
|
206
|
+
* - 'rm <pattern>': Removes files matching the pattern from the context.
|
|
207
|
+
* - 'ls <pattern>': Lists files in the context matching the pattern.
|
|
208
|
+
* - 'git <command>': Executes a git command.
|
|
209
|
+
* - Any other input is processed as a user query to the AI.
|
|
210
|
+
*/
|
|
117
211
|
async handleCommand(command) {
|
|
118
212
|
switch (command) {
|
|
213
|
+
case 'help':
|
|
214
|
+
this.displayHelp();
|
|
215
|
+
break;
|
|
119
216
|
case 'copy':
|
|
120
217
|
await this.handleCopy();
|
|
121
218
|
break;
|
|
@@ -125,11 +222,29 @@ class ChatManager {
|
|
|
125
222
|
case 'commit':
|
|
126
223
|
await this.handleCommit();
|
|
127
224
|
break;
|
|
225
|
+
case 'ls':
|
|
226
|
+
this.handleList("ls *");
|
|
227
|
+
break;
|
|
228
|
+
case 'rm':
|
|
229
|
+
await this.handleRemove("rm *");
|
|
230
|
+
break;
|
|
128
231
|
case 'review':
|
|
129
232
|
await this.handleReview();
|
|
130
233
|
break;
|
|
131
234
|
default:
|
|
132
|
-
if (this.
|
|
235
|
+
if (this.isAddCommand(command)) {
|
|
236
|
+
await this.handleAdd(command);
|
|
237
|
+
}
|
|
238
|
+
else if (this.isFindCommand(command)) {
|
|
239
|
+
await this.handleFind(command);
|
|
240
|
+
}
|
|
241
|
+
else if (this.isRemoveCommand(command)) {
|
|
242
|
+
await this.handleRemove(command);
|
|
243
|
+
}
|
|
244
|
+
else if (this.isListCommand(command)) {
|
|
245
|
+
this.handleList(command);
|
|
246
|
+
}
|
|
247
|
+
else if (this.isGitCommand(command)) {
|
|
133
248
|
this.executeGitCommand(command);
|
|
134
249
|
}
|
|
135
250
|
else {
|
|
@@ -137,11 +252,144 @@ class ChatManager {
|
|
|
137
252
|
}
|
|
138
253
|
}
|
|
139
254
|
}
|
|
255
|
+
isFindCommand(command) {
|
|
256
|
+
const parts = command.trim().split(/\s+/);
|
|
257
|
+
return parts.length === 2 && parts[0] === 'find';
|
|
258
|
+
}
|
|
259
|
+
async handleFind(command) {
|
|
260
|
+
const fileName = command.slice(5).trim();
|
|
261
|
+
if (fileName) {
|
|
262
|
+
try {
|
|
263
|
+
const foundFiles = await this.findFiles(fileName);
|
|
264
|
+
if (foundFiles.length === 0) {
|
|
265
|
+
console.log(`No files found matching ${colors_1.default.bold(colors_1.default.red(fileName))}.`);
|
|
266
|
+
}
|
|
267
|
+
else {
|
|
268
|
+
for (const filePath of foundFiles) {
|
|
269
|
+
this.contextFiles = await (0, command_utils_1.addFileOrDirectoryToContext)(filePath, this.contextFiles, this.options);
|
|
270
|
+
const content = this.contextFiles[filePath];
|
|
271
|
+
const tokenEstimate = this.estimateTokens(content);
|
|
272
|
+
console.log(`\nAdded ${colors_1.default.bold(colors_1.default.green(filePath))} to this chat session's context (${colors_1.default.yellow(`~${tokenEstimate.toLocaleString()} tokens`)})`);
|
|
273
|
+
}
|
|
274
|
+
const totalTokens = this.calculateTotalTokens();
|
|
275
|
+
console.log(colors_1.default.yellow(`\nš Total tokens in context now: ${totalTokens.toLocaleString()}\n`));
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
catch (error) {
|
|
279
|
+
console.error(`Failed to find and add files matching ${colors_1.default.bold(colors_1.default.red(fileName))}:`, error);
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
else {
|
|
283
|
+
console.log('No file name provided.');
|
|
284
|
+
}
|
|
285
|
+
this.rl.prompt();
|
|
286
|
+
}
|
|
287
|
+
async findFiles(fileName) {
|
|
288
|
+
const { exec } = require('child_process');
|
|
289
|
+
return new Promise((resolve, reject) => {
|
|
290
|
+
exec(`find . -name "${fileName}"`, (error, stdout) => {
|
|
291
|
+
if (error) {
|
|
292
|
+
return reject(error);
|
|
293
|
+
}
|
|
294
|
+
const files = stdout.split('\n').filter((filePath) => filePath.trim() !== '')
|
|
295
|
+
.map((filePath) => filePath.startsWith('./') ? filePath.slice(2) : filePath);
|
|
296
|
+
resolve(files);
|
|
297
|
+
});
|
|
298
|
+
});
|
|
299
|
+
}
|
|
300
|
+
displayHelp() {
|
|
301
|
+
console.log('');
|
|
302
|
+
this.displayAvailableCommands();
|
|
303
|
+
console.log('\nFor any other input, I will process it as a request or question.\n');
|
|
304
|
+
this.rl.prompt();
|
|
305
|
+
}
|
|
306
|
+
isAddCommand(command) {
|
|
307
|
+
const parts = command.trim().split(/\s+/);
|
|
308
|
+
return parts.length === 2 && parts[0] === 'add';
|
|
309
|
+
}
|
|
310
|
+
async handleAdd(command) {
|
|
311
|
+
const filePath = command.slice(4).trim();
|
|
312
|
+
if (filePath) {
|
|
313
|
+
try {
|
|
314
|
+
this.contextFiles = await (0, command_utils_1.addFileOrDirectoryToContext)(filePath, this.contextFiles, this.options);
|
|
315
|
+
console.log(`\nAdded ${colors_1.default.bold(colors_1.default.green(filePath))} to this chat session's context`);
|
|
316
|
+
}
|
|
317
|
+
catch (error) {
|
|
318
|
+
console.error(`Failed to add ${colors_1.default.bold(colors_1.default.red(filePath))} to this chat session's context:`, error);
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
else {
|
|
322
|
+
console.log('No file path provided.');
|
|
323
|
+
}
|
|
324
|
+
this.rl.prompt();
|
|
325
|
+
}
|
|
326
|
+
isRemoveCommand(command) {
|
|
327
|
+
return command.startsWith('rm ');
|
|
328
|
+
}
|
|
329
|
+
// Function to convert glob-like pattern to a regular expression
|
|
330
|
+
globToRegExp(glob) {
|
|
331
|
+
const escapedGlob = glob.replace(/[-/\\^$+?.()|[\]{}]/g, '\\$&');
|
|
332
|
+
const regexString = escapedGlob.replace(/\*/g, '.*');
|
|
333
|
+
return new RegExp(`^${regexString}$`);
|
|
334
|
+
}
|
|
335
|
+
// Function to filter paths using the pattern
|
|
336
|
+
filterPaths(paths, pattern) {
|
|
337
|
+
const regex = this.globToRegExp(pattern);
|
|
338
|
+
return paths.filter(path => regex.test(path));
|
|
339
|
+
}
|
|
340
|
+
async handleRemove(command) {
|
|
341
|
+
const pattern = command.slice(3).trim();
|
|
342
|
+
if (pattern) {
|
|
343
|
+
const matchedFiles = this.filterPaths(Object.keys(this.contextFiles), pattern);
|
|
344
|
+
if (matchedFiles.length > 0) {
|
|
345
|
+
matchedFiles.forEach(filePath => {
|
|
346
|
+
delete this.contextFiles[filePath];
|
|
347
|
+
console.log(`Removed ${colors_1.default.bold(colors_1.default.green(filePath))} from this chat session's context files`);
|
|
348
|
+
});
|
|
349
|
+
}
|
|
350
|
+
else {
|
|
351
|
+
console.log(`No files matching pattern "${colors_1.default.bold(pattern)}" found in this chat session's context files, try running ${colors_1.default.yellow('ls')} to see the list of files.`);
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
else {
|
|
355
|
+
console.log('No pattern provided.');
|
|
356
|
+
}
|
|
357
|
+
this.rl.prompt();
|
|
358
|
+
}
|
|
359
|
+
isListCommand(command) {
|
|
360
|
+
return command.startsWith('ls ');
|
|
361
|
+
}
|
|
362
|
+
estimateTokens(text) {
|
|
363
|
+
// A simple estimation: 1 token ā 4 characters
|
|
364
|
+
return Math.ceil(text.length / 4);
|
|
365
|
+
}
|
|
366
|
+
handleList(command) {
|
|
367
|
+
const pattern = command.slice(3).trim();
|
|
368
|
+
const fileNames = Object.keys(this.contextFiles);
|
|
369
|
+
const matchedFiles = pattern ? this.filterPaths(fileNames, pattern) : fileNames;
|
|
370
|
+
if (matchedFiles.length > 0) {
|
|
371
|
+
console.log(`\nFiles in the current chat session context:`);
|
|
372
|
+
let totalTokens = 0;
|
|
373
|
+
matchedFiles.forEach(fileName => {
|
|
374
|
+
const content = this.contextFiles[fileName];
|
|
375
|
+
const tokenEstimate = this.estimateTokens(content);
|
|
376
|
+
totalTokens += tokenEstimate;
|
|
377
|
+
console.log(` - ${colors_1.default.bold(colors_1.default.green(fileName))} (${colors_1.default.yellow(`~${tokenEstimate.toLocaleString()} tokens`)})`);
|
|
378
|
+
});
|
|
379
|
+
console.log(`\nTotal files: ${matchedFiles.length}`);
|
|
380
|
+
console.log(`Total estimated tokens: ${colors_1.default.yellow(totalTokens.toLocaleString())}\n`);
|
|
381
|
+
}
|
|
382
|
+
else {
|
|
383
|
+
console.log('No files currently in context.');
|
|
384
|
+
}
|
|
385
|
+
this.rl.prompt();
|
|
386
|
+
}
|
|
140
387
|
async handleCopy() {
|
|
141
388
|
const lastResponse = this.chatHistory.filter(msg => msg.role === 'assistant').pop();
|
|
142
389
|
if (lastResponse) {
|
|
143
390
|
copy_paste_1.default.copy(lastResponse.content, () => {
|
|
144
391
|
console.info('Last response copied to clipboard.');
|
|
392
|
+
this.rl.prompt();
|
|
145
393
|
});
|
|
146
394
|
}
|
|
147
395
|
else {
|
|
@@ -149,22 +397,30 @@ class ChatManager {
|
|
|
149
397
|
}
|
|
150
398
|
}
|
|
151
399
|
async handleReview() {
|
|
152
|
-
this.
|
|
153
|
-
const reviewManager = new ReviewManager_1.default(this.
|
|
400
|
+
this.options.stream = false;
|
|
401
|
+
const reviewManager = new ReviewManager_1.default(this.options);
|
|
154
402
|
await reviewManager.review();
|
|
155
|
-
this.
|
|
403
|
+
this.options.stream = true;
|
|
404
|
+
this.rl.prompt();
|
|
405
|
+
}
|
|
406
|
+
async handleCommit() {
|
|
407
|
+
this.options.stream = false;
|
|
408
|
+
const commitManager = new CommitManager_1.default(this.options);
|
|
409
|
+
await commitManager.commit();
|
|
410
|
+
this.options.stream = true;
|
|
411
|
+
this.rl.prompt();
|
|
156
412
|
}
|
|
157
413
|
async handleSave() {
|
|
158
|
-
const lastResponse = this.chatHistory.
|
|
414
|
+
const lastResponse = this.chatHistory.slice().reverse().find(msg => msg.role === 'assistant');
|
|
159
415
|
if (lastResponse) {
|
|
160
|
-
const
|
|
161
|
-
if (Object.keys(
|
|
162
|
-
await (0, string_utils_1.
|
|
163
|
-
console.info('
|
|
416
|
+
const currentNewBlocks = (0, string_utils_1.extractCurrentNewBlocks)(lastResponse.content);
|
|
417
|
+
if (Object.keys(currentNewBlocks).length > 0) {
|
|
418
|
+
await (0, string_utils_1.applyAndSaveCurrentNewBlocks)(currentNewBlocks);
|
|
419
|
+
console.info(`\n${colors_1.default.bold('save')} has finished\n`);
|
|
164
420
|
this.rl.prompt();
|
|
165
421
|
}
|
|
166
422
|
else {
|
|
167
|
-
console.info('No
|
|
423
|
+
console.info('No changes found to save in the last response.');
|
|
168
424
|
this.rl.prompt();
|
|
169
425
|
}
|
|
170
426
|
}
|
|
@@ -173,32 +429,6 @@ class ChatManager {
|
|
|
173
429
|
this.rl.prompt();
|
|
174
430
|
}
|
|
175
431
|
}
|
|
176
|
-
async handleCommit() {
|
|
177
|
-
const diff = (0, child_process_1.execSync)('git diff HEAD').toString().trim();
|
|
178
|
-
if (!diff) {
|
|
179
|
-
console.error('No changes to commit.');
|
|
180
|
-
this.rl.prompt();
|
|
181
|
-
return;
|
|
182
|
-
}
|
|
183
|
-
this.prompt = (0, git_utils_1.generateCommitMessagePrompt)(diff);
|
|
184
|
-
this.gpt.stream = false;
|
|
185
|
-
const rawCommitMessage = await this.gpt.generateText({ prompt: this.prompt });
|
|
186
|
-
this.gpt.stream = true;
|
|
187
|
-
const commitMessage = (0, string_utils_1.extractMarkdown)(rawCommitMessage);
|
|
188
|
-
const tempCommitFilePath = path_1.default.join('.git', 'SUGGESTED_COMMIT_EDITMSG');
|
|
189
|
-
fs_1.default.writeFileSync(tempCommitFilePath, commitMessage);
|
|
190
|
-
try {
|
|
191
|
-
(0, child_process_1.execFileSync)('git', ['commit', '-a', '--edit', '--file', tempCommitFilePath], { stdio: 'inherit' });
|
|
192
|
-
}
|
|
193
|
-
catch (commitError) {
|
|
194
|
-
console.error('Commit was canceled or failed.');
|
|
195
|
-
}
|
|
196
|
-
fs_1.default.unlink(tempCommitFilePath, (err) => {
|
|
197
|
-
if (err)
|
|
198
|
-
throw err;
|
|
199
|
-
});
|
|
200
|
-
this.rl.prompt();
|
|
201
|
-
}
|
|
202
432
|
isGitCommand(command) {
|
|
203
433
|
return command.split(' ').length <= 3 && command.startsWith('git ');
|
|
204
434
|
}
|
|
@@ -211,6 +441,18 @@ class ChatManager {
|
|
|
211
441
|
}
|
|
212
442
|
this.rl.prompt();
|
|
213
443
|
}
|
|
444
|
+
/**
|
|
445
|
+
* Processes user input by sending it to the AI model and handling the response.
|
|
446
|
+
*
|
|
447
|
+
* This method manages the interaction with the AI model by:
|
|
448
|
+
* 1. Checking if a request is already being processed.
|
|
449
|
+
* 2. Initializing the chat history and generating a prompt.
|
|
450
|
+
* 3. Streaming the response from the AI model.
|
|
451
|
+
* 4. Handling the response stream and updating the chat history.
|
|
452
|
+
*
|
|
453
|
+
* @param {string} input - The user's input or request to be processed.
|
|
454
|
+
* @returns {Promise<void>} - A promise that resolves when the processing is complete.
|
|
455
|
+
*/
|
|
214
456
|
async processUserInput(input) {
|
|
215
457
|
if (this.isProcessing) {
|
|
216
458
|
console.log('Please wait for the current request to complete.');
|
|
@@ -219,73 +461,122 @@ class ChatManager {
|
|
|
219
461
|
this.isProcessing = true;
|
|
220
462
|
this.chunkManager = new ChunkManager_1.default();
|
|
221
463
|
try {
|
|
222
|
-
this.
|
|
464
|
+
this.initializeChatHistory(input);
|
|
223
465
|
this.prompt = this.generatePrompt(input);
|
|
224
|
-
const responseStream = await this.gpt.streamText({ prompt: this.prompt });
|
|
466
|
+
const responseStream = await this.gpt.streamText({ prompt: this.prompt, messages: this.chatHistory });
|
|
225
467
|
let accumulatedContent = '';
|
|
226
|
-
this.
|
|
227
|
-
let convertedStream = this.gpt.convertStream(buffer);
|
|
228
|
-
while (convertedStream !== null) {
|
|
229
|
-
const { output, lastChar } = convertedStream;
|
|
230
|
-
process.stdout.write(output);
|
|
231
|
-
accumulatedContent += output;
|
|
232
|
-
this.chunkManager.clearBuffer(lastChar);
|
|
233
|
-
buffer = buffer.slice(lastChar);
|
|
234
|
-
convertedStream = this.gpt.convertStream(buffer);
|
|
235
|
-
}
|
|
236
|
-
});
|
|
237
|
-
responseStream.data.on('data', (chunk) => {
|
|
238
|
-
const chunkString = chunk.toString();
|
|
239
|
-
this.chunkManager.addChunk(chunkString);
|
|
240
|
-
});
|
|
241
|
-
responseStream.data.on('end', () => {
|
|
242
|
-
this.chatHistory.push({ role: 'assistant', content: accumulatedContent.trim() });
|
|
243
|
-
this.isProcessing = false;
|
|
244
|
-
process.stdout.write('\n');
|
|
245
|
-
this.rl.prompt();
|
|
246
|
-
});
|
|
247
|
-
responseStream.data.on('error', (error) => {
|
|
248
|
-
console.error('Error streaming response:', error);
|
|
249
|
-
this.isProcessing = false;
|
|
250
|
-
process.stdout.write('\n');
|
|
251
|
-
this.rl.prompt();
|
|
252
|
-
});
|
|
468
|
+
this.handleResponseStream(responseStream, accumulatedContent);
|
|
253
469
|
}
|
|
254
470
|
catch (err) {
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
471
|
+
this.handleError(err);
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
initializeChatHistory(input) {
|
|
475
|
+
if (this.chatHistory.length === 0) {
|
|
476
|
+
const systemPrompt = (0, command_utils_1.generateCodegenPrompt)(this.contextFiles);
|
|
477
|
+
this.chatHistory.push({ role: 'user', content: systemPrompt });
|
|
478
|
+
this.chatHistory.push({ role: 'assistant', content: 'What would you like to do?' });
|
|
479
|
+
}
|
|
480
|
+
this.chatHistory.push({ role: 'user', content: input });
|
|
481
|
+
}
|
|
482
|
+
handleResponseStream(responseStream, accumulatedContent) {
|
|
483
|
+
this.chunkManager.on('content', (buffer) => {
|
|
484
|
+
let convertedStream = this.gpt.convertStream(buffer);
|
|
485
|
+
while (convertedStream !== null) {
|
|
486
|
+
const { output, lastChar } = convertedStream;
|
|
487
|
+
process.stdout.write(output);
|
|
488
|
+
accumulatedContent += output;
|
|
489
|
+
this.chunkManager.clearBuffer(lastChar);
|
|
490
|
+
buffer = buffer.slice(lastChar);
|
|
491
|
+
convertedStream = this.gpt.convertStream(buffer);
|
|
272
492
|
}
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
493
|
+
});
|
|
494
|
+
responseStream.data.on('data', (chunk) => {
|
|
495
|
+
const chunkString = chunk.toString();
|
|
496
|
+
this.chunkManager.addChunk(chunkString);
|
|
497
|
+
});
|
|
498
|
+
responseStream.data.on('end', () => {
|
|
499
|
+
this.finalizeResponse(accumulatedContent);
|
|
500
|
+
});
|
|
501
|
+
responseStream.data.on('error', (error) => {
|
|
502
|
+
console.error('Error streaming response:', error);
|
|
276
503
|
this.isProcessing = false;
|
|
504
|
+
process.stdout.write('\n');
|
|
277
505
|
this.rl.prompt();
|
|
506
|
+
});
|
|
507
|
+
}
|
|
508
|
+
finalizeResponse(accumulatedContent) {
|
|
509
|
+
this.chatHistory.push({ role: 'assistant', content: accumulatedContent.trim() });
|
|
510
|
+
this.isProcessing = false;
|
|
511
|
+
process.stdout.write(`
|
|
512
|
+
|
|
513
|
+
You can follow up with another request or:
|
|
514
|
+
- type ${colors_1.default.yellow(colors_1.default.bold('"save"'))} to save all the changes to files
|
|
515
|
+
- type ${colors_1.default.yellow(colors_1.default.bold('"commit"'))} to commit the changes to git with a AI-generated message
|
|
516
|
+
- type ${colors_1.default.yellow(colors_1.default.bold('"copy"'))} to copy the last response to clipboard
|
|
517
|
+
- type ${colors_1.default.yellow(colors_1.default.bold('"review"'))} to start a code review
|
|
518
|
+
- type ${colors_1.default.yellow(colors_1.default.bold('"add <file-path>"'))} to add a file to the chat context
|
|
519
|
+
- type ${colors_1.default.yellow(colors_1.default.bold('"rm <pattern>"'))} to remove files from the chat context
|
|
520
|
+
- type ${colors_1.default.yellow(colors_1.default.bold('"ls <pattern>"'))} to list files in the chat context
|
|
521
|
+
- type ${colors_1.default.yellow(colors_1.default.bold('"git <command>"'))} to run a git command
|
|
522
|
+
- type ${colors_1.default.yellow(colors_1.default.bold('"exit"'))} to quit this session
|
|
523
|
+
`);
|
|
524
|
+
this.rl.prompt();
|
|
525
|
+
}
|
|
526
|
+
handleError(err) {
|
|
527
|
+
const error = err;
|
|
528
|
+
let errorMessage = error.message || 'An error occurred.';
|
|
529
|
+
const errorNumber = error.response?.status || 'unknown';
|
|
530
|
+
switch (errorNumber) {
|
|
531
|
+
case 400:
|
|
532
|
+
errorMessage = "Invalid model or prompt size too large. Try specifying fewer files.";
|
|
533
|
+
break;
|
|
534
|
+
case 403:
|
|
535
|
+
errorMessage = "Inactive subscription or usage limit reached";
|
|
536
|
+
break;
|
|
537
|
+
case 429:
|
|
538
|
+
errorMessage = "Rate limit error";
|
|
539
|
+
break;
|
|
540
|
+
case 500:
|
|
541
|
+
errorMessage = "Internal server error";
|
|
542
|
+
break;
|
|
278
543
|
}
|
|
544
|
+
const promptTokens = Math.ceil(this.prompt.length / 4).toLocaleString();
|
|
545
|
+
console.error(`Error processing a ${promptTokens} token prompt:`, errorMessage, `(${errorNumber})\n`);
|
|
546
|
+
this.isProcessing = false;
|
|
547
|
+
this.rl.prompt();
|
|
279
548
|
}
|
|
280
|
-
|
|
549
|
+
/**
|
|
550
|
+
* Generates a comprehensive prompt for the AI model.
|
|
551
|
+
*
|
|
552
|
+
* This function constructs a detailed prompt that includes:
|
|
553
|
+
* 1. The user's current request
|
|
554
|
+
* 2. Contents of all context files
|
|
555
|
+
* 3. Full chat history (excluding the current request)
|
|
556
|
+
* 4. The current request repeated at the end
|
|
557
|
+
*
|
|
558
|
+
* @private
|
|
559
|
+
* @param {string} prompt - The user's current request or input.
|
|
560
|
+
* @returns {string} A formatted string containing the complete prompt for the AI.
|
|
561
|
+
*
|
|
562
|
+
* @description
|
|
563
|
+
* The generated prompt follows this structure:
|
|
564
|
+
* 1. "### Request" section with the current prompt
|
|
565
|
+
* 2. Contents of all context files, each prefixed with its file name
|
|
566
|
+
* 3. "### Full Chat History Context" section with all previous messages
|
|
567
|
+
* 4. "### Current Request" section repeating the current prompt
|
|
568
|
+
*
|
|
569
|
+
* This structure provides the AI with comprehensive context for generating
|
|
570
|
+
* accurate and relevant responses.
|
|
571
|
+
*/
|
|
572
|
+
generatePrompt(prompt) {
|
|
281
573
|
const contextFileContents = Object.keys(this.contextFiles)
|
|
282
574
|
.map((file) => `### Contents of ${file}\n\n${this.contextFiles[file]}\n\n`)
|
|
283
575
|
.join('\n');
|
|
284
576
|
const allButLast = this.chatHistory.slice(0, -1).map(msg => `${msg.role.toUpperCase()}: ${msg.content}`).join('\n\n');
|
|
285
|
-
const lastUserMessage = this.chatHistory.slice(-1).map(msg => msg.content).join('\n\n');
|
|
286
577
|
return `### Request
|
|
287
578
|
|
|
288
|
-
${
|
|
579
|
+
${prompt}
|
|
289
580
|
|
|
290
581
|
${contextFileContents}
|
|
291
582
|
|
|
@@ -295,14 +586,25 @@ ${allButLast}
|
|
|
295
586
|
|
|
296
587
|
### Current Request
|
|
297
588
|
|
|
298
|
-
${
|
|
589
|
+
${prompt}
|
|
299
590
|
|
|
300
|
-
|
|
591
|
+
### Note
|
|
592
|
+
|
|
593
|
+
Whenever possible, break up the changes into pieces and make sure every change is in its own CURRENT/NEW block.`;
|
|
301
594
|
}
|
|
302
595
|
handleClose() {
|
|
303
596
|
console.log('Goodbye!');
|
|
304
597
|
process.exit(0);
|
|
305
598
|
}
|
|
599
|
+
/**
|
|
600
|
+
* Displays the prompt in green and bold.
|
|
601
|
+
* This method should be used instead of directly calling this.rl.prompt().
|
|
602
|
+
* @private
|
|
603
|
+
*/
|
|
604
|
+
displayPrompt() {
|
|
605
|
+
this.rl.setPrompt(colors_1.default.green.bold('cloving> '));
|
|
606
|
+
this.rl.prompt();
|
|
607
|
+
}
|
|
306
608
|
handleKeypress(_, key) {
|
|
307
609
|
if (key && (key.name === 'up' || key.name === 'down')) {
|
|
308
610
|
if (key.name === 'up' && this.historyIndex < this.commandHistory.length - 1) {
|
|
@@ -319,6 +621,14 @@ ${lastUserMessage}`;
|
|
|
319
621
|
this.rl.write(null, { ctrl: true, name: 'u' });
|
|
320
622
|
}
|
|
321
623
|
}
|
|
624
|
+
else if (key && key.name === 'tab') {
|
|
625
|
+
const line = this.rl.line.trim();
|
|
626
|
+
const hits = specialCommands.filter((command) => command.startsWith(line));
|
|
627
|
+
if (hits.length > 0) {
|
|
628
|
+
this.rl.write(null, { ctrl: true, name: 'u' });
|
|
629
|
+
this.rl.write(hits[0]);
|
|
630
|
+
}
|
|
631
|
+
}
|
|
322
632
|
}
|
|
323
633
|
}
|
|
324
634
|
exports.default = ChatManager;
|