recoder-code 2.2.9 → 2.3.4
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/cli/interactive.js +194 -125
- package/cli/run.js +61 -19
- package/cli/user-onboarding.js +19 -2
- package/config.js +4 -3
- package/package.json +2 -2
- package/plugins/enhanced-plugin-manager.js +21 -9
- package/plugins/ide-integration-plugin.js +1 -1
- package/plugins/ide-integration-plugin.js.bak +235 -0
- package/plugins/productivity-plugin.js +1 -1
- package/plugins/productivity-plugin.js.bak +215 -0
- package/plugins/sample-plugin.js +1 -1
- package/plugins/sample-plugin.js.bak +7 -0
- package/plugins/team-collaboration-plugin.js +1 -1
- package/plugins/team-collaboration-plugin.js.bak +208 -0
- package/plugins/voice-commands-plugin.js +1 -1
- package/plugins/voice-commands-plugin.js.bak +136 -0
- package/plugins/weather-plugin.js +1 -1
- package/plugins/weather-plugin.js.bak +83 -0
package/cli/interactive.js
CHANGED
|
@@ -153,13 +153,15 @@ class InteractiveRecoderChat {
|
|
|
153
153
|
|
|
154
154
|
// Always initialize readline interface first to prevent errors
|
|
155
155
|
this.initializeReadlineInterface();
|
|
156
|
-
|
|
156
|
+
|
|
157
|
+
// Handle API key prompt and initialization
|
|
157
158
|
if (!this.apiKey) {
|
|
158
|
-
this.promptForApiKey()
|
|
159
|
-
|
|
159
|
+
this.promptForApiKey().then(() => {
|
|
160
|
+
this.initializeWithApiKey();
|
|
161
|
+
});
|
|
162
|
+
} else {
|
|
163
|
+
this.initializeWithApiKey();
|
|
160
164
|
}
|
|
161
|
-
|
|
162
|
-
this.initializeWithApiKey();
|
|
163
165
|
}
|
|
164
166
|
|
|
165
167
|
/**
|
|
@@ -944,9 +946,10 @@ class InteractiveRecoderChat {
|
|
|
944
946
|
|
|
945
947
|
async loadPlugins() {
|
|
946
948
|
try {
|
|
947
|
-
|
|
949
|
+
// Load plugins silently for production mode
|
|
950
|
+
await this.pluginManager.loadPlugins(true); // true = silent mode
|
|
948
951
|
} catch (error) {
|
|
949
|
-
|
|
952
|
+
// Silently fail - don't show errors
|
|
950
953
|
}
|
|
951
954
|
}
|
|
952
955
|
|
|
@@ -1029,7 +1032,7 @@ class InteractiveRecoderChat {
|
|
|
1029
1032
|
});
|
|
1030
1033
|
}
|
|
1031
1034
|
|
|
1032
|
-
promptForApiKey() {
|
|
1035
|
+
async promptForApiKey() {
|
|
1033
1036
|
console.clear();
|
|
1034
1037
|
console.log('');
|
|
1035
1038
|
console.log(chalk.magenta('╭─────────────────────────────────────────────────────────────╮'));
|
|
@@ -1039,41 +1042,52 @@ class InteractiveRecoderChat {
|
|
|
1039
1042
|
console.log(chalk.white('🔑 API key required to continue'));
|
|
1040
1043
|
console.log(chalk.gray('Get your free API key at: ') + chalk.magenta('https://openrouter.ai'));
|
|
1041
1044
|
console.log('');
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
process.env.OPENROUTER_API_KEY = apiKey.trim();
|
|
1058
|
-
this.apiKey = apiKey.trim();
|
|
1059
|
-
|
|
1060
|
-
// Continue initialization
|
|
1061
|
-
this.initializeWithApiKey();
|
|
1045
|
+
|
|
1046
|
+
// Use existing readline interface instead of creating a new one
|
|
1047
|
+
return new Promise((resolve) => {
|
|
1048
|
+
this.rl.question(chalk.magenta('Enter your OpenRouter API key: '), (apiKey) => {
|
|
1049
|
+
if (!apiKey || apiKey.trim() === '') {
|
|
1050
|
+
console.log(chalk.red('No API key provided. Exiting...'));
|
|
1051
|
+
process.exit(1);
|
|
1052
|
+
}
|
|
1053
|
+
|
|
1054
|
+
// Save the API key to environment
|
|
1055
|
+
process.env.OPENROUTER_API_KEY = apiKey.trim();
|
|
1056
|
+
this.apiKey = apiKey.trim();
|
|
1057
|
+
|
|
1058
|
+
resolve();
|
|
1059
|
+
});
|
|
1062
1060
|
});
|
|
1063
1061
|
}
|
|
1064
1062
|
|
|
1065
1063
|
initializeReadlineInterface() {
|
|
1064
|
+
// Don't re-initialize if already exists
|
|
1065
|
+
if (this.rl) {
|
|
1066
|
+
return;
|
|
1067
|
+
}
|
|
1068
|
+
|
|
1069
|
+
// Check if stdin is a TTY
|
|
1070
|
+
if (!process.stdin.isTTY) {
|
|
1071
|
+
console.error('Error: stdin is not a TTY. Please run in a proper terminal.');
|
|
1072
|
+
process.exit(1);
|
|
1073
|
+
}
|
|
1074
|
+
|
|
1066
1075
|
// Initialize readline interface with enhanced options
|
|
1076
|
+
// Set terminal based on environment variable or default to true
|
|
1077
|
+
// If you see double characters, set RECODER_TERMINAL_MODE=false
|
|
1078
|
+
const terminalMode = process.env.RECODER_TERMINAL_MODE !== 'false';
|
|
1079
|
+
|
|
1067
1080
|
this.rl = readline.createInterface({
|
|
1068
1081
|
input: process.stdin,
|
|
1069
1082
|
output: process.stdout,
|
|
1070
1083
|
prompt: chalk.gray('> '),
|
|
1071
1084
|
historySize: 100,
|
|
1072
1085
|
removeHistoryDuplicates: true,
|
|
1086
|
+
terminal: terminalMode,
|
|
1073
1087
|
completer: (line) => this.autoComplete(line)
|
|
1074
1088
|
});
|
|
1075
1089
|
|
|
1076
|
-
// Set up readline event handlers
|
|
1090
|
+
// Set up readline event handlers ONCE
|
|
1077
1091
|
this.setupReadlineHandlers();
|
|
1078
1092
|
}
|
|
1079
1093
|
|
|
@@ -1119,10 +1133,21 @@ class InteractiveRecoderChat {
|
|
|
1119
1133
|
this.analyzeProjectContext().catch(error => {
|
|
1120
1134
|
console.log(chalk.yellow(`⚠️ Project context analysis failed: ${error.message}`));
|
|
1121
1135
|
});
|
|
1122
|
-
this.
|
|
1136
|
+
this.displayWelcome();
|
|
1137
|
+
|
|
1138
|
+
// Load plugins after welcome, then show prompt again
|
|
1139
|
+
this.loadPlugins().then(() => {
|
|
1140
|
+
// Re-show prompt after plugins finish loading
|
|
1141
|
+
if (this.rl) {
|
|
1142
|
+
console.log(''); // Add blank line
|
|
1143
|
+
this.rl.prompt();
|
|
1144
|
+
}
|
|
1145
|
+
}).catch(error => {
|
|
1123
1146
|
console.log(chalk.yellow(`⚠️ Plugin loading failed: ${error.message}`));
|
|
1147
|
+
if (this.rl) {
|
|
1148
|
+
this.rl.prompt();
|
|
1149
|
+
}
|
|
1124
1150
|
});
|
|
1125
|
-
this.displayWelcome();
|
|
1126
1151
|
}
|
|
1127
1152
|
|
|
1128
1153
|
updatePrompt() {
|
|
@@ -1144,119 +1169,158 @@ class InteractiveRecoderChat {
|
|
|
1144
1169
|
return;
|
|
1145
1170
|
}
|
|
1146
1171
|
|
|
1147
|
-
// Handle slash commands
|
|
1172
|
+
// Handle slash commands
|
|
1148
1173
|
if (input.startsWith('/')) {
|
|
1149
1174
|
this.handleSlashCommand(input);
|
|
1150
1175
|
return;
|
|
1151
1176
|
}
|
|
1152
1177
|
|
|
1153
1178
|
try {
|
|
1154
|
-
// Track performance metrics
|
|
1155
1179
|
const startTime = Date.now();
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
// Start progressive loading with enhanced animations
|
|
1160
|
-
this.startProgressiveLoading();
|
|
1161
|
-
|
|
1162
|
-
// Create a RecoderCode instance to process the conversation
|
|
1180
|
+
|
|
1181
|
+
// Create RecoderCode instance
|
|
1163
1182
|
const { RecoderCode } = require('./run.js');
|
|
1183
|
+
if (this.apiKey && !process.env.OPENROUTER_API_KEY) {
|
|
1184
|
+
process.env.OPENROUTER_API_KEY = this.apiKey;
|
|
1185
|
+
}
|
|
1164
1186
|
const recoder = new RecoderCode();
|
|
1165
|
-
|
|
1166
|
-
//
|
|
1167
|
-
const
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1187
|
+
|
|
1188
|
+
// Override tool execution to show indicators
|
|
1189
|
+
const originalExecuteToolCall = recoder.executeToolCall.bind(recoder);
|
|
1190
|
+
recoder.executeToolCall = async (toolCall) => {
|
|
1191
|
+
const toolName = toolCall.function.name;
|
|
1192
|
+
process.stdout.write(chalk.dim(` ${toolName}...\r`));
|
|
1193
|
+
const result = await originalExecuteToolCall(toolCall);
|
|
1194
|
+
process.stdout.write(' \r');
|
|
1195
|
+
return result;
|
|
1172
1196
|
};
|
|
1173
|
-
|
|
1174
|
-
// Enhanced streaming response handler
|
|
1175
|
-
const response = await this.processWithAdvancedStreaming({
|
|
1176
|
-
recoder,
|
|
1177
|
-
input,
|
|
1178
|
-
startTime,
|
|
1179
|
-
streamBuffer,
|
|
1180
|
-
onFirstChunk: (time) => { firstChunkTime = time; },
|
|
1181
|
-
onChunk: (chunk) => {
|
|
1182
|
-
totalChunks++;
|
|
1183
|
-
this.displayStreamingChunk(chunk, totalChunks);
|
|
1184
|
-
}
|
|
1185
|
-
});
|
|
1186
|
-
|
|
1187
|
-
// Calculate comprehensive metrics
|
|
1188
|
-
const totalDuration = Math.round((Date.now() - startTime) / 1000);
|
|
1189
|
-
const timeToFirstChunk = firstChunkTime ? Math.round((firstChunkTime - startTime) / 1000) : null;
|
|
1190
|
-
|
|
1191
|
-
// Track conversation history with enhanced metadata
|
|
1192
|
-
this.conversationHistory.push({
|
|
1193
|
-
timestamp: new Date(),
|
|
1194
|
-
input: input,
|
|
1195
|
-
response: response,
|
|
1196
|
-
duration: totalDuration,
|
|
1197
|
-
timeToFirstChunk,
|
|
1198
|
-
totalChunks,
|
|
1199
|
-
streamingUsed: streamBuffer.isStreaming
|
|
1200
|
-
});
|
|
1201
|
-
this.currentSession.messageCount++;
|
|
1202
|
-
this.currentSession.lastActivity = Date.now();
|
|
1203
|
-
|
|
1204
|
-
console.log('');
|
|
1205
|
-
|
|
1206
|
-
// Show performance metrics and tool usage
|
|
1207
|
-
this.showAdvancedMetrics({
|
|
1208
|
-
duration: totalDuration,
|
|
1209
|
-
timeToFirstChunk,
|
|
1210
|
-
totalChunks,
|
|
1211
|
-
streamingUsed: streamBuffer.isStreaming
|
|
1212
|
-
});
|
|
1213
|
-
|
|
1214
|
-
// Enhanced response display if not already streamed
|
|
1215
|
-
if (!streamBuffer.streamingStarted) {
|
|
1216
|
-
await this.displayAdvancedTypingResponse(response, startTime);
|
|
1217
|
-
} else {
|
|
1218
|
-
// Finalize streaming display
|
|
1219
|
-
this.finalizeStreamingResponse(response, totalDuration);
|
|
1220
|
-
}
|
|
1221
1197
|
|
|
1222
|
-
//
|
|
1223
|
-
|
|
1224
|
-
this.stopStreamingDisplay();
|
|
1198
|
+
// Simple thinking indicator
|
|
1199
|
+
process.stdout.write(chalk.dim(' thinking...\r'));
|
|
1225
1200
|
|
|
1226
|
-
|
|
1201
|
+
// Get AI response (non-streaming for cleaner output)
|
|
1202
|
+
const response = await recoder.processConversation(input, false, false, null);
|
|
1203
|
+
|
|
1204
|
+
// Clear thinking indicator
|
|
1205
|
+
process.stdout.write(' \r');
|
|
1206
|
+
|
|
1207
|
+
// Display response and handle tool calls
|
|
1208
|
+
if (response) {
|
|
1209
|
+
// Check if response contains DeepSeek-style tool calls
|
|
1210
|
+
const hasToolCalls = response.includes('<|tool▁call▁begin|>');
|
|
1211
|
+
|
|
1212
|
+
if (hasToolCalls) {
|
|
1213
|
+
// Parse and execute tool calls
|
|
1214
|
+
const result = await this.parseAndExecuteToolCalls(response, recoder);
|
|
1215
|
+
|
|
1216
|
+
if (result.toolResults.length > 0) {
|
|
1217
|
+
// Show tool execution results
|
|
1218
|
+
console.log('');
|
|
1219
|
+
result.toolResults.forEach(tr => {
|
|
1220
|
+
console.log(chalk.dim(` ✓ ${tr.tool}`));
|
|
1221
|
+
});
|
|
1222
|
+
|
|
1223
|
+
// Get follow-up response with tool results
|
|
1224
|
+
const toolContext = result.toolResults.map(tr =>
|
|
1225
|
+
`Tool ${tr.tool} executed: ${tr.result.substring(0, 200)}`
|
|
1226
|
+
).join('\n');
|
|
1227
|
+
|
|
1228
|
+
const followUp = await recoder.processConversation(
|
|
1229
|
+
`Based on these tool results:\n${toolContext}\n\nProvide a helpful summary of what was done.`,
|
|
1230
|
+
false, false, null
|
|
1231
|
+
);
|
|
1232
|
+
|
|
1233
|
+
const cleanFollowUp = followUp.replace(/<|tool[^|]*|>/g, '').trim();
|
|
1234
|
+
if (cleanFollowUp) {
|
|
1235
|
+
console.log('\n' + cleanFollowUp + '\n');
|
|
1236
|
+
}
|
|
1237
|
+
} else {
|
|
1238
|
+
// No tools executed, show cleaned response
|
|
1239
|
+
const cleanResponse = result.cleanText;
|
|
1240
|
+
if (cleanResponse) {
|
|
1241
|
+
console.log('\n' + cleanResponse + '\n');
|
|
1242
|
+
}
|
|
1243
|
+
}
|
|
1244
|
+
} else {
|
|
1245
|
+
// No tool calls, display normally
|
|
1246
|
+
console.log('\n' + response + '\n');
|
|
1247
|
+
}
|
|
1248
|
+
|
|
1249
|
+
// Track conversation
|
|
1250
|
+
this.conversationHistory.push({
|
|
1251
|
+
timestamp: new Date(),
|
|
1252
|
+
input: input,
|
|
1253
|
+
response: response
|
|
1254
|
+
});
|
|
1255
|
+
this.currentSession.messageCount++;
|
|
1256
|
+
this.currentSession.lastActivity = Date.now();
|
|
1257
|
+
}
|
|
1227
1258
|
|
|
1228
1259
|
} catch (error) {
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
console.log('');
|
|
1232
|
-
this.showEnhancedError(error);
|
|
1233
|
-
console.log('');
|
|
1260
|
+
process.stdout.write(' \r');
|
|
1261
|
+
console.log(chalk.red('\n Error: ' + error.message + '\n'));
|
|
1234
1262
|
}
|
|
1235
1263
|
|
|
1236
1264
|
this.rl.prompt();
|
|
1237
1265
|
}
|
|
1238
1266
|
|
|
1267
|
+
async parseAndExecuteToolCalls(response, recoder) {
|
|
1268
|
+
const toolResults = [];
|
|
1269
|
+
let cleanText = response;
|
|
1270
|
+
|
|
1271
|
+
// Parse DeepSeek-style tool calls: <|tool▁call▁begin|>toolname<|tool▁sep|>{"args"}<|tool▁call▁end|>
|
|
1272
|
+
const toolCallRegex = /<|tool▁call▁begin|>(\w+)<|tool▁sep|>({[^}]+})<|tool▁call▁end|>/g;
|
|
1273
|
+
let match;
|
|
1274
|
+
|
|
1275
|
+
while ((match = toolCallRegex.exec(response)) !== null) {
|
|
1276
|
+
const toolName = match[1];
|
|
1277
|
+
const argsJson = match[2];
|
|
1278
|
+
|
|
1279
|
+
try {
|
|
1280
|
+
const args = JSON.parse(argsJson);
|
|
1281
|
+
|
|
1282
|
+
// Execute the tool
|
|
1283
|
+
let result = '';
|
|
1284
|
+
if (toolName === 'bash') {
|
|
1285
|
+
result = await recoder.handleBashCommand(args);
|
|
1286
|
+
} else if (toolName === 'str_replace_editor') {
|
|
1287
|
+
result = await recoder.handleFileOperation(args);
|
|
1288
|
+
} else if (toolName === 'codebase_search') {
|
|
1289
|
+
result = await recoder.handleCodebaseSearch(args);
|
|
1290
|
+
} else if (toolName === 'web_fetch') {
|
|
1291
|
+
result = await recoder.handleWebFetch(args);
|
|
1292
|
+
}
|
|
1293
|
+
|
|
1294
|
+
toolResults.push({ tool: toolName, args, result });
|
|
1295
|
+
|
|
1296
|
+
// Remove the tool call from clean text
|
|
1297
|
+
cleanText = cleanText.replace(match[0], '');
|
|
1298
|
+
} catch (error) {
|
|
1299
|
+
console.log(chalk.yellow(` ⚠ Tool ${toolName} failed: ${error.message}`));
|
|
1300
|
+
}
|
|
1301
|
+
}
|
|
1302
|
+
|
|
1303
|
+
// Clean up tool markers
|
|
1304
|
+
cleanText = cleanText.replace(/<|tool▁calls▁begin|>/g, '');
|
|
1305
|
+
cleanText = cleanText.replace(/<|tool▁calls▁end|>/g, '');
|
|
1306
|
+
cleanText = cleanText.trim();
|
|
1307
|
+
|
|
1308
|
+
return { toolResults, cleanText };
|
|
1309
|
+
}
|
|
1310
|
+
|
|
1239
1311
|
displayWelcome() {
|
|
1240
1312
|
console.clear();
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
//
|
|
1246
|
-
this.displayAnimatedBorder();
|
|
1247
|
-
|
|
1248
|
-
// Beautiful gradient ASCII art
|
|
1249
|
-
console.log('');
|
|
1250
|
-
this.displayGradientLogo();
|
|
1313
|
+
|
|
1314
|
+
const projectName = this.getCurrentProjectContext().name;
|
|
1315
|
+
const modelName = this.model || 'deepseek/deepseek-chat-v3.1:free';
|
|
1316
|
+
|
|
1317
|
+
// Simple, clean header like Claude Code
|
|
1251
1318
|
console.log('');
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
1319
|
+
console.log(chalk.cyan.bold(' Recoder Code') + chalk.dim(` v${require('../package.json').version}`));
|
|
1320
|
+
console.log(chalk.dim(` ${modelName}`));
|
|
1321
|
+
console.log(chalk.dim(` ${process.cwd()}`));
|
|
1255
1322
|
console.log('');
|
|
1256
|
-
|
|
1257
|
-
// Enhanced status hints with themes
|
|
1258
|
-
this.showEnhancedStatusHints();
|
|
1259
|
-
|
|
1323
|
+
|
|
1260
1324
|
this.rl.prompt();
|
|
1261
1325
|
}
|
|
1262
1326
|
|
|
@@ -4113,6 +4177,7 @@ ${projectInfo}`
|
|
|
4113
4177
|
|
|
4114
4178
|
async processWithAdvancedStreaming({ recoder, input, startTime, streamBuffer, onFirstChunk, onChunk }) {
|
|
4115
4179
|
try {
|
|
4180
|
+
console.log(chalk.gray('[DEBUG] Starting API call...'));
|
|
4116
4181
|
// Attempt streaming first
|
|
4117
4182
|
const response = await recoder.processConversation(input, false, true, {
|
|
4118
4183
|
onChunk: (chunk) => {
|
|
@@ -4122,19 +4187,23 @@ ${projectInfo}`
|
|
|
4122
4187
|
streamBuffer.streamingStarted = true;
|
|
4123
4188
|
onFirstChunk(Date.now());
|
|
4124
4189
|
}
|
|
4125
|
-
|
|
4190
|
+
|
|
4126
4191
|
streamBuffer.content += chunk;
|
|
4127
4192
|
streamBuffer.chunks.push(chunk);
|
|
4128
4193
|
streamBuffer.isStreaming = true;
|
|
4129
4194
|
onChunk(chunk);
|
|
4130
4195
|
}
|
|
4131
4196
|
});
|
|
4132
|
-
|
|
4133
|
-
return
|
|
4134
|
-
|
|
4197
|
+
|
|
4198
|
+
// If streaming was used, return the accumulated content
|
|
4199
|
+
// Otherwise return the response (which is the final message content)
|
|
4200
|
+
return streamBuffer.streamingStarted ? streamBuffer.content : response;
|
|
4201
|
+
|
|
4135
4202
|
} catch (error) {
|
|
4136
4203
|
// Fallback to non-streaming
|
|
4137
4204
|
this.stopProgressiveLoading();
|
|
4205
|
+
console.log(chalk.red('\n❌ Streaming failed:', error.message));
|
|
4206
|
+
console.log(chalk.yellow('Trying non-streaming mode...'));
|
|
4138
4207
|
return await recoder.processConversation(input, false, false, null);
|
|
4139
4208
|
}
|
|
4140
4209
|
}
|
package/cli/run.js
CHANGED
|
@@ -92,10 +92,11 @@ class RecoderCode {
|
|
|
92
92
|
console.log(chalk.white(' recoder-code --setup\n'));
|
|
93
93
|
process.exit(1);
|
|
94
94
|
}
|
|
95
|
-
|
|
95
|
+
// Don't return early - initialize with empty key and it will be set later
|
|
96
|
+
this.apiKey = process.env.OPENROUTER_API_KEY || '';
|
|
96
97
|
}
|
|
97
98
|
|
|
98
|
-
//
|
|
99
|
+
// Always initialize OpenAI client (will use apiKey from config or empty string)
|
|
99
100
|
this.openai = new OpenAI({
|
|
100
101
|
baseURL: config.baseURL,
|
|
101
102
|
apiKey: this.apiKey,
|
|
@@ -265,7 +266,7 @@ class RecoderCode {
|
|
|
265
266
|
}
|
|
266
267
|
}
|
|
267
268
|
|
|
268
|
-
async callOpenRouter(messages, tools = null, useAutoRouter = false, streamResponse = false) {
|
|
269
|
+
async callOpenRouter(messages, tools = null, useAutoRouter = false, streamResponse = false, onChunkCallback = null) {
|
|
269
270
|
const payload = {
|
|
270
271
|
model: useAutoRouter ? 'openrouter/auto' : this.model,
|
|
271
272
|
messages: messages,
|
|
@@ -301,13 +302,15 @@ class RecoderCode {
|
|
|
301
302
|
if (tools) {
|
|
302
303
|
payload.tools = tools;
|
|
303
304
|
payload.tool_choice = 'auto';
|
|
305
|
+
// Disable streaming when tools are used
|
|
306
|
+
payload.stream = false;
|
|
304
307
|
}
|
|
305
308
|
|
|
306
309
|
try {
|
|
307
310
|
if (process.env.RECODER_DEBUG === 'true') {
|
|
308
311
|
console.log(`🔄 Using model: ${payload.model} ${payload.models ? `with fallbacks: ${payload.models.slice(1).join(', ')}` : ''}`);
|
|
309
312
|
}
|
|
310
|
-
|
|
313
|
+
|
|
311
314
|
if (streamResponse && !tools) {
|
|
312
315
|
// Handle streaming response
|
|
313
316
|
const stream = await this.openai.chat.completions.create(payload);
|
|
@@ -320,14 +323,19 @@ class RecoderCode {
|
|
|
320
323
|
if (process.env.RECODER_DEBUG === 'true') {
|
|
321
324
|
console.log('\nDEBUG - Chunk:', JSON.stringify(chunk, null, 2));
|
|
322
325
|
}
|
|
323
|
-
|
|
326
|
+
|
|
324
327
|
if (chunk.choices && chunk.choices.length > 0 && chunk.choices[0]) {
|
|
325
328
|
const content = chunk.choices[0]?.delta?.content || '';
|
|
326
329
|
if (content) {
|
|
327
|
-
|
|
330
|
+
// Call the callback if provided, otherwise write to stdout
|
|
331
|
+
if (onChunkCallback) {
|
|
332
|
+
onChunkCallback(content);
|
|
333
|
+
} else {
|
|
334
|
+
process.stdout.write(content);
|
|
335
|
+
}
|
|
328
336
|
fullContent += content;
|
|
329
337
|
}
|
|
330
|
-
|
|
338
|
+
|
|
331
339
|
// Store the full response structure from the last chunk
|
|
332
340
|
if (chunk.choices[0]?.finish_reason) {
|
|
333
341
|
fullResponse = {
|
|
@@ -352,7 +360,7 @@ class RecoderCode {
|
|
|
352
360
|
this.logRequest(payload, response);
|
|
353
361
|
return response;
|
|
354
362
|
}
|
|
355
|
-
|
|
363
|
+
|
|
356
364
|
// If no response was captured from streaming, create a basic one
|
|
357
365
|
if (!fullResponse) {
|
|
358
366
|
fullResponse = {
|
|
@@ -367,8 +375,10 @@ class RecoderCode {
|
|
|
367
375
|
usage: null
|
|
368
376
|
};
|
|
369
377
|
}
|
|
370
|
-
|
|
371
|
-
|
|
378
|
+
|
|
379
|
+
if (!onChunkCallback) {
|
|
380
|
+
console.log('\n'); // Add newline after streaming only if we wrote to stdout
|
|
381
|
+
}
|
|
372
382
|
this.logRequest(payload, fullResponse);
|
|
373
383
|
return fullResponse;
|
|
374
384
|
} else {
|
|
@@ -385,7 +395,23 @@ class RecoderCode {
|
|
|
385
395
|
}
|
|
386
396
|
} catch (error) {
|
|
387
397
|
this.logRequest(payload, null, error);
|
|
388
|
-
|
|
398
|
+
|
|
399
|
+
// Show detailed error message to help user debug
|
|
400
|
+
console.log(chalk.red('\n❌ API Request Failed in callOpenRouter:'));
|
|
401
|
+
|
|
402
|
+
// Check for authentication errors specifically
|
|
403
|
+
if (error.status === 401 || error.message?.includes('401')) {
|
|
404
|
+
console.log(chalk.red(' Authentication failed - Invalid API key'));
|
|
405
|
+
console.log(chalk.yellow(' Please check your OPENROUTER_API_KEY'));
|
|
406
|
+
} else if (error.status === 402) {
|
|
407
|
+
console.log(chalk.red(' Insufficient credits'));
|
|
408
|
+
console.log(chalk.yellow(' Please add credits to your OpenRouter account'));
|
|
409
|
+
} else {
|
|
410
|
+
console.log(chalk.yellow(' Status:', error.status || 'N/A'));
|
|
411
|
+
console.log(chalk.yellow(' Message:', error.message || 'N/A'));
|
|
412
|
+
console.log(chalk.yellow(' Code:', error.code || 'N/A'));
|
|
413
|
+
}
|
|
414
|
+
|
|
389
415
|
// Let unified error handler manage the error instead of re-throwing
|
|
390
416
|
// Return null for graceful degradation
|
|
391
417
|
return null;
|
|
@@ -694,7 +720,25 @@ Pending Tasks: ${projectStatus.todos.pending}
|
|
|
694
720
|
Documentation: Plan=${projectStatus.files.plan ? '✓' : '✗'} Architecture=${projectStatus.files.architecture ? '✓' : '✗'} Notes=${projectStatus.files.notes ? '✓' : '✗'}`;
|
|
695
721
|
}
|
|
696
722
|
|
|
697
|
-
async processConversation(userMessage, useAutoRouter = false, enableStreaming = false,
|
|
723
|
+
async processConversation(userMessage, useAutoRouter = false, enableStreaming = false, streamingOptions = null) {
|
|
724
|
+
// streamingOptions can be:
|
|
725
|
+
// - null/false: regular resumeSession string for backward compatibility
|
|
726
|
+
// - object with { onChunk: callback }: streaming with callback
|
|
727
|
+
// - true: enable streaming without callback
|
|
728
|
+
|
|
729
|
+
let resumeSession = null;
|
|
730
|
+
let onChunkCallback = null;
|
|
731
|
+
|
|
732
|
+
if (streamingOptions) {
|
|
733
|
+
if (typeof streamingOptions === 'object' && streamingOptions.onChunk) {
|
|
734
|
+
// New streaming mode with callback
|
|
735
|
+
onChunkCallback = streamingOptions.onChunk;
|
|
736
|
+
} else if (typeof streamingOptions === 'string') {
|
|
737
|
+
// Backward compatibility: string is resumeSession
|
|
738
|
+
resumeSession = streamingOptions;
|
|
739
|
+
}
|
|
740
|
+
}
|
|
741
|
+
|
|
698
742
|
// If resuming a session, restore conversation history
|
|
699
743
|
if (resumeSession) {
|
|
700
744
|
const session = this.projectManager.loadSession(resumeSession);
|
|
@@ -702,9 +746,8 @@ Documentation: Plan=${projectStatus.files.plan ? '✓' : '✗'} Architecture=${p
|
|
|
702
746
|
this.conversationHistory = [...session.conversationHistory];
|
|
703
747
|
console.log(chalk.cyan(`🔄 Resumed session: ${resumeSession}`));
|
|
704
748
|
console.log(chalk.gray(` Previous messages: ${this.conversationHistory.length}`));
|
|
705
|
-
} else {
|
|
706
|
-
console.log(chalk.yellow(`⚠️ Could not resume session: ${resumeSession}`));
|
|
707
749
|
}
|
|
750
|
+
// Silently ignore session resume failures - don't warn users
|
|
708
751
|
}
|
|
709
752
|
|
|
710
753
|
// Check if this is a slash command
|
|
@@ -815,12 +858,11 @@ Project context: ${this.getProjectContextString()}${this.getRulesContextString()
|
|
|
815
858
|
|
|
816
859
|
const messages = [systemMessage, ...this.conversationHistory];
|
|
817
860
|
|
|
818
|
-
let response = await this.callOpenRouter(messages, config.tools, useAutoRouter, enableStreaming);
|
|
819
|
-
|
|
861
|
+
let response = await this.callOpenRouter(messages, config.tools, useAutoRouter, enableStreaming, onChunkCallback);
|
|
862
|
+
|
|
820
863
|
// Add safety checks for response structure
|
|
821
864
|
if (!response || !response.choices || !response.choices[0]) {
|
|
822
|
-
|
|
823
|
-
return 'Unable to process request - API service unavailable. Please check your API key and credits.';
|
|
865
|
+
return 'Unable to process request. Please check your API key and try again.';
|
|
824
866
|
}
|
|
825
867
|
|
|
826
868
|
while (response.choices[0].message && response.choices[0].message.tool_calls) {
|
|
@@ -838,7 +880,7 @@ Project context: ${this.getProjectContextString()}${this.getRulesContextString()
|
|
|
838
880
|
}
|
|
839
881
|
|
|
840
882
|
// Don't stream when there are tool calls, as we need to process them
|
|
841
|
-
response = await this.callOpenRouter([systemMessage, ...this.conversationHistory], null, useAutoRouter, false);
|
|
883
|
+
response = await this.callOpenRouter([systemMessage, ...this.conversationHistory], null, useAutoRouter, false, null);
|
|
842
884
|
|
|
843
885
|
// Add safety check again
|
|
844
886
|
if (!response || !response.choices || !response.choices[0]) {
|
package/cli/user-onboarding.js
CHANGED
|
@@ -237,9 +237,26 @@ class UserOnboarding {
|
|
|
237
237
|
*/
|
|
238
238
|
async runOnboarding() {
|
|
239
239
|
this.showWelcome();
|
|
240
|
-
|
|
240
|
+
|
|
241
241
|
const userName = await this.getUserName();
|
|
242
|
-
|
|
242
|
+
|
|
243
|
+
// Check if API key already exists in .env file
|
|
244
|
+
const envFile = path.join(require('os').homedir(), '.recoder-code', '.env');
|
|
245
|
+
let apiKey = null;
|
|
246
|
+
|
|
247
|
+
if (fs.existsSync(envFile)) {
|
|
248
|
+
const envContent = fs.readFileSync(envFile, 'utf8');
|
|
249
|
+
const match = envContent.match(/OPENROUTER_API_KEY=(.+)/);
|
|
250
|
+
if (match && match[1] && match[1].startsWith('sk-or-v1-')) {
|
|
251
|
+
apiKey = match[1].trim();
|
|
252
|
+
console.log(chalk.green('\n✅ Found existing API key in .env file'));
|
|
253
|
+
console.log(chalk.gray(' Using: ' + apiKey.substring(0, 20) + '...'));
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
if (!apiKey) {
|
|
258
|
+
apiKey = await this.getApiKey();
|
|
259
|
+
}
|
|
243
260
|
|
|
244
261
|
if (!apiKey) {
|
|
245
262
|
console.log(chalk.red('\n❌ API key is required to use Recoder Code.'));
|
package/config.js
CHANGED
|
@@ -6,12 +6,13 @@ const fs = require('fs');
|
|
|
6
6
|
const userEnvPath = path.join(os.homedir(), '.recoder-code', '.env');
|
|
7
7
|
const projectEnvPath = path.join(process.cwd(), '.env');
|
|
8
8
|
|
|
9
|
+
// Suppress dotenv logs with quiet option
|
|
9
10
|
if (fs.existsSync(userEnvPath)) {
|
|
10
|
-
require('dotenv').config({ path: userEnvPath });
|
|
11
|
+
require('dotenv').config({ path: userEnvPath, override: false, quiet: true });
|
|
11
12
|
} else if (fs.existsSync(projectEnvPath)) {
|
|
12
|
-
require('dotenv').config({ path: projectEnvPath });
|
|
13
|
+
require('dotenv').config({ path: projectEnvPath, override: false, quiet: true });
|
|
13
14
|
} else {
|
|
14
|
-
require('dotenv').config();
|
|
15
|
+
require('dotenv').config({ override: false, quiet: true });
|
|
15
16
|
}
|
|
16
17
|
|
|
17
18
|
module.exports = {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "recoder-code",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.3.4",
|
|
4
4
|
"description": "🚀 AI-powered development platform - Chat with 32+ models, build projects, automate workflows. Free models included!",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"scripts": {
|
|
@@ -34,7 +34,7 @@
|
|
|
34
34
|
"production:check": "npm run lint && npm run typecheck && npm run security:audit",
|
|
35
35
|
"production:bundle": "npm run production:check && npm run build:production",
|
|
36
36
|
"production:deploy": "npm run production:bundle && npm publish --access public",
|
|
37
|
-
"postinstall": "node -e \"console.log('\\\\n🚀 Recoder-Code v2.
|
|
37
|
+
"postinstall": "node -e \"console.log('\\\\n🚀 Recoder-Code v2.3.3 installed!\\\\n\\\\n✅ Quick Start: recoder-code\\\\n💡 Get API key: https://openrouter.ai\\\\n')\"",
|
|
38
38
|
"prepare": "echo 'Package prepared for distribution'",
|
|
39
39
|
"prepublishOnly": "npm run prepare && npm run security:audit"
|
|
40
40
|
},
|