corex-cli 1.0.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/.env.example +1 -0
- package/.vscode/launch.json +15 -0
- package/Corex AI TERMINAL CLI +7 -0
- package/Corex AI TERMINAL CLI.pub +1 -0
- package/README.md +32 -0
- package/assets/COREX_SYSTEM_PROMPT.txt +155 -0
- package/assets/logo.txt +10 -0
- package/bin/corex.js +904 -0
- package/corex-ai-terminal-cli@1.0.0 +0 -0
- package/dist/index.js +742 -0
- package/install.sh +26 -0
- package/package.json +34 -0
- package/src/app.tsx +217 -0
- package/src/components/ApiKeyScreen.tsx +65 -0
- package/src/components/BootScreen.tsx +62 -0
- package/src/components/ChatHistory.tsx +45 -0
- package/src/components/Header.tsx +60 -0
- package/src/components/InputBar.tsx +43 -0
- package/src/components/StatusArea.tsx +23 -0
- package/src/components/StatusBar.tsx +27 -0
- package/src/components/ThinkingDots.tsx +22 -0
- package/src/components/TopBar.tsx +31 -0
- package/src/core/network/request.ts +211 -0
- package/src/core/providers/anthropic.ts +107 -0
- package/src/core/providers/gemini.ts +56 -0
- package/src/core/providers/index.ts +4 -0
- package/src/core/providers/openai.ts +64 -0
- package/src/index.ts +62 -0
- package/src/lib/ai.ts +167 -0
- package/src/lib/config.ts +250 -0
- package/src/lib/history.ts +43 -0
- package/src/lib/markdown.ts +3 -0
- package/src/themes/themes.ts +70 -0
- package/src/types/gradient-string.d.ts +12 -0
- package/src/types.ts +34 -0
- package/tsconfig.json +20 -0
- package/tsup.config.ts +12 -0
- package/tsx +0 -0
package/bin/corex.js
ADDED
|
@@ -0,0 +1,904 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import * as fs from 'fs';
|
|
4
|
+
import * as path from 'path';
|
|
5
|
+
import * as os from 'os';
|
|
6
|
+
import * as readline from 'readline';
|
|
7
|
+
import { createRequire } from 'module';
|
|
8
|
+
import { fileURLToPath } from 'url';
|
|
9
|
+
import Anthropic from '@anthropic-ai/sdk';
|
|
10
|
+
import { GoogleGenerativeAI } from '@google/generative-ai';
|
|
11
|
+
import OpenAI from 'openai';
|
|
12
|
+
import { glob } from 'glob';
|
|
13
|
+
|
|
14
|
+
const require = createRequire(import.meta.url);
|
|
15
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
16
|
+
const __dirname = path.dirname(__filename);
|
|
17
|
+
|
|
18
|
+
// ═══════════════════════════════════════════════════════════════════
|
|
19
|
+
// CONFIG
|
|
20
|
+
// ═══════════════════════════════════════════════════════════════════
|
|
21
|
+
|
|
22
|
+
const CONFIG_DIR = path.join(os.homedir(), '.corex');
|
|
23
|
+
const CONFIG_PATH = path.join(CONFIG_DIR, 'config.json');
|
|
24
|
+
|
|
25
|
+
function loadConfig() {
|
|
26
|
+
try {
|
|
27
|
+
const raw = fs.readFileSync(CONFIG_PATH, 'utf-8');
|
|
28
|
+
const parsed = JSON.parse(raw);
|
|
29
|
+
if (!parsed.apiKey || !parsed.provider) return null;
|
|
30
|
+
return parsed;
|
|
31
|
+
} catch (e) {
|
|
32
|
+
return null;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function saveConfig(data) {
|
|
37
|
+
fs.mkdirSync(path.dirname(CONFIG_PATH), { recursive: true });
|
|
38
|
+
const existing = loadConfig() || {};
|
|
39
|
+
const merged = { ...existing, ...data };
|
|
40
|
+
fs.writeFileSync(CONFIG_PATH, JSON.stringify(merged, null, 2));
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// ═══════════════════════════════════════════════════════════════════
|
|
44
|
+
// SYSTEM PROMPT
|
|
45
|
+
// ═══════════════════════════════════════════════════════════════════
|
|
46
|
+
|
|
47
|
+
const FALLBACK_SYSTEM_PROMPT = `You are COREX, an elite AI assistant.
|
|
48
|
+
You are direct, insightful, and technically brilliant.
|
|
49
|
+
Format your responses for terminal display using clean spacing.
|
|
50
|
+
When showing code, use markdown code blocks.
|
|
51
|
+
Keep responses focused and avoid unnecessary filler text.`;
|
|
52
|
+
|
|
53
|
+
function loadSystemPrompt() {
|
|
54
|
+
const filename = 'COREX_SYSTEM_PROMPT.txt';
|
|
55
|
+
const possiblePaths = [
|
|
56
|
+
path.join(__dirname, '..', 'assets', filename),
|
|
57
|
+
path.join(__dirname, '..', '..', 'assets', filename),
|
|
58
|
+
path.join(process.cwd(), 'assets', filename),
|
|
59
|
+
];
|
|
60
|
+
for (const p of possiblePaths) {
|
|
61
|
+
try {
|
|
62
|
+
if (fs.existsSync(p)) return fs.readFileSync(p, 'utf-8').trim();
|
|
63
|
+
} catch { /* continue */ }
|
|
64
|
+
}
|
|
65
|
+
return FALLBACK_SYSTEM_PROMPT;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const SYSTEM_PROMPT = loadSystemPrompt();
|
|
69
|
+
|
|
70
|
+
// ═══════════════════════════════════════════════════════════════════
|
|
71
|
+
// i18n STRINGS
|
|
72
|
+
// ═══════════════════════════════════════════════════════════════════
|
|
73
|
+
|
|
74
|
+
const STRINGS = {
|
|
75
|
+
en: {
|
|
76
|
+
tips_header: 'Tips for getting started:',
|
|
77
|
+
tips_1: '1. Ask questions, edit files, or run commands.',
|
|
78
|
+
tips_2: '2. Be specific for the best results.',
|
|
79
|
+
tips_3: '3. /help for more information.',
|
|
80
|
+
apikey_instruction: 'Enter your API key below and use what you purchased.',
|
|
81
|
+
apikey_supports: 'Supports: Anthropic · Gemini · OpenAI · OpenRouter · DeepSeek',
|
|
82
|
+
apikey_auto: 'Your key will be auto-detected. No manual setup needed.',
|
|
83
|
+
apikey_prompt: 'API Key ❯ ',
|
|
84
|
+
detected: '✓ Detected: ',
|
|
85
|
+
not_detected: '⚠ Could not detect provider. Select manually:',
|
|
86
|
+
select_provider: 'Select AI Provider:',
|
|
87
|
+
select_theme: 'Select Theme:',
|
|
88
|
+
select_lang: 'Select Language:',
|
|
89
|
+
select_model: 'Select Model:',
|
|
90
|
+
theme_changed: '✓ Theme changed to: ',
|
|
91
|
+
lang_changed: '✓ Language changed to: ',
|
|
92
|
+
model_changed: '✓ Model set to: ',
|
|
93
|
+
copied: '✓ Copied to clipboard.',
|
|
94
|
+
nothing_to_copy: '⚠ No response to copy yet.',
|
|
95
|
+
screen_cleared: '',
|
|
96
|
+
logged_out: "Logged out. Run 'corex' to set up again.",
|
|
97
|
+
invalid_key: '✗ Invalid API key. Run /config to update.',
|
|
98
|
+
network_error: '✗ Network error. Check your connection.',
|
|
99
|
+
file_not_found: '✗ File not found: ',
|
|
100
|
+
file_read_error: '✗ Error reading file: ',
|
|
101
|
+
no_key_provided: '✗ No API key provided.',
|
|
102
|
+
config_updated: '✓ Config updated.',
|
|
103
|
+
provider_changed: '✓ Provider changed to ',
|
|
104
|
+
help_title: 'Commands:',
|
|
105
|
+
help_model: '/model Switch AI model (arrow keys + Enter)',
|
|
106
|
+
help_theme: '/theme Change color theme (arrow keys + Enter)',
|
|
107
|
+
help_lang: '/lang Change interface language',
|
|
108
|
+
help_config: '/config Change provider or API key',
|
|
109
|
+
help_help: '/help Show this message',
|
|
110
|
+
help_ctrlc: 'Ctrl+C Exit',
|
|
111
|
+
help_file_title: 'File upload:',
|
|
112
|
+
help_file_usage: '@filename Attach a file to your message',
|
|
113
|
+
help_file_example: 'Example: @notes.txt summarize this',
|
|
114
|
+
help_outside_title: 'Outside chat:',
|
|
115
|
+
help_logout: 'corex logout Reset all saved data',
|
|
116
|
+
navigate_hint: '↑ ↓ to move Enter to confirm',
|
|
117
|
+
change_api_key: 'Change API key',
|
|
118
|
+
change_provider: 'Change provider',
|
|
119
|
+
show_current_config: 'Show current config',
|
|
120
|
+
cancel: 'Cancel',
|
|
121
|
+
},
|
|
122
|
+
vi: {
|
|
123
|
+
tips_header: 'Mẹo để bắt đầu:',
|
|
124
|
+
tips_1: '1. Đặt câu hỏi, chỉnh sửa file hoặc chạy lệnh.',
|
|
125
|
+
tips_2: '2. Mô tả càng cụ thể, kết quả càng tốt.',
|
|
126
|
+
tips_3: '3. Gõ /help để xem thêm thông tin.',
|
|
127
|
+
apikey_instruction: 'Nhập API key của bạn bên dưới và sử dụng những gì bạn đã mua.',
|
|
128
|
+
apikey_supports: 'Hỗ trợ: Anthropic · Gemini · OpenAI · OpenRouter · DeepSeek',
|
|
129
|
+
apikey_auto: 'Key sẽ được tự động nhận diện. Không cần cài đặt thủ công.',
|
|
130
|
+
apikey_prompt: 'API Key ❯ ',
|
|
131
|
+
detected: '✓ Đã nhận diện: ',
|
|
132
|
+
not_detected: '⚠ Không nhận diện được. Vui lòng chọn thủ công:',
|
|
133
|
+
select_provider: 'Chọn nhà cung cấp AI:',
|
|
134
|
+
select_theme: 'Chọn giao diện:',
|
|
135
|
+
select_lang: 'Chọn ngôn ngữ:',
|
|
136
|
+
select_model: 'Chọn model:',
|
|
137
|
+
theme_changed: '✓ Đã đổi giao diện: ',
|
|
138
|
+
lang_changed: '✓ Đã đổi ngôn ngữ: ',
|
|
139
|
+
model_changed: '✓ Đã chọn model: ',
|
|
140
|
+
copied: '✓ Đã sao chép vào clipboard.',
|
|
141
|
+
nothing_to_copy: '⚠ Chưa có phản hồi nào để sao chép.',
|
|
142
|
+
screen_cleared: '',
|
|
143
|
+
logged_out: "Đã đăng xuất. Chạy 'corex' để thiết lập lại.",
|
|
144
|
+
invalid_key: '✗ API key không hợp lệ. Chạy /config để cập nhật.',
|
|
145
|
+
network_error: '✗ Lỗi mạng. Kiểm tra kết nối internet.',
|
|
146
|
+
file_not_found: '✗ Không tìm thấy file: ',
|
|
147
|
+
file_read_error: '✗ Lỗi đọc file: ',
|
|
148
|
+
no_key_provided: '✗ Chưa nhập API key.',
|
|
149
|
+
config_updated: '✓ Đã cập nhật cấu hình.',
|
|
150
|
+
provider_changed: '✓ Đã đổi nhà cung cấp: ',
|
|
151
|
+
help_title: 'Các lệnh:',
|
|
152
|
+
help_model: '/model Đổi model AI (phím mũi tên + Enter)',
|
|
153
|
+
help_theme: '/theme Đổi giao diện màu sắc',
|
|
154
|
+
help_lang: '/lang Đổi ngôn ngữ giao diện',
|
|
155
|
+
help_config: '/config Đổi nhà cung cấp hoặc API key',
|
|
156
|
+
help_help: '/help Hiển thị danh sách lệnh',
|
|
157
|
+
help_ctrlc: 'Ctrl+C Thoát',
|
|
158
|
+
help_file_title: 'Tải file lên:',
|
|
159
|
+
help_file_usage: '@tênfile Đính kèm file vào tin nhắn',
|
|
160
|
+
help_file_example: 'Ví dụ: @notes.txt tóm tắt file này',
|
|
161
|
+
help_outside_title: 'Ngoài chat:',
|
|
162
|
+
help_logout: 'corex logout Xóa toàn bộ dữ liệu đã lưu',
|
|
163
|
+
navigate_hint: '↑ ↓ di chuyển Enter xác nhận',
|
|
164
|
+
change_api_key: 'Đổi API key',
|
|
165
|
+
change_provider: 'Đổi nhà cung cấp',
|
|
166
|
+
show_current_config: 'Xem cấu hình hiện tại',
|
|
167
|
+
cancel: 'Hủy',
|
|
168
|
+
},
|
|
169
|
+
ja: {
|
|
170
|
+
tips_header: '始め方のヒント:',
|
|
171
|
+
tips_1: '1. 質問する、ファイルを編集する、コマンドを実行する。',
|
|
172
|
+
tips_2: '2. 具体的に伝えると良い結果が得られます。',
|
|
173
|
+
tips_3: '3. /help で詳細情報を表示。',
|
|
174
|
+
apikey_instruction: '以下にAPIキーを入力して使い始めましょう。',
|
|
175
|
+
apikey_supports: '対応: Anthropic · Gemini · OpenAI · OpenRouter · DeepSeek',
|
|
176
|
+
apikey_auto: 'キーは自動検出されます。手動設定は不要です。',
|
|
177
|
+
apikey_prompt: 'APIキー ❯ ',
|
|
178
|
+
detected: '✓ 検出: ',
|
|
179
|
+
not_detected: '⚠ 自動検出できません。手動で選択してください:',
|
|
180
|
+
select_provider: 'AIプロバイダーを選択:',
|
|
181
|
+
select_theme: 'テーマを選択:',
|
|
182
|
+
select_lang: '言語を選択:',
|
|
183
|
+
select_model: 'モデルを選択:',
|
|
184
|
+
theme_changed: '✓ テーマ変更: ',
|
|
185
|
+
lang_changed: '✓ 言語変更: ',
|
|
186
|
+
model_changed: '✓ モデル設定: ',
|
|
187
|
+
copied: '✓ クリップボードにコピーしました。',
|
|
188
|
+
nothing_to_copy: '⚠ コピーする応答がありません。',
|
|
189
|
+
screen_cleared: '',
|
|
190
|
+
logged_out: "ログアウトしました。'corex' で再設定できます。",
|
|
191
|
+
invalid_key: '✗ 無効なAPIキーです。/config で更新してください。',
|
|
192
|
+
network_error: '✗ ネットワークエラー。接続を確認してください。',
|
|
193
|
+
file_not_found: '✗ ファイルが見つかりません: ',
|
|
194
|
+
file_read_error: '✗ ファイル読み込みエラー: ',
|
|
195
|
+
no_key_provided: '✗ APIキーが入力されていません。',
|
|
196
|
+
config_updated: '✓ 設定を更新しました。',
|
|
197
|
+
provider_changed: '✓ プロバイダーを変更: ',
|
|
198
|
+
help_title: 'コマンド:',
|
|
199
|
+
help_model: '/model AIモデルを切り替え (矢印キー + Enter)',
|
|
200
|
+
help_theme: '/theme カラーテーマを変更',
|
|
201
|
+
help_lang: '/lang インターフェース言語を変更',
|
|
202
|
+
help_config: '/config プロバイダーまたはAPIキーを変更',
|
|
203
|
+
help_help: '/help このメッセージを表示',
|
|
204
|
+
help_ctrlc: 'Ctrl+C 終了',
|
|
205
|
+
help_file_title: 'ファイルアップロード:',
|
|
206
|
+
help_file_usage: '@ファイル名 メッセージにファイルを添付',
|
|
207
|
+
help_file_example: '例: @notes.txt このファイルを要約して',
|
|
208
|
+
help_outside_title: 'チャット外:',
|
|
209
|
+
help_logout: 'corex logout 保存データをすべてリセット',
|
|
210
|
+
navigate_hint: '↑ ↓ で移動 Enter で確定',
|
|
211
|
+
change_api_key: 'API キーを変更',
|
|
212
|
+
change_provider: 'プロバイダーを変更',
|
|
213
|
+
show_current_config: '現在の設定を表示',
|
|
214
|
+
cancel: 'キャンセル',
|
|
215
|
+
},
|
|
216
|
+
ko: {
|
|
217
|
+
tips_header: '시작 팁:', tips_1: '1. 질문하기, 파일 편집, 명령 실행.', tips_2: '2. 구체적일수록 더 좋은 결과를 얻습니다.', tips_3: '3. /help 로 자세한 정보 확인.',
|
|
218
|
+
apikey_instruction: '아래에 API 키를 입력하여 시작하세요.', apikey_supports: '지원: Anthropic · Gemini · OpenAI · OpenRouter · DeepSeek', apikey_auto: '키는 자동으로 감지됩니다. 수동 설정 불필요.', apikey_prompt: 'API 키 ❯ ',
|
|
219
|
+
detected: '✓ 감지됨: ', not_detected: '⚠ 감지 실패. 수동으로 선택하세요:', select_provider: 'AI 공급자 선택:', select_theme: '테마 선택:', select_lang: '언어 선택:', select_model: '모델 선택:',
|
|
220
|
+
theme_changed: '✓ 테마 변경: ', lang_changed: '✓ 언어 변경: ', model_changed: '✓ 모델 설정: ', copied: '✓ 클립보드에 복사되었습니다.', nothing_to_copy: '⚠ 복사할 응답이 없습니다.', screen_cleared: '',
|
|
221
|
+
logged_out: "로그아웃되었습니다. 'corex' 로 재설정하세요.", invalid_key: '✗ 잘못된 API 키입니다. /config 로 업데이트하세요.', network_error: '✗ 네트워크 오류. 연결을 확인하세요.', file_not_found: '✗ 파일을 찾을 수 없습니다: ', file_read_error: '✗ 파일 읽기 오류: ', no_key_provided: '✗ API 키가 입력되지 않았습니다.',
|
|
222
|
+
config_updated: '✓ 설정이 업데이트되었습니다.', provider_changed: '✓ 공급자 변경: ',
|
|
223
|
+
help_title: '명령어:', help_model: '/model AI 모델 전환 (화살표 + Enter)', help_theme: '/theme 색상 테마 변경', help_lang: '/lang 인터페이스 언어 변경', help_config: '/config 공급자 또는 API 키 변경', help_help: '/help 이 메시지 표시', help_ctrlc: 'Ctrl+C 종료',
|
|
224
|
+
help_file_title: '파일 업로드:', help_file_usage: '@파일명 메시지에 파일 첨부', help_file_example: '예시: @notes.txt 이 파일 요약해줘', help_outside_title: '채팅 외부:', help_logout: 'corex logout 저장된 모든 데이터 초기화',
|
|
225
|
+
navigate_hint: '↑ ↓ 이동 Enter 확인', change_api_key: 'API 키 변경', change_provider: '공급자 변경', show_current_config: '현재 설정 보기', cancel: '취소',
|
|
226
|
+
},
|
|
227
|
+
zh: {
|
|
228
|
+
tips_header: '入门提示:', tips_1: '1. 提问、编辑文件或运行命令。', tips_2: '2. 越具体,结果越好。', tips_3: '3. 输入 /help 查看更多信息。',
|
|
229
|
+
apikey_instruction: '在下方输入您的 API 密钥即可开始使用。', apikey_supports: '支持:Anthropic · Gemini · OpenAI · OpenRouter · DeepSeek', apikey_auto: '密钥将自动识别,无需手动配置。', apikey_prompt: 'API 密钥 ❯ ',
|
|
230
|
+
detected: '✓ 已识别:', not_detected: '⚠ 无法自动识别,请手动选择:', select_provider: '选择 AI 提供商:', select_theme: '选择主题:', select_lang: '选择语言:', select_model: '选择模型:',
|
|
231
|
+
theme_changed: '✓ 主题已更改为:', lang_changed: '✓ 语言已更改为:', model_changed: '✓ 模型已设置为:', copied: '✓ 已复制到剪贴板。', nothing_to_copy: '⚠ 暂无可复制的回复。', screen_cleared: '',
|
|
232
|
+
logged_out: "已退出登录。运行 'corex' 重新设置。", invalid_key: '✗ API 密钥无效,请运行 /config 更新。', network_error: '✗ 网络错误,请检查连接。', file_not_found: '✗ 找不到文件:', file_read_error: '✗ 文件读取错误:', no_key_provided: '✗ 未输入 API 密钥。',
|
|
233
|
+
config_updated: '✓ 配置已更新。', provider_changed: '✓ 提供商已更改为 ',
|
|
234
|
+
help_title: '命令:', help_model: '/model 切换 AI 模型(方向键 + Enter)', help_theme: '/theme 更改颜色主题', help_lang: '/lang 更改界面语言', help_config: '/config 更改提供商或 API 密钥', help_help: '/help 显示此帮助信息', help_ctrlc: 'Ctrl+C 退出',
|
|
235
|
+
help_file_title: '文件上传:', help_file_usage: '@文件名 将文件附加到消息', help_file_example: '示例: @notes.txt 总结这个文件', help_outside_title: '聊天外:', help_logout: 'corex logout 重置所有保存的数据',
|
|
236
|
+
navigate_hint: '↑ ↓ 移动 Enter 确认', change_api_key: '更改 API 密钥', change_provider: '更改提供商', show_current_config: '查看当前配置', cancel: '取消',
|
|
237
|
+
},
|
|
238
|
+
fr: {
|
|
239
|
+
tips_header: 'Conseils pour commencer :', tips_1: '1. Posez des questions, modifiez des fichiers ou exécutez des commandes.', tips_2: '2. Soyez précis pour de meilleurs résultats.', tips_3: "3. /help pour plus d'informations.",
|
|
240
|
+
apikey_instruction: 'Entrez votre clé API ci-dessous pour commencer.', apikey_supports: 'Compatible : Anthropic · Gemini · OpenAI · OpenRouter · DeepSeek', apikey_auto: 'La clé sera détectée automatiquement. Aucune configuration manuelle.', apikey_prompt: 'Clé API ❯ ',
|
|
241
|
+
detected: '✓ Détecté : ', not_detected: '⚠ Détection impossible. Sélectionnez manuellement :', select_provider: 'Choisir le fournisseur IA :', select_theme: 'Choisir le thème :', select_lang: 'Choisir la langue :', select_model: 'Choisir le modèle :',
|
|
242
|
+
theme_changed: '✓ Thème changé : ', lang_changed: '✓ Langue changée : ', model_changed: '✓ Modèle défini : ', copied: '✓ Copié dans le presse-papiers.', nothing_to_copy: '⚠ Aucune réponse à copier.', screen_cleared: '',
|
|
243
|
+
logged_out: "Déconnecté. Lancez 'corex' pour reconfigurer.", invalid_key: '✗ Clé API invalide. Lancez /config pour mettre à jour.', network_error: '✗ Erreur réseau. Vérifiez votre connexion.', file_not_found: '✗ Fichier introuvable : ', file_read_error: '✗ Erreur de lecture : ', no_key_provided: '✗ Aucune clé API fournie.',
|
|
244
|
+
config_updated: '✓ Configuration mise à jour.', provider_changed: '✓ Fournisseur changé : ',
|
|
245
|
+
help_title: 'Commandes :', help_model: '/model Changer de modèle IA (flèches + Entrée)', help_theme: '/theme Changer le thème de couleur', help_lang: "/lang Changer la langue de l'interface", help_config: '/config Changer fournisseur ou clé API', help_help: '/help Afficher ce message', help_ctrlc: 'Ctrl+C Quitter',
|
|
246
|
+
help_file_title: 'Envoi de fichier :', help_file_usage: '@nomfichier Joindre un fichier au message', help_file_example: 'Exemple : @notes.txt résume ce fichier', help_outside_title: 'Hors chat :', help_logout: 'corex logout Réinitialiser toutes les données',
|
|
247
|
+
navigate_hint: '↑ ↓ naviguer Entrée confirmer', change_api_key: 'Changer la clé API', change_provider: 'Changer le fournisseur', show_current_config: 'Afficher la config actuelle', cancel: 'Annuler',
|
|
248
|
+
},
|
|
249
|
+
es: {
|
|
250
|
+
tips_header: 'Consejos para empezar:', tips_1: '1. Haz preguntas, edita archivos o ejecuta comandos.', tips_2: '2. Sé específico para mejores resultados.', tips_3: '3. /help para más información.',
|
|
251
|
+
apikey_instruction: 'Introduce tu clave API abajo para comenzar.', apikey_supports: 'Compatible: Anthropic · Gemini · OpenAI · OpenRouter · DeepSeek', apikey_auto: 'La clave se detectará automáticamente. Sin configuración manual.', apikey_prompt: 'Clave API ❯ ',
|
|
252
|
+
detected: '✓ Detectado: ', not_detected: '⚠ No se pudo detectar. Selecciona manualmente:', select_provider: 'Seleccionar proveedor IA:', select_theme: 'Seleccionar tema:', select_lang: 'Seleccionar idioma:', select_model: 'Seleccionar modelo:',
|
|
253
|
+
theme_changed: '✓ Tema cambiado a: ', lang_changed: '✓ Idioma cambiado a: ', model_changed: '✓ Modelo establecido: ', copied: '✓ Copiado al portapapeles.', nothing_to_copy: '⚠ No hay respuesta para copiar.', screen_cleared: '',
|
|
254
|
+
logged_out: "Sesión cerrada. Ejecuta 'corex' para configurar de nuevo.", invalid_key: '✗ Clave API inválida. Ejecuta /config para actualizar.', network_error: '✗ Error de red. Comprueba tu conexión.', file_not_found: '✗ Archivo no encontrado: ', file_read_error: '✗ Error al leer archivo: ', no_key_provided: '✗ No se proporcionó clave API.',
|
|
255
|
+
config_updated: '✓ Configuración actualizada.', provider_changed: '✓ Proveedor cambiado a ',
|
|
256
|
+
help_title: 'Comandos:', help_model: '/model Cambiar modelo IA (flechas + Enter)', help_theme: '/theme Cambiar tema de color', help_lang: '/lang Cambiar idioma de la interfaz', help_config: '/config Cambiar proveedor o clave API', help_help: '/help Mostrar este mensaje', help_ctrlc: 'Ctrl+C Salir',
|
|
257
|
+
help_file_title: 'Subir archivo:', help_file_usage: '@archivo Adjuntar archivo al mensaje', help_file_example: 'Ejemplo: @notes.txt resume este archivo', help_outside_title: 'Fuera del chat:', help_logout: 'corex logout Restablecer todos los datos',
|
|
258
|
+
navigate_hint: '↑ ↓ mover Enter confirmar', change_api_key: 'Cambiar clave API', change_provider: 'Cambiar proveedor', show_current_config: 'Mostrar config actual', cancel: 'Cancelar',
|
|
259
|
+
},
|
|
260
|
+
de: {
|
|
261
|
+
tips_header: 'Tipps zum Einstieg:', tips_1: '1. Fragen stellen, Dateien bearbeiten oder Befehle ausführen.', tips_2: '2. Je spezifischer, desto besser die Ergebnisse.', tips_3: '3. /help für weitere Informationen.',
|
|
262
|
+
apikey_instruction: 'Gib unten deinen API-Schlüssel ein, um zu beginnen.', apikey_supports: 'Unterstützt: Anthropic · Gemini · OpenAI · OpenRouter · DeepSeek', apikey_auto: 'Der Schlüssel wird automatisch erkannt. Keine manuelle Einrichtung.', apikey_prompt: 'API-Schlüssel ❯ ',
|
|
263
|
+
detected: '✓ Erkannt: ', not_detected: '⚠ Erkennung fehlgeschlagen. Bitte manuell auswählen:', select_provider: 'KI-Anbieter auswählen:', select_theme: 'Thema auswählen:', select_lang: 'Sprache auswählen:', select_model: 'Modell auswählen:',
|
|
264
|
+
theme_changed: '✓ Thema geändert zu: ', lang_changed: '✓ Sprache geändert zu: ', model_changed: '✓ Modell gesetzt: ', copied: '✓ In Zwischenablage kopiert.', nothing_to_copy: '⚠ Keine Antwort zum Kopieren.', screen_cleared: '',
|
|
265
|
+
logged_out: "Abgemeldet. Starte 'corex' zur Neueinrichtung.", invalid_key: '✗ Ungültiger API-Schlüssel. Führe /config aus.', network_error: '✗ Netzwerkfehler. Überprüfe deine Verbindung.', file_not_found: '✗ Datei nicht gefunden: ', file_read_error: '✗ Fehler beim Lesen: ', no_key_provided: '✗ Kein API-Schlüssel eingegeben.',
|
|
266
|
+
config_updated: '✓ Konfiguration aktualisiert.', provider_changed: '✓ Anbieter geändert zu ',
|
|
267
|
+
help_title: 'Befehle:', help_model: '/model KI-Modell wechseln (Pfeiltasten + Enter)', help_theme: '/theme Farbthema ändern', help_lang: '/lang Oberflächensprache ändern', help_config: '/config Anbieter oder API-Schlüssel ändern', help_help: '/help Diese Nachricht anzeigen', help_ctrlc: 'Ctrl+C Beenden',
|
|
268
|
+
help_file_title: 'Datei-Upload:', help_file_usage: '@dateiname Datei an Nachricht anhängen', help_file_example: 'Beispiel: @notes.txt fasse diese Datei zusammen', help_outside_title: 'Außerhalb des Chats:', help_logout: 'corex logout Alle Daten zurücksetzen',
|
|
269
|
+
navigate_hint: '↑ ↓ bewegen Enter bestätigen', change_api_key: 'API-Schlüssel ändern', change_provider: 'Anbieter ändern', show_current_config: 'Aktuelle Konfiguration', cancel: 'Abbrechen',
|
|
270
|
+
},
|
|
271
|
+
pt: {
|
|
272
|
+
tips_header: 'Dicas para começar:', tips_1: '1. Faça perguntas, edite arquivos ou execute comandos.', tips_2: '2. Seja específico para melhores resultados.', tips_3: '3. /help para mais informações.',
|
|
273
|
+
apikey_instruction: 'Insira sua chave API abaixo para começar.', apikey_supports: 'Suporta: Anthropic · Gemini · OpenAI · OpenRouter · DeepSeek', apikey_auto: 'A chave será detectada automaticamente. Sem configuração manual.', apikey_prompt: 'Chave API ❯ ',
|
|
274
|
+
detected: '✓ Detectado: ', not_detected: '⚠ Não foi possível detectar. Selecione manualmente:', select_provider: 'Selecionar provedor de IA:', select_theme: 'Selecionar tema:', select_lang: 'Selecionar idioma:', select_model: 'Selecionar modelo:',
|
|
275
|
+
theme_changed: '✓ Tema alterado para: ', lang_changed: '✓ Idioma alterado para: ', model_changed: '✓ Modelo definido: ', copied: '✓ Copiado para a área de transferência.', nothing_to_copy: '⚠ Nenhuma resposta para copiar.', screen_cleared: '',
|
|
276
|
+
logged_out: "Desconectado. Execute 'corex' para reconfigurar.", invalid_key: '✗ Chave API inválida. Execute /config para atualizar.', network_error: '✗ Erro de rede. Verifique sua conexão.', file_not_found: '✗ Arquivo não encontrado: ', file_read_error: '✗ Erro ao ler arquivo: ', no_key_provided: '✗ Nenhuma chave API fornecida.',
|
|
277
|
+
config_updated: '✓ Configuração atualizada.', provider_changed: '✓ Provedor alterado para ',
|
|
278
|
+
help_title: 'Comandos:', help_model: '/model Trocar modelo de IA (setas + Enter)', help_theme: '/theme Mudar tema de cor', help_lang: '/lang Mudar idioma da interface', help_config: '/config Mudar provedor ou chave API', help_help: '/help Mostrar esta mensagem', help_ctrlc: 'Ctrl+C Sair',
|
|
279
|
+
help_file_title: 'Upload de arquivo:', help_file_usage: '@arquivo Anexar arquivo à mensagem', help_file_example: 'Exemplo: @notes.txt resuma este arquivo', help_outside_title: 'Fora do chat:', help_logout: 'corex logout Redefinir todos os dados',
|
|
280
|
+
navigate_hint: '↑ ↓ mover Enter confirmar', change_api_key: 'Alterar chave API', change_provider: 'Alterar provedor', show_current_config: 'Mostrar config atual', cancel: 'Cancelar',
|
|
281
|
+
},
|
|
282
|
+
ru: {
|
|
283
|
+
tips_header: 'Советы по началу работы:', tips_1: '1. Задавайте вопросы, редактируйте файлы или выполняйте команды.', tips_2: '2. Чем конкретнее запрос, тем лучше результат.', tips_3: '3. /help — дополнительная информация.',
|
|
284
|
+
apikey_instruction: 'Введите ваш API-ключ ниже, чтобы начать.', apikey_supports: 'Поддерживает: Anthropic · Gemini · OpenAI · OpenRouter · DeepSeek', apikey_auto: 'Ключ будет определён автоматически. Ручная настройка не нужна.', apikey_prompt: 'API-ключ ❯ ',
|
|
285
|
+
detected: '✓ Определено: ', not_detected: '⚠ Не удалось определить. Выберите вручную:', select_provider: 'Выбрать ИИ-провайдера:', select_theme: 'Выбрать тему:', select_lang: 'Выбрать язык:', select_model: 'Выбрать модель:',
|
|
286
|
+
theme_changed: '✓ Тема изменена: ', lang_changed: '✓ Язык изменён: ', model_changed: '✓ Модель установлена: ', copied: '✓ Скопировано в буфер обмена.', nothing_to_copy: '⚠ Нет ответа для копирования.', screen_cleared: '',
|
|
287
|
+
logged_out: "Выход выполнен. Запустите 'corex' для повторной настройки.", invalid_key: '✗ Неверный API-ключ. Запустите /config для обновления.', network_error: '✗ Ошибка сети. Проверьте подключение.', file_not_found: '✗ Файл не найден: ', file_read_error: '✗ Ошибка чтения файла: ', no_key_provided: '✗ API-ключ не введён.',
|
|
288
|
+
config_updated: '✓ Конфигурация обновлена.', provider_changed: '✓ Провайдер изменён: ',
|
|
289
|
+
help_title: 'Команды:', help_model: '/model Сменить модель ИИ (стрелки + Enter)', help_theme: '/theme Изменить цветовую тему', help_lang: '/lang Изменить язык интерфейса', help_config: '/config Изменить провайдера или API-ключ', help_help: '/help Показать это сообщение', help_ctrlc: 'Ctrl+C Выход',
|
|
290
|
+
help_file_title: 'Загрузка файла:', help_file_usage: '@имяфайла Прикрепить файл к сообщению', help_file_example: 'Пример: @notes.txt кратко изложи этот файл', help_outside_title: 'Вне чата:', help_logout: 'corex logout Сбросить все данные',
|
|
291
|
+
navigate_hint: '↑ ↓ перемещение Enter подтвердить', change_api_key: 'Изменить API-ключ', change_provider: 'Изменить провайдера', show_current_config: 'Показать текущую конфигурацию', cancel: 'Отмена',
|
|
292
|
+
},
|
|
293
|
+
};
|
|
294
|
+
|
|
295
|
+
let currentLang = loadConfig()?.lang || 'en';
|
|
296
|
+
|
|
297
|
+
function t(key) {
|
|
298
|
+
return (STRINGS[currentLang] && STRINGS[currentLang][key]) || STRINGS.en[key] || key;
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
const LANG_LIST = [
|
|
302
|
+
{ id: 'en', label: '🇺🇸 English' },
|
|
303
|
+
{ id: 'vi', label: '🇻🇳 Tiếng Việt' },
|
|
304
|
+
{ id: 'ja', label: '🇯🇵 日本語' },
|
|
305
|
+
{ id: 'ko', label: '🇰🇷 한국어' },
|
|
306
|
+
{ id: 'zh', label: '🇨🇳 中文' },
|
|
307
|
+
{ id: 'fr', label: '🇫🇷 Français' },
|
|
308
|
+
{ id: 'es', label: '🇪🇸 Español' },
|
|
309
|
+
{ id: 'de', label: '🇩🇪 Deutsch' },
|
|
310
|
+
{ id: 'pt', label: '🇧🇷 Português' },
|
|
311
|
+
{ id: 'ru', label: '🇷🇺 Русский' },
|
|
312
|
+
];
|
|
313
|
+
// ═══════════════════════════════════════════════════════════════════
|
|
314
|
+
// THEMES (7 themes including Spectrum)
|
|
315
|
+
// ═══════════════════════════════════════════════════════════════════
|
|
316
|
+
|
|
317
|
+
const RESET = '\x1b[0m';
|
|
318
|
+
const DIM = '\x1b[90m';
|
|
319
|
+
|
|
320
|
+
const SPECTRUM = [
|
|
321
|
+
'\x1b[96m', '\x1b[94m', '\x1b[95m', '\x1b[35m',
|
|
322
|
+
'\x1b[91m', '\x1b[93m', '\x1b[92m',
|
|
323
|
+
];
|
|
324
|
+
|
|
325
|
+
const THEMES = {
|
|
326
|
+
default: { ansi: '\x1b[37m', name: 'Default', desc: 'ANSI 37', descDetail: 'white/gray', swatch: '\x1b[37m■\x1b[0m' },
|
|
327
|
+
green: { ansi: '\x1b[92m', name: 'Green', desc: 'ANSI 92', descDetail: 'bright green', swatch: '\x1b[92m■\x1b[0m' },
|
|
328
|
+
red: { ansi: '\x1b[91m', name: 'Red', desc: 'ANSI 91', descDetail: 'bright red', swatch: '\x1b[91m■\x1b[0m' },
|
|
329
|
+
purple: { ansi: '\x1b[95m', name: 'Purple', desc: 'ANSI 95', descDetail: 'bright purple', swatch: '\x1b[95m■\x1b[0m' },
|
|
330
|
+
yellow: { ansi: '\x1b[93m', name: 'Yellow', desc: 'ANSI 93', descDetail: 'bright yellow', swatch: '\x1b[93m■\x1b[0m' },
|
|
331
|
+
blue: { ansi: '\x1b[94m', name: 'Blue', desc: 'ANSI 94', descDetail: 'bright blue', swatch: '\x1b[94m■\x1b[0m' },
|
|
332
|
+
spectrum: { ansi: '\x1b[96m', name: 'Spectrum', desc: '', descDetail: 'rainbow gradient', swatch: '\x1b[96m■\x1b[95m■\x1b[91m■\x1b[93m■\x1b[92m■\x1b[0m' },
|
|
333
|
+
};
|
|
334
|
+
const THEME_KEYS = Object.keys(THEMES);
|
|
335
|
+
|
|
336
|
+
function getThemeAnsi(themeName) {
|
|
337
|
+
return (THEMES[themeName] || THEMES.default).ansi;
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
function getPromptColor(themeName) {
|
|
341
|
+
if (themeName === 'spectrum') return '\x1b[96m';
|
|
342
|
+
return getThemeAnsi(themeName);
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
function getResponseColor(themeName) {
|
|
346
|
+
if (themeName === 'spectrum') return '\x1b[0m';
|
|
347
|
+
return getThemeAnsi(themeName);
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
function getSystemColor(themeName) {
|
|
351
|
+
if (themeName === 'spectrum') return '\x1b[94m';
|
|
352
|
+
return getThemeAnsi(themeName);
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
function renderSpectrum(text) {
|
|
356
|
+
let colorIndex = 0;
|
|
357
|
+
let result = '';
|
|
358
|
+
for (let i = 0; i < text.length; i++) {
|
|
359
|
+
const ch = text[i];
|
|
360
|
+
if (ch === '\n') {
|
|
361
|
+
result += '\n';
|
|
362
|
+
} else if (ch === ' ') {
|
|
363
|
+
result += ' ';
|
|
364
|
+
} else {
|
|
365
|
+
result += SPECTRUM[colorIndex % SPECTRUM.length] + ch + RESET;
|
|
366
|
+
colorIndex++;
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
return result;
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
function renderSpectrumSeparator(sep) {
|
|
373
|
+
let colorIndex = 0;
|
|
374
|
+
let result = '';
|
|
375
|
+
for (let i = 0; i < sep.length; i++) {
|
|
376
|
+
const ch = sep[i];
|
|
377
|
+
if (ch === ' ' || ch === '\n') {
|
|
378
|
+
result += ch;
|
|
379
|
+
} else {
|
|
380
|
+
result += SPECTRUM[colorIndex % SPECTRUM.length] + ch + RESET;
|
|
381
|
+
colorIndex++;
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
return result;
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
// ═══════════════════════════════════════════════════════════════════
|
|
388
|
+
// LOGO
|
|
389
|
+
// ═══════════════════════════════════════════════════════════════════
|
|
390
|
+
|
|
391
|
+
const LOGO_TEXT = `
|
|
392
|
+
██████╗ ██████╗ ██████╗ ███████╗██╗ ██╗ ██████╗██╗ ██╗
|
|
393
|
+
██╔════╝██╔═══██╗██╔══██╗██╔════╝╚██╗██╔╝ ██╔════╝██║ ██║
|
|
394
|
+
██║ ██║ ██║██████╔╝█████╗ ╚███╔╝ ██║ ██║ ██║
|
|
395
|
+
██║ ██║ ██║██╔══██╗██╔══╝ ██╔██╗ ██║ ██║ ██║
|
|
396
|
+
╚██████╗╚██████╔╝██║ ██║███████╗██╔╝ ██╗ ╚██████╗███████╗██║
|
|
397
|
+
╚═════╝ ╚═════╝ ╚═╝ ╚═╝╚══════╝╚═╝ ╚═╝ ╚═════╝╚══════╝╚═╝`;
|
|
398
|
+
|
|
399
|
+
const SEPARATOR = '──────────────────────────────────────────────────────────────────';
|
|
400
|
+
|
|
401
|
+
function printLogo(themeName) {
|
|
402
|
+
const tn = themeName || 'default';
|
|
403
|
+
if (tn === 'spectrum') {
|
|
404
|
+
process.stdout.write(renderSpectrum(LOGO_TEXT) + '\n');
|
|
405
|
+
process.stdout.write(renderSpectrumSeparator(SEPARATOR) + '\n');
|
|
406
|
+
} else {
|
|
407
|
+
const color = getThemeAnsi(tn);
|
|
408
|
+
process.stdout.write(color + LOGO_TEXT + RESET + '\n');
|
|
409
|
+
process.stdout.write(color + SEPARATOR + RESET + '\n');
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
// ═══════════════════════════════════════════════════════════════════
|
|
414
|
+
// PROVIDER DETECTION
|
|
415
|
+
// ═══════════════════════════════════════════════════════════════════
|
|
416
|
+
|
|
417
|
+
function detectProvider(key) {
|
|
418
|
+
const k = key.trim();
|
|
419
|
+
if (k.startsWith('sk-ant-')) return 'anthropic';
|
|
420
|
+
if (k.startsWith('AIza')) return 'gemini';
|
|
421
|
+
if (k.startsWith('sk-or-v1-') || k.startsWith('sk-or-')) return 'openrouter';
|
|
422
|
+
if (k.startsWith('sk-proj-')) return 'openai';
|
|
423
|
+
if (k.startsWith('sk-') && !k.startsWith('sk-or-')) return 'openai';
|
|
424
|
+
if (k.startsWith('ds-') || k.toLowerCase().includes('deepseek')) return 'deepseek';
|
|
425
|
+
return null;
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
const PROVIDER_NAMES = {
|
|
429
|
+
anthropic: 'Anthropic (Claude)', gemini: 'Google Gemini',
|
|
430
|
+
openrouter: 'OpenRouter', openai: 'OpenAI', deepseek: 'DeepSeek',
|
|
431
|
+
};
|
|
432
|
+
|
|
433
|
+
const PROVIDERS_LIST = [
|
|
434
|
+
{ id: 'anthropic', label: 'Anthropic (Claude)' },
|
|
435
|
+
{ id: 'gemini', label: 'Google Gemini' },
|
|
436
|
+
{ id: 'openai', label: 'OpenAI (GPT)' },
|
|
437
|
+
{ id: 'openrouter', label: 'OpenRouter' },
|
|
438
|
+
{ id: 'deepseek', label: 'DeepSeek' },
|
|
439
|
+
];
|
|
440
|
+
|
|
441
|
+
// ═══════════════════════════════════════════════════════════════════
|
|
442
|
+
// STDIN CLEANUP
|
|
443
|
+
// ═══════════════════════════════════════════════════════════════════
|
|
444
|
+
|
|
445
|
+
function cleanupStdin() {
|
|
446
|
+
try { if (process.stdin.isTTY && process.stdin.isRaw) process.stdin.setRawMode(false); } catch (e) { }
|
|
447
|
+
process.stdin.removeAllListeners('data');
|
|
448
|
+
process.stdin.removeAllListeners('keypress');
|
|
449
|
+
process.stdin.pause();
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
// ═══════════════════════════════════════════════════════════════════
|
|
453
|
+
// MASKED API KEY INPUT
|
|
454
|
+
// ═══════════════════════════════════════════════════════════════════
|
|
455
|
+
|
|
456
|
+
function askApiKey() {
|
|
457
|
+
return new Promise((resolve) => {
|
|
458
|
+
const prompt = ' ' + t('apikey_prompt');
|
|
459
|
+
process.stdout.write('\n' + prompt);
|
|
460
|
+
if (process.stdin.isTTY) process.stdin.setRawMode(true);
|
|
461
|
+
process.stdin.resume();
|
|
462
|
+
let key = '';
|
|
463
|
+
const onData = (chunk) => {
|
|
464
|
+
const chars = chunk.toString();
|
|
465
|
+
for (let i = 0; i < chars.length; i++) {
|
|
466
|
+
const ch = chars[i];
|
|
467
|
+
if (ch === '\r' || ch === '\n') {
|
|
468
|
+
process.stdout.write('\n');
|
|
469
|
+
if (process.stdin.isTTY) process.stdin.setRawMode(false);
|
|
470
|
+
process.stdin.removeListener('data', onData);
|
|
471
|
+
process.stdin.pause();
|
|
472
|
+
resolve(key.trim());
|
|
473
|
+
return;
|
|
474
|
+
} else if (ch === '\x03') {
|
|
475
|
+
process.stdout.write('\n');
|
|
476
|
+
if (process.stdin.isTTY) process.stdin.setRawMode(false);
|
|
477
|
+
process.exit(0);
|
|
478
|
+
} else if (ch === '\x7f' || ch === '\b') {
|
|
479
|
+
if (key.length > 0) {
|
|
480
|
+
key = key.slice(0, -1);
|
|
481
|
+
process.stdout.clearLine(0);
|
|
482
|
+
process.stdout.cursorTo(0);
|
|
483
|
+
process.stdout.write(prompt + '•'.repeat(key.length));
|
|
484
|
+
}
|
|
485
|
+
} else {
|
|
486
|
+
key += ch;
|
|
487
|
+
process.stdout.write('•');
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
};
|
|
491
|
+
process.stdin.on('data', onData);
|
|
492
|
+
});
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
// ═══════════════════════════════════════════════════════════════════
|
|
496
|
+
// ARROW KEY MENU
|
|
497
|
+
// ═══════════════════════════════════════════════════════════════════
|
|
498
|
+
|
|
499
|
+
function showArrowMenu(title, items) {
|
|
500
|
+
return new Promise((resolve) => {
|
|
501
|
+
let selectedIdx = 0;
|
|
502
|
+
const hintText = ' ' + t('navigate_hint');
|
|
503
|
+
const printMenu = () => {
|
|
504
|
+
process.stdout.write('\x1b[?25l');
|
|
505
|
+
for (let i = 0; i < items.length; i++) {
|
|
506
|
+
const prefix = i === selectedIdx ? ' ❯ ' : ' ';
|
|
507
|
+
process.stdout.write(prefix + items[i].label + '\n');
|
|
508
|
+
}
|
|
509
|
+
process.stdout.write('\n' + hintText + '\n');
|
|
510
|
+
};
|
|
511
|
+
const lineCount = items.length + 2;
|
|
512
|
+
const clearMenu = () => {
|
|
513
|
+
for (let i = 0; i < lineCount; i++) {
|
|
514
|
+
process.stdout.write('\x1b[A');
|
|
515
|
+
process.stdout.clearLine(0);
|
|
516
|
+
}
|
|
517
|
+
};
|
|
518
|
+
if (title) process.stdout.write(title + '\n\n');
|
|
519
|
+
printMenu();
|
|
520
|
+
if (process.stdin.isTTY) process.stdin.setRawMode(true);
|
|
521
|
+
process.stdin.resume();
|
|
522
|
+
const onData = (chunk) => {
|
|
523
|
+
const key = chunk.toString();
|
|
524
|
+
if (key === '\x1b[A' || key === '\x1b[D') {
|
|
525
|
+
if (selectedIdx > 0) { selectedIdx--; clearMenu(); printMenu(); }
|
|
526
|
+
} else if (key === '\x1b[B' || key === '\x1b[C') {
|
|
527
|
+
if (selectedIdx < items.length - 1) { selectedIdx++; clearMenu(); printMenu(); }
|
|
528
|
+
} else if (key === '\r' || key === '\n') {
|
|
529
|
+
process.stdin.removeListener('data', onData);
|
|
530
|
+
process.stdout.write('\x1b[?25h');
|
|
531
|
+
cleanupStdin();
|
|
532
|
+
resolve(items[selectedIdx]);
|
|
533
|
+
} else if (key === '\x03') {
|
|
534
|
+
process.stdout.write('\x1b[?25h\n');
|
|
535
|
+
cleanupStdin();
|
|
536
|
+
process.exit(0);
|
|
537
|
+
}
|
|
538
|
+
};
|
|
539
|
+
process.stdin.on('data', onData);
|
|
540
|
+
});
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
// ═══════════════════════════════════════════════════════════════════
|
|
544
|
+
// FIRST LAUNCH & TIPS
|
|
545
|
+
// ═══════════════════════════════════════════════════════════════════
|
|
546
|
+
|
|
547
|
+
function printApiKeyPrompt() {
|
|
548
|
+
process.stdout.write('\n');
|
|
549
|
+
process.stdout.write(' ' + t('apikey_instruction') + '\n');
|
|
550
|
+
process.stdout.write(' ' + t('apikey_supports') + '\n');
|
|
551
|
+
process.stdout.write(' ' + t('apikey_auto') + '\n');
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
function printTips(themeName) {
|
|
555
|
+
const color = getSystemColor(themeName || 'default');
|
|
556
|
+
process.stdout.write('\n');
|
|
557
|
+
process.stdout.write(color + ' ' + t('tips_header') + '\n');
|
|
558
|
+
process.stdout.write(' ' + t('tips_1') + '\n');
|
|
559
|
+
process.stdout.write(' ' + t('tips_2') + '\n');
|
|
560
|
+
process.stdout.write(' ' + t('tips_3') + RESET + '\n');
|
|
561
|
+
process.stdout.write('\n');
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
function buildHelpText(themeName) {
|
|
565
|
+
const c = getSystemColor(themeName || 'default');
|
|
566
|
+
return c + '\n' +
|
|
567
|
+
' ' + t('help_title') + '\n' +
|
|
568
|
+
' ' + t('help_model') + '\n' +
|
|
569
|
+
' ' + t('help_theme') + '\n' +
|
|
570
|
+
' ' + t('help_lang') + '\n' +
|
|
571
|
+
' ' + t('help_config') + '\n' +
|
|
572
|
+
' ' + t('help_help') + '\n' +
|
|
573
|
+
' ' + t('help_ctrlc') + '\n\n' +
|
|
574
|
+
' ' + t('help_file_title') + '\n' +
|
|
575
|
+
' ' + t('help_file_usage') + '\n' +
|
|
576
|
+
' ' + t('help_file_example') + '\n\n' +
|
|
577
|
+
' ' + t('help_outside_title') + '\n' +
|
|
578
|
+
' ' + t('help_logout') + '\n' + RESET;
|
|
579
|
+
}
|
|
580
|
+
// ═══════════════════════════════════════════════════════════════════
|
|
581
|
+
// FILE HANDLING
|
|
582
|
+
// ═══════════════════════════════════════════════════════════════════
|
|
583
|
+
|
|
584
|
+
const IMAGE_EXTENSIONS = ['.png', '.jpg', '.jpeg', '.gif', '.webp'];
|
|
585
|
+
|
|
586
|
+
async function resolveFilePath(filename) {
|
|
587
|
+
if (path.isAbsolute(filename)) return fs.existsSync(filename) ? filename : null;
|
|
588
|
+
if (filename.startsWith('~/')) {
|
|
589
|
+
const hp = path.join(os.homedir(), filename.slice(2));
|
|
590
|
+
return fs.existsSync(hp) ? hp : null;
|
|
591
|
+
}
|
|
592
|
+
const cwdPath = path.join(process.cwd(), filename);
|
|
593
|
+
if (fs.existsSync(cwdPath)) return cwdPath;
|
|
594
|
+
try {
|
|
595
|
+
const matches = await glob('**/' + filename, { cwd: os.homedir(), maxDepth: 8, absolute: true, ignore: ['**/node_modules/**', '**/.git/**'] });
|
|
596
|
+
if (matches && matches.length > 0) return matches[0];
|
|
597
|
+
} catch (e) { }
|
|
598
|
+
return null;
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
async function readFileForMessage(filePath) {
|
|
602
|
+
const ext = path.extname(filePath).toLowerCase();
|
|
603
|
+
if (IMAGE_EXTENSIONS.includes(ext)) {
|
|
604
|
+
return { type: 'image', data: fs.readFileSync(filePath).toString('base64'), ext };
|
|
605
|
+
}
|
|
606
|
+
if (ext === '.pdf') {
|
|
607
|
+
const pdf = require('pdf-parse');
|
|
608
|
+
const result = await pdf(fs.readFileSync(filePath));
|
|
609
|
+
return { type: 'text', data: result.text };
|
|
610
|
+
}
|
|
611
|
+
return { type: 'text', data: fs.readFileSync(filePath, 'utf-8') };
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
function getMimeType(ext) {
|
|
615
|
+
const m = { '.png': 'image/png', '.jpg': 'image/jpeg', '.jpeg': 'image/jpeg', '.gif': 'image/gif', '.webp': 'image/webp' };
|
|
616
|
+
return m[ext] || 'image/png';
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
// ═══════════════════════════════════════════════════════════════════
|
|
620
|
+
// AI PROVIDERS
|
|
621
|
+
// ═══════════════════════════════════════════════════════════════════
|
|
622
|
+
|
|
623
|
+
const PROVIDER_DEFAULTS = {
|
|
624
|
+
anthropic: { model: 'claude-sonnet-4-20250514' }, gemini: { model: 'gemini-1.5-pro' },
|
|
625
|
+
openai: { model: 'gpt-4o' }, openrouter: { model: 'openai/gpt-4o' }, deepseek: { model: 'deepseek-chat' },
|
|
626
|
+
};
|
|
627
|
+
|
|
628
|
+
function createAIClient(config) {
|
|
629
|
+
const { apiKey, provider } = config;
|
|
630
|
+
if (provider === 'anthropic') return { type: 'anthropic', client: new Anthropic({ apiKey }) };
|
|
631
|
+
if (provider === 'gemini') return { type: 'gemini', client: new GoogleGenerativeAI(apiKey) };
|
|
632
|
+
if (provider === 'openrouter') return { type: 'openai-compat', client: new OpenAI({ apiKey, baseURL: 'https://openrouter.ai/api/v1', defaultHeaders: { 'HTTP-Referer': 'https://github.com/corex-ai', 'X-Title': 'COREX CLI' } }) };
|
|
633
|
+
if (provider === 'deepseek') return { type: 'openai-compat', client: new OpenAI({ apiKey, baseURL: 'https://api.deepseek.com' }) };
|
|
634
|
+
return { type: 'openai-compat', client: new OpenAI({ apiKey }) };
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
async function streamAIResponse(aiClient, config, history, userMessage, imageContent, onToken) {
|
|
638
|
+
const model = config.model || PROVIDER_DEFAULTS[config.provider]?.model || 'gpt-4o';
|
|
639
|
+
const maxTokens = config.maxTokens || 4096;
|
|
640
|
+
const temperature = config.temperature ?? 0.7;
|
|
641
|
+
|
|
642
|
+
if (aiClient.type === 'anthropic') {
|
|
643
|
+
const msgContent = [{ type: 'text', text: userMessage }];
|
|
644
|
+
if (imageContent) msgContent.push({ type: 'image', source: { type: 'base64', media_type: getMimeType(imageContent.ext), data: imageContent.data } });
|
|
645
|
+
const stream = aiClient.client.messages.stream({ model, max_tokens: maxTokens, temperature, system: SYSTEM_PROMPT, messages: [...history.map(m => ({ role: m.role, content: m.content })), { role: 'user', content: msgContent }] });
|
|
646
|
+
let fullText = '';
|
|
647
|
+
stream.on('text', (text) => { fullText += text; onToken(text); });
|
|
648
|
+
await stream.finalMessage();
|
|
649
|
+
return fullText;
|
|
650
|
+
} else if (aiClient.type === 'gemini') {
|
|
651
|
+
const genModel = aiClient.client.getGenerativeModel({ model });
|
|
652
|
+
const chat = genModel.startChat({ history: history.map(m => ({ role: m.role === 'user' ? 'user' : 'model', parts: [{ text: m.content }] })), generationConfig: { maxOutputTokens: maxTokens, temperature } });
|
|
653
|
+
const parts = [{ text: userMessage }];
|
|
654
|
+
if (imageContent) parts.push({ inlineData: { mimeType: getMimeType(imageContent.ext), data: imageContent.data } });
|
|
655
|
+
const result = await chat.sendMessageStream(parts);
|
|
656
|
+
let fullText = '';
|
|
657
|
+
for await (const chunk of result.stream) { const ct = chunk.text(); fullText += ct; onToken(ct); }
|
|
658
|
+
return fullText;
|
|
659
|
+
} else {
|
|
660
|
+
const messages = [{ role: 'system', content: SYSTEM_PROMPT }, ...history.map(m => ({ role: m.role, content: m.content }))];
|
|
661
|
+
if (imageContent) { messages.push({ role: 'user', content: [{ type: 'text', text: userMessage }, { type: 'image_url', image_url: { url: `data:${getMimeType(imageContent.ext)};base64,${imageContent.data}` } }] }); }
|
|
662
|
+
else { messages.push({ role: 'user', content: userMessage }); }
|
|
663
|
+
const stream = await aiClient.client.chat.completions.create({ model, messages, stream: true, temperature, max_tokens: maxTokens });
|
|
664
|
+
let fullText = '';
|
|
665
|
+
for await (const chunk of stream) { const c = chunk.choices[0]?.delta?.content || ''; if (c) { fullText += c; onToken(c); } }
|
|
666
|
+
return fullText;
|
|
667
|
+
}
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
// ═══════════════════════════════════════════════════════════════════
|
|
671
|
+
// SPINNER
|
|
672
|
+
// ═══════════════════════════════════════════════════════════════════
|
|
673
|
+
|
|
674
|
+
const SPINNER_FRAMES = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'];
|
|
675
|
+
|
|
676
|
+
function startSpinner(themeName) {
|
|
677
|
+
let frameIdx = 0;
|
|
678
|
+
const color = getPromptColor(themeName || 'default');
|
|
679
|
+
const interval = setInterval(() => {
|
|
680
|
+
process.stdout.clearLine(0); process.stdout.cursorTo(0);
|
|
681
|
+
process.stdout.write(color + ' ' + SPINNER_FRAMES[frameIdx] + ' ...' + RESET);
|
|
682
|
+
frameIdx = (frameIdx + 1) % SPINNER_FRAMES.length;
|
|
683
|
+
}, 80);
|
|
684
|
+
return { stop() { clearInterval(interval); process.stdout.clearLine(0); process.stdout.cursorTo(0); } };
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
// ═══════════════════════════════════════════════════════════════════
|
|
688
|
+
// CHAT LOOP
|
|
689
|
+
// ═══════════════════════════════════════════════════════════════════
|
|
690
|
+
|
|
691
|
+
let chatHistory = [];
|
|
692
|
+
let lastResponse = '';
|
|
693
|
+
let currentRl = null;
|
|
694
|
+
|
|
695
|
+
function startChat(config) {
|
|
696
|
+
const aiClient = createAIClient(config);
|
|
697
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
698
|
+
currentRl = rl;
|
|
699
|
+
|
|
700
|
+
const prompt = () => {
|
|
701
|
+
const pColor = getPromptColor(config.theme || 'default');
|
|
702
|
+
rl.question(pColor + '> ' + RESET, async (input) => {
|
|
703
|
+
const trimmed = (input || '').trim();
|
|
704
|
+
if (!trimmed) { prompt(); return; }
|
|
705
|
+
const cmd = trimmed.toLowerCase();
|
|
706
|
+
|
|
707
|
+
if (cmd === '/help') { process.stdout.write(buildHelpText(config.theme) + '\n'); prompt(); return; }
|
|
708
|
+
if (cmd === '/clear') { printLogo(config.theme); prompt(); return; }
|
|
709
|
+
if (cmd === '/theme') { rl.close(); currentRl = null; await handleThemeCommand(config); return; }
|
|
710
|
+
if (cmd === '/lang') { rl.close(); currentRl = null; await handleLangCommand(config); return; }
|
|
711
|
+
if (cmd === '/config') { rl.close(); currentRl = null; await handleConfigCommand(config); return; }
|
|
712
|
+
|
|
713
|
+
// File attachment
|
|
714
|
+
let finalMessage = trimmed;
|
|
715
|
+
let imageContent = null;
|
|
716
|
+
if (trimmed.startsWith('@')) {
|
|
717
|
+
const spaceIdx = trimmed.indexOf(' ');
|
|
718
|
+
let filename, question;
|
|
719
|
+
if (spaceIdx === -1) { filename = trimmed.slice(1); question = 'Describe this file.'; }
|
|
720
|
+
else { filename = trimmed.slice(1, spaceIdx); question = trimmed.slice(spaceIdx + 1).trim(); }
|
|
721
|
+
const filePath = await resolveFilePath(filename);
|
|
722
|
+
if (!filePath) { process.stdout.write('\x1b[31m ' + t('file_not_found') + filename + RESET + '\n'); prompt(); return; }
|
|
723
|
+
try {
|
|
724
|
+
const fileResult = await readFileForMessage(filePath);
|
|
725
|
+
if (fileResult.type === 'image') { imageContent = { data: fileResult.data, ext: fileResult.ext }; finalMessage = question || 'What does this image show?'; }
|
|
726
|
+
else { finalMessage = 'File: ' + filename + '\n\n' + fileResult.data + '\n\n' + question; }
|
|
727
|
+
} catch (err) { process.stdout.write('\x1b[31m ' + t('file_read_error') + err.message + RESET + '\n'); prompt(); return; }
|
|
728
|
+
}
|
|
729
|
+
|
|
730
|
+
// Send to AI
|
|
731
|
+
chatHistory.push({ role: 'user', content: finalMessage });
|
|
732
|
+
const spinner = startSpinner(config.theme);
|
|
733
|
+
try {
|
|
734
|
+
let firstToken = true;
|
|
735
|
+
const rColor = getResponseColor(config.theme);
|
|
736
|
+
const fullText = await streamAIResponse(aiClient, config, chatHistory.slice(0, -1), finalMessage, imageContent, (token) => {
|
|
737
|
+
if (firstToken) { spinner.stop(); process.stdout.write('\n'); firstToken = false; }
|
|
738
|
+
process.stdout.write(rColor + token + RESET);
|
|
739
|
+
});
|
|
740
|
+
if (firstToken) { spinner.stop(); process.stdout.write('\n'); }
|
|
741
|
+
process.stdout.write('\n\n');
|
|
742
|
+
chatHistory.push({ role: 'assistant', content: fullText });
|
|
743
|
+
lastResponse = fullText;
|
|
744
|
+
} catch (err) {
|
|
745
|
+
spinner.stop();
|
|
746
|
+
const msg = err.message || '';
|
|
747
|
+
if (err.status === 401 || msg.toLowerCase().includes('api key') || msg.toLowerCase().includes('unauthorized')) { process.stdout.write('\x1b[31m ' + t('invalid_key') + RESET + '\n'); }
|
|
748
|
+
else if (msg.toLowerCase().includes('enotfound') || msg.toLowerCase().includes('network') || msg.toLowerCase().includes('fetch')) { process.stdout.write('\x1b[31m ' + t('network_error') + RESET + '\n'); }
|
|
749
|
+
else { process.stdout.write('\x1b[31m ✗ ' + msg + RESET + '\n'); }
|
|
750
|
+
}
|
|
751
|
+
prompt();
|
|
752
|
+
});
|
|
753
|
+
};
|
|
754
|
+
prompt();
|
|
755
|
+
}
|
|
756
|
+
|
|
757
|
+
// ═══════════════════════════════════════════════════════════════════
|
|
758
|
+
// THEME COMMAND
|
|
759
|
+
// ═══════════════════════════════════════════════════════════════════
|
|
760
|
+
|
|
761
|
+
async function handleThemeCommand(config) {
|
|
762
|
+
process.stdout.write('\n');
|
|
763
|
+
const activeTheme = config.theme || 'default';
|
|
764
|
+
const items = THEME_KEYS.map(key => {
|
|
765
|
+
const th = THEMES[key];
|
|
766
|
+
const active = key === activeTheme ? ' \x1b[90m(active)\x1b[0m' : '';
|
|
767
|
+
let descPart = '';
|
|
768
|
+
if (key === 'spectrum') {
|
|
769
|
+
descPart = ' \x1b[90m(rainbow gradient)\x1b[0m';
|
|
770
|
+
} else {
|
|
771
|
+
descPart = ' \x1b[90m(' + th.ansi + th.desc + '\x1b[90m — ' + th.descDetail + ')\x1b[0m';
|
|
772
|
+
}
|
|
773
|
+
return { id: key, label: th.swatch + ' ' + th.name.padEnd(10) + descPart + active };
|
|
774
|
+
});
|
|
775
|
+
const selected = await showArrowMenu(' ' + t('select_theme'), items);
|
|
776
|
+
process.stdout.write('\n');
|
|
777
|
+
config.theme = selected.id;
|
|
778
|
+
saveConfig({ theme: selected.id });
|
|
779
|
+
printLogo(selected.id);
|
|
780
|
+
process.stdout.write('\x1b[32m ' + t('theme_changed') + THEMES[selected.id].name + RESET + '\n\n');
|
|
781
|
+
startChat(config);
|
|
782
|
+
}
|
|
783
|
+
|
|
784
|
+
// ═══════════════════════════════════════════════════════════════════
|
|
785
|
+
// LANG COMMAND
|
|
786
|
+
// ═══════════════════════════════════════════════════════════════════
|
|
787
|
+
|
|
788
|
+
async function handleLangCommand(config) {
|
|
789
|
+
process.stdout.write('\n');
|
|
790
|
+
const selected = await showArrowMenu(' ' + t('select_lang'), LANG_LIST);
|
|
791
|
+
currentLang = selected.id;
|
|
792
|
+
config.lang = selected.id;
|
|
793
|
+
saveConfig({ lang: selected.id });
|
|
794
|
+
process.stdout.write('\n\x1b[32m ' + t('lang_changed') + selected.label + RESET + '\n\n');
|
|
795
|
+
startChat(config);
|
|
796
|
+
}
|
|
797
|
+
|
|
798
|
+
// ═══════════════════════════════════════════════════════════════════
|
|
799
|
+
// CONFIG COMMAND
|
|
800
|
+
// ═══════════════════════════════════════════════════════════════════
|
|
801
|
+
|
|
802
|
+
async function handleConfigCommand(config) {
|
|
803
|
+
process.stdout.write('\n');
|
|
804
|
+
const items = [
|
|
805
|
+
{ id: 'change_key', label: t('change_api_key') },
|
|
806
|
+
{ id: 'change_provider', label: t('change_provider') },
|
|
807
|
+
{ id: 'show_config', label: t('show_current_config') },
|
|
808
|
+
{ id: 'cancel', label: t('cancel') },
|
|
809
|
+
];
|
|
810
|
+
const selected = await showArrowMenu('', items);
|
|
811
|
+
|
|
812
|
+
if (selected.id === 'change_key') {
|
|
813
|
+
process.stdout.write('\n');
|
|
814
|
+
const newKey = await askApiKey();
|
|
815
|
+
if (newKey) {
|
|
816
|
+
let provider = detectProvider(newKey);
|
|
817
|
+
if (provider) { process.stdout.write('\x1b[32m ' + t('detected') + PROVIDER_NAMES[provider] + RESET + '\n'); }
|
|
818
|
+
else { process.stdout.write('\x1b[33m ' + t('not_detected') + RESET + '\n\n'); const ps = await showArrowMenu('', PROVIDERS_LIST); provider = ps.id; }
|
|
819
|
+
config.apiKey = newKey; config.provider = provider;
|
|
820
|
+
config.model = PROVIDER_DEFAULTS[provider]?.model || 'gpt-4o';
|
|
821
|
+
saveConfig({ apiKey: newKey, provider, model: config.model });
|
|
822
|
+
process.stdout.write('\x1b[32m ' + t('config_updated') + RESET + '\n\n');
|
|
823
|
+
}
|
|
824
|
+
cleanupStdin();
|
|
825
|
+
startChat(config);
|
|
826
|
+
} else if (selected.id === 'change_provider') {
|
|
827
|
+
process.stdout.write('\n');
|
|
828
|
+
const ps = await showArrowMenu(' ' + t('select_provider'), PROVIDERS_LIST);
|
|
829
|
+
config.provider = ps.id; config.model = PROVIDER_DEFAULTS[ps.id]?.model || 'gpt-4o';
|
|
830
|
+
saveConfig({ provider: ps.id, model: config.model });
|
|
831
|
+
process.stdout.write('\x1b[32m ' + t('provider_changed') + PROVIDER_NAMES[ps.id] + RESET + '\n\n');
|
|
832
|
+
startChat(config);
|
|
833
|
+
} else if (selected.id === 'show_config') {
|
|
834
|
+
process.stdout.write('\n');
|
|
835
|
+
const cur = loadConfig() || config;
|
|
836
|
+
process.stdout.write(' Provider: ' + (PROVIDER_NAMES[cur.provider] || cur.provider) + '\n');
|
|
837
|
+
process.stdout.write(' Model: ' + (cur.model || 'default') + '\n');
|
|
838
|
+
process.stdout.write(' Theme: ' + (cur.theme || 'default') + '\n');
|
|
839
|
+
process.stdout.write(' Language: ' + (cur.lang || 'en') + '\n');
|
|
840
|
+
process.stdout.write(' API Key: ' + (cur.apiKey ? cur.apiKey.slice(0, 8) + '...' : 'not set') + '\n\n');
|
|
841
|
+
startChat(config);
|
|
842
|
+
} else {
|
|
843
|
+
process.stdout.write('\n');
|
|
844
|
+
startChat(config);
|
|
845
|
+
}
|
|
846
|
+
}
|
|
847
|
+
|
|
848
|
+
// ═══════════════════════════════════════════════════════════════════
|
|
849
|
+
// LOGOUT & SIGINT
|
|
850
|
+
// ═══════════════════════════════════════════════════════════════════
|
|
851
|
+
|
|
852
|
+
function handleLogout() {
|
|
853
|
+
try { if (fs.existsSync(CONFIG_PATH)) fs.unlinkSync(CONFIG_PATH); } catch (e) { }
|
|
854
|
+
process.stdout.write(t('logged_out') + '\n');
|
|
855
|
+
process.exit(0);
|
|
856
|
+
}
|
|
857
|
+
|
|
858
|
+
process.on('SIGINT', () => {
|
|
859
|
+
try { if (process.stdin.isTTY && process.stdin.isRaw) process.stdin.setRawMode(false); } catch (e) { }
|
|
860
|
+
process.stdout.write('\x1b[?25h\n');
|
|
861
|
+
process.exit(0);
|
|
862
|
+
});
|
|
863
|
+
|
|
864
|
+
// ═══════════════════════════════════════════════════════════════════
|
|
865
|
+
// MAIN
|
|
866
|
+
// ═══════════════════════════════════════════════════════════════════
|
|
867
|
+
|
|
868
|
+
async function main() {
|
|
869
|
+
const args = process.argv.slice(2);
|
|
870
|
+
if (args[0] === 'logout') { handleLogout(); return; }
|
|
871
|
+
|
|
872
|
+
const existingConfig = loadConfig();
|
|
873
|
+
const currentTheme = existingConfig?.theme || 'default';
|
|
874
|
+
currentLang = existingConfig?.lang || 'en';
|
|
875
|
+
|
|
876
|
+
printLogo(currentTheme);
|
|
877
|
+
|
|
878
|
+
if (!existingConfig || !existingConfig.apiKey || !existingConfig.provider) {
|
|
879
|
+
printApiKeyPrompt();
|
|
880
|
+
const key = await askApiKey();
|
|
881
|
+
if (!key) { process.stdout.write('\x1b[31m ' + t('no_key_provided') + RESET + '\n'); process.exit(1); }
|
|
882
|
+
let provider = detectProvider(key);
|
|
883
|
+
if (!provider) {
|
|
884
|
+
process.stdout.write('\x1b[33m ' + t('not_detected') + RESET + '\n\n');
|
|
885
|
+
const selected = await showArrowMenu('', PROVIDERS_LIST);
|
|
886
|
+
provider = selected.id;
|
|
887
|
+
} else {
|
|
888
|
+
process.stdout.write('\x1b[32m ' + t('detected') + PROVIDER_NAMES[provider] + RESET + '\n\n');
|
|
889
|
+
}
|
|
890
|
+
const model = PROVIDER_DEFAULTS[provider]?.model || 'gpt-4o';
|
|
891
|
+
saveConfig({ apiKey: key, provider, theme: 'default', model, lang: currentLang });
|
|
892
|
+
cleanupStdin();
|
|
893
|
+
startChat(loadConfig());
|
|
894
|
+
} else {
|
|
895
|
+
printTips(currentTheme);
|
|
896
|
+
cleanupStdin();
|
|
897
|
+
startChat(existingConfig);
|
|
898
|
+
}
|
|
899
|
+
}
|
|
900
|
+
|
|
901
|
+
main().catch(err => {
|
|
902
|
+
process.stderr.write('\x1b[31mFatal error: ' + (err.message || err) + RESET + '\n');
|
|
903
|
+
process.exit(1);
|
|
904
|
+
});
|