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
|
@@ -0,0 +1,250 @@
|
|
|
1
|
+
import Conf from 'conf';
|
|
2
|
+
import * as fs from 'fs';
|
|
3
|
+
import * as path from 'path';
|
|
4
|
+
import * as os from 'os';
|
|
5
|
+
import * as readline from 'readline';
|
|
6
|
+
import { fileURLToPath } from 'url';
|
|
7
|
+
import { CorexConfig, ThemeName } from '../types.js';
|
|
8
|
+
|
|
9
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
10
|
+
const __dirname = path.dirname(__filename);
|
|
11
|
+
|
|
12
|
+
const FALLBACK_SYSTEM_PROMPT = `You are COREX, an elite AI assistant.
|
|
13
|
+
You are direct, insightful, and technically brilliant.
|
|
14
|
+
Format your responses for terminal display using clean spacing.
|
|
15
|
+
When showing code, use markdown code blocks.
|
|
16
|
+
Keep responses focused and avoid unnecessary filler text.`;
|
|
17
|
+
|
|
18
|
+
function loadSystemPrompt(): string {
|
|
19
|
+
const filename = 'COREX_SYSTEM_PROMPT.txt';
|
|
20
|
+
const possiblePaths = [
|
|
21
|
+
path.join(__dirname, '..', 'assets', filename),
|
|
22
|
+
path.join(__dirname, '..', '..', 'assets', filename),
|
|
23
|
+
path.join(process.cwd(), 'assets', filename),
|
|
24
|
+
];
|
|
25
|
+
|
|
26
|
+
for (const p of possiblePaths) {
|
|
27
|
+
try {
|
|
28
|
+
if (fs.existsSync(p)) {
|
|
29
|
+
return fs.readFileSync(p, 'utf-8').trim();
|
|
30
|
+
}
|
|
31
|
+
} catch {
|
|
32
|
+
// continue
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
return FALLBACK_SYSTEM_PROMPT;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const DEFAULT_SYSTEM_PROMPT = loadSystemPrompt();
|
|
39
|
+
|
|
40
|
+
const defaults: CorexConfig = {
|
|
41
|
+
apiKey: '',
|
|
42
|
+
provider: 'anthropic',
|
|
43
|
+
model: 'claude-3-5-sonnet-20241022',
|
|
44
|
+
theme: 'default' as ThemeName,
|
|
45
|
+
systemPrompt: DEFAULT_SYSTEM_PROMPT,
|
|
46
|
+
maxTokens: 4096,
|
|
47
|
+
temperature: 0.7,
|
|
48
|
+
saveHistory: false,
|
|
49
|
+
userName: 'You',
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
const config = new Conf<CorexConfig>({
|
|
53
|
+
projectName: 'corex',
|
|
54
|
+
defaults,
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
export function loadConfig(): CorexConfig | null {
|
|
58
|
+
try {
|
|
59
|
+
const apiKey = config.get('apiKey');
|
|
60
|
+
if (!apiKey) return null;
|
|
61
|
+
return {
|
|
62
|
+
apiKey: config.get('apiKey'),
|
|
63
|
+
provider: config.get('provider'),
|
|
64
|
+
model: config.get('model'),
|
|
65
|
+
theme: config.get('theme'),
|
|
66
|
+
systemPrompt: config.get('systemPrompt'),
|
|
67
|
+
maxTokens: config.get('maxTokens'),
|
|
68
|
+
temperature: config.get('temperature'),
|
|
69
|
+
saveHistory: config.get('saveHistory'),
|
|
70
|
+
userName: config.get('userName'),
|
|
71
|
+
};
|
|
72
|
+
} catch (error) {
|
|
73
|
+
return null;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export function saveConfig(partial: Partial<CorexConfig>): void {
|
|
78
|
+
for (const [key, value] of Object.entries(partial)) {
|
|
79
|
+
config.set(key as keyof CorexConfig, value as any);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export function deleteConfig(): void {
|
|
84
|
+
config.clear();
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export function isFirstRun(): boolean {
|
|
88
|
+
const c = loadConfig();
|
|
89
|
+
return !c || !c.apiKey;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function detectProvider(key: string): string | null {
|
|
93
|
+
const k = key.trim();
|
|
94
|
+
if (k.startsWith('sk-ant-')) return 'anthropic';
|
|
95
|
+
if (k.startsWith('AIza')) return 'gemini';
|
|
96
|
+
if (k.startsWith('sk-or-v1-') || k.startsWith('sk-or-')) return 'openrouter';
|
|
97
|
+
if (k.startsWith('sk-proj-')) return 'openai';
|
|
98
|
+
if (k.startsWith('sk-') && !k.startsWith('sk-or-')) return 'openai';
|
|
99
|
+
if (k.startsWith('ds-') || k.toLowerCase().includes('deepseek')) return 'deepseek';
|
|
100
|
+
return null;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function cleanupStdin() {
|
|
104
|
+
if (process.stdin.isTTY) {
|
|
105
|
+
process.stdin.setRawMode(false);
|
|
106
|
+
}
|
|
107
|
+
process.stdin.removeAllListeners('data');
|
|
108
|
+
process.stdin.removeAllListeners('keypress');
|
|
109
|
+
process.stdin.pause();
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function askApiKey(): Promise<string> {
|
|
113
|
+
return new Promise((resolve) => {
|
|
114
|
+
process.stdout.write(' API Key ❯ ');
|
|
115
|
+
process.stdin.setRawMode(true);
|
|
116
|
+
process.stdin.resume();
|
|
117
|
+
let key = '';
|
|
118
|
+
|
|
119
|
+
const onData = (chunk: Buffer) => {
|
|
120
|
+
const chars = chunk.toString();
|
|
121
|
+
for (let i = 0; i < chars.length; i++) {
|
|
122
|
+
const ch = chars[i];
|
|
123
|
+
if (ch === '\r' || ch === '\n') {
|
|
124
|
+
process.stdout.write('\n');
|
|
125
|
+
process.stdin.setRawMode(false);
|
|
126
|
+
process.stdin.removeListener('data', onData);
|
|
127
|
+
process.stdin.pause();
|
|
128
|
+
resolve(key.trim());
|
|
129
|
+
return;
|
|
130
|
+
} else if (ch === '\x03') {
|
|
131
|
+
process.stdout.write('\n');
|
|
132
|
+
if (process.stdin.isTTY) process.stdin.setRawMode(false);
|
|
133
|
+
process.exit(0);
|
|
134
|
+
} else if (ch === '\x7f' || ch === '\b') {
|
|
135
|
+
if (key.length > 0) {
|
|
136
|
+
key = key.slice(0, -1);
|
|
137
|
+
process.stdout.clearLine(0);
|
|
138
|
+
process.stdout.cursorTo(0);
|
|
139
|
+
process.stdout.write(' API Key ❯ ' + '•'.repeat(key.length));
|
|
140
|
+
}
|
|
141
|
+
} else {
|
|
142
|
+
key += ch;
|
|
143
|
+
process.stdout.write('•'.repeat(ch.length));
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
};
|
|
147
|
+
|
|
148
|
+
process.stdin.on('data', onData);
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
const PROVIDERS = [
|
|
153
|
+
{ id: 'anthropic', label: 'Anthropic (Claude)' },
|
|
154
|
+
{ id: 'gemini', label: 'Google Gemini' },
|
|
155
|
+
{ id: 'openai', label: 'OpenAI (GPT)' },
|
|
156
|
+
{ id: 'openrouter', label: 'OpenRouter' },
|
|
157
|
+
{ id: 'deepseek', label: 'DeepSeek' },
|
|
158
|
+
];
|
|
159
|
+
|
|
160
|
+
export async function runFirstRunWizard(): Promise<void> {
|
|
161
|
+
console.log('─────────────────────────────────────────────────────────────────────');
|
|
162
|
+
console.log(' Enter your API key below and use what you purchased.');
|
|
163
|
+
console.log(' Supports: Anthropic · Gemini · OpenAI · OpenRouter · DeepSeek');
|
|
164
|
+
console.log('');
|
|
165
|
+
console.log(' Your key will be auto-detected. No manual setup needed.');
|
|
166
|
+
console.log('');
|
|
167
|
+
|
|
168
|
+
const apiKey = await askApiKey();
|
|
169
|
+
|
|
170
|
+
console.log('');
|
|
171
|
+
|
|
172
|
+
const detected = detectProvider(apiKey);
|
|
173
|
+
let providerId = detected;
|
|
174
|
+
|
|
175
|
+
if (detected) {
|
|
176
|
+
const label = PROVIDERS.find(p => p.id === detected)?.label || detected;
|
|
177
|
+
console.log(` \x1b[32m✓ Detected: ${label}\x1b[0m`);
|
|
178
|
+
} else {
|
|
179
|
+
console.log(' \x1b[33m⚠ Could not detect provider. Select manually:\x1b[0m');
|
|
180
|
+
console.log('');
|
|
181
|
+
|
|
182
|
+
let selectedIdx = 0;
|
|
183
|
+
|
|
184
|
+
const printMenu = () => {
|
|
185
|
+
process.stdout.write('\x1b[?25l'); // hide cursor
|
|
186
|
+
for (let i = 0; i < PROVIDERS.length; i++) {
|
|
187
|
+
const prefix = i === selectedIdx ? ' ❯ ' : ' ';
|
|
188
|
+
console.log(`${prefix}${PROVIDERS[i].label}`);
|
|
189
|
+
}
|
|
190
|
+
console.log('\n Navigate: ↑ ↓ Confirm: Enter');
|
|
191
|
+
};
|
|
192
|
+
|
|
193
|
+
const clearMenu = () => {
|
|
194
|
+
// PROVIDERS.length + 2 lines for spacing/nav text
|
|
195
|
+
process.stdout.write(`\x1b[${PROVIDERS.length + 2}A`);
|
|
196
|
+
};
|
|
197
|
+
|
|
198
|
+
printMenu();
|
|
199
|
+
|
|
200
|
+
providerId = await new Promise<string>((resolve) => {
|
|
201
|
+
const onKey = (chunk: Buffer) => {
|
|
202
|
+
const key = chunk.toString();
|
|
203
|
+
if (key === '\x1b[A') { // Up
|
|
204
|
+
if (selectedIdx > 0) {
|
|
205
|
+
selectedIdx--;
|
|
206
|
+
clearMenu();
|
|
207
|
+
printMenu();
|
|
208
|
+
}
|
|
209
|
+
} else if (key === '\x1b[B') { // Down
|
|
210
|
+
if (selectedIdx < PROVIDERS.length - 1) {
|
|
211
|
+
selectedIdx++;
|
|
212
|
+
clearMenu();
|
|
213
|
+
printMenu();
|
|
214
|
+
}
|
|
215
|
+
} else if (key === '\r' || key === '\n') {
|
|
216
|
+
process.stdin.removeListener('data', onKey);
|
|
217
|
+
process.stdout.write('\x1b[?25h'); // show cursor
|
|
218
|
+
resolve(PROVIDERS[selectedIdx].id);
|
|
219
|
+
} else if (key === '\x03') {
|
|
220
|
+
cleanupStdin();
|
|
221
|
+
process.stdout.write('\n');
|
|
222
|
+
process.exit(0);
|
|
223
|
+
}
|
|
224
|
+
};
|
|
225
|
+
process.stdin.setRawMode(true);
|
|
226
|
+
process.stdin.resume();
|
|
227
|
+
process.stdin.on('data', onKey);
|
|
228
|
+
});
|
|
229
|
+
console.log('');
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
const providerDefaults: Record<string, string> = {
|
|
233
|
+
anthropic: 'claude-3-5-sonnet-20241022',
|
|
234
|
+
gemini: 'gemini-1.5-pro',
|
|
235
|
+
openai: 'gpt-4o',
|
|
236
|
+
openrouter: 'openai/gpt-4o',
|
|
237
|
+
deepseek: 'deepseek-chat',
|
|
238
|
+
};
|
|
239
|
+
|
|
240
|
+
saveConfig({
|
|
241
|
+
apiKey,
|
|
242
|
+
provider: providerId as any,
|
|
243
|
+
model: providerDefaults[providerId || 'openai']
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
console.log('─────────────────────────────────────────────────────────────────────');
|
|
247
|
+
|
|
248
|
+
// CRITICAL CLEANUP
|
|
249
|
+
cleanupStdin();
|
|
250
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import * as fs from 'fs';
|
|
2
|
+
import * as path from 'path';
|
|
3
|
+
import * as os from 'os';
|
|
4
|
+
import { Message } from '../types.js';
|
|
5
|
+
|
|
6
|
+
let history: Message[] = [];
|
|
7
|
+
|
|
8
|
+
export function addMessage(role: 'user' | 'assistant', content: string): void {
|
|
9
|
+
history.push({ role, content });
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function getHistory(): Message[] {
|
|
13
|
+
return [...history];
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function clearHistory(): void {
|
|
17
|
+
history = [];
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function saveSession(): string {
|
|
21
|
+
const sessionsDir = path.join(os.homedir(), '.corex', 'sessions');
|
|
22
|
+
if (!fs.existsSync(sessionsDir)) {
|
|
23
|
+
fs.mkdirSync(sessionsDir, { recursive: true });
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const now = new Date();
|
|
27
|
+
const pad = (n: number) => n.toString().padStart(2, '0');
|
|
28
|
+
const filename = `${now.getFullYear()}-${pad(now.getMonth() + 1)}-${pad(now.getDate())}_${pad(now.getHours())}-${pad(now.getMinutes())}.txt`;
|
|
29
|
+
const filepath = path.join(sessionsDir, filename);
|
|
30
|
+
|
|
31
|
+
let content = '=== COREX Chat Session ===\n';
|
|
32
|
+
content += `Date: ${now.toLocaleString()}\n`;
|
|
33
|
+
content += `Messages: ${history.length}\n`;
|
|
34
|
+
content += '='.repeat(40) + '\n\n';
|
|
35
|
+
|
|
36
|
+
for (const msg of history) {
|
|
37
|
+
const label = msg.role === 'user' ? 'You' : 'COREX';
|
|
38
|
+
content += `[${label}]\n${msg.content}\n\n`;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
fs.writeFileSync(filepath, content, 'utf-8');
|
|
42
|
+
return filepath;
|
|
43
|
+
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { Theme, ThemeName } from '../types.js';
|
|
2
|
+
|
|
3
|
+
export const theme = {
|
|
4
|
+
background: '#0B1220',
|
|
5
|
+
surface: '#0F1E3A',
|
|
6
|
+
primary: '#3B82F6',
|
|
7
|
+
highlight: '#93C5FD',
|
|
8
|
+
textPrimary: '#E5E7EB',
|
|
9
|
+
textDim: '#6B7280',
|
|
10
|
+
border: '#1E3A5F',
|
|
11
|
+
success: '#10B981',
|
|
12
|
+
error: '#EF4444',
|
|
13
|
+
warning: '#F59E0B',
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
export const themes: Record<ThemeName, Theme> = {
|
|
17
|
+
default: {
|
|
18
|
+
label: 'Default',
|
|
19
|
+
userText: theme.textPrimary,
|
|
20
|
+
aiText: theme.highlight,
|
|
21
|
+
border: theme.border,
|
|
22
|
+
accent: theme.primary,
|
|
23
|
+
statusBar: theme.textDim,
|
|
24
|
+
headerGradient: [theme.primary, theme.highlight],
|
|
25
|
+
},
|
|
26
|
+
matrix: {
|
|
27
|
+
label: 'Matrix (green hacker)',
|
|
28
|
+
userText: '#FFFFFF',
|
|
29
|
+
aiText: '#00FF00',
|
|
30
|
+
border: '#00FF00',
|
|
31
|
+
accent: '#00FF00',
|
|
32
|
+
statusBar: '#003300',
|
|
33
|
+
headerGradient: ['#00FF00', '#003300'],
|
|
34
|
+
},
|
|
35
|
+
cyberpunk: {
|
|
36
|
+
label: 'Cyberpunk (yellow + magenta)',
|
|
37
|
+
userText: '#FFFF00',
|
|
38
|
+
aiText: '#FF00FF',
|
|
39
|
+
border: '#00FFFF',
|
|
40
|
+
accent: '#FFFF00',
|
|
41
|
+
statusBar: '#FF00FF',
|
|
42
|
+
headerGradient: ['#FFFF00', '#FF00FF'],
|
|
43
|
+
},
|
|
44
|
+
ocean: {
|
|
45
|
+
label: 'Ocean (cyan + blue)',
|
|
46
|
+
userText: '#00FFFF',
|
|
47
|
+
aiText: '#0099FF',
|
|
48
|
+
border: '#00FFFF',
|
|
49
|
+
accent: '#00FFFF',
|
|
50
|
+
statusBar: '#003366',
|
|
51
|
+
headerGradient: ['#00FFFF', '#0000FF'],
|
|
52
|
+
},
|
|
53
|
+
blood: {
|
|
54
|
+
label: 'Blood (red + dark red)',
|
|
55
|
+
userText: '#FF0000',
|
|
56
|
+
aiText: '#CC0000',
|
|
57
|
+
border: '#FF0000',
|
|
58
|
+
accent: '#FF0000',
|
|
59
|
+
statusBar: '#330000',
|
|
60
|
+
headerGradient: ['#FF0000', '#330000'],
|
|
61
|
+
},
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
export function getTheme(name: ThemeName): Theme {
|
|
65
|
+
return themes[name] || themes.default;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export function getThemeNames(): ThemeName[] {
|
|
69
|
+
return Object.keys(themes) as ThemeName[];
|
|
70
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
declare module 'gradient-string' {
|
|
2
|
+
interface GradientOptions {
|
|
3
|
+
interpolation?: string;
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
function gradient(colors: string[], options?: GradientOptions): {
|
|
7
|
+
(text: string): string;
|
|
8
|
+
multiline(text: string): string;
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
export default gradient;
|
|
12
|
+
}
|
package/src/types.ts
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
export interface Message {
|
|
2
|
+
role: 'user' | 'assistant';
|
|
3
|
+
content: string;
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
export interface TokenUsage {
|
|
7
|
+
inputTokens: number;
|
|
8
|
+
outputTokens: number;
|
|
9
|
+
totalTokens: number;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export type ThemeName = 'default' | 'matrix' | 'cyberpunk' | 'ocean' | 'blood';
|
|
13
|
+
|
|
14
|
+
export interface Theme {
|
|
15
|
+
label: string;
|
|
16
|
+
userText: string;
|
|
17
|
+
aiText: string;
|
|
18
|
+
border: string;
|
|
19
|
+
accent: string;
|
|
20
|
+
statusBar: string;
|
|
21
|
+
headerGradient: string[];
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export interface CorexConfig {
|
|
25
|
+
apiKey: string;
|
|
26
|
+
provider: 'anthropic' | 'gemini' | 'openai' | 'openrouter' | 'deepseek';
|
|
27
|
+
model: string;
|
|
28
|
+
theme: ThemeName;
|
|
29
|
+
systemPrompt: string;
|
|
30
|
+
maxTokens: number;
|
|
31
|
+
temperature: number;
|
|
32
|
+
saveHistory: boolean;
|
|
33
|
+
userName: string;
|
|
34
|
+
}
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2022",
|
|
4
|
+
"module": "nodenext",
|
|
5
|
+
"moduleResolution": "nodenext",
|
|
6
|
+
"lib": [
|
|
7
|
+
"ES2022"
|
|
8
|
+
],
|
|
9
|
+
"jsx": "react",
|
|
10
|
+
"strict": true,
|
|
11
|
+
"esModuleInterop": true,
|
|
12
|
+
"skipLibCheck": true,
|
|
13
|
+
"outDir": "./dist",
|
|
14
|
+
"rootDir": "./src",
|
|
15
|
+
"resolveJsonModule": true
|
|
16
|
+
},
|
|
17
|
+
"include": [
|
|
18
|
+
"src/**/*"
|
|
19
|
+
]
|
|
20
|
+
}
|
package/tsup.config.ts
ADDED
package/tsx
ADDED
|
File without changes
|