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/install.sh
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
set -e
|
|
3
|
+
|
|
4
|
+
echo "---------------------------------------"
|
|
5
|
+
echo " Installing COREX AI Terminal Chat"
|
|
6
|
+
echo "---------------------------------------"
|
|
7
|
+
|
|
8
|
+
if ! command -v node &> /dev/null; then
|
|
9
|
+
echo "ERROR: Node.js is required. Install from https://nodejs.org"
|
|
10
|
+
exit 1
|
|
11
|
+
fi
|
|
12
|
+
|
|
13
|
+
NODE_VERSION=$(node -v | cut -d'v' -f2 | cut -d'.' -f1)
|
|
14
|
+
if [ "$NODE_VERSION" -lt 18 ]; then
|
|
15
|
+
echo "ERROR: Node.js 18 or higher is required. Current: $(node -v)"
|
|
16
|
+
exit 1
|
|
17
|
+
fi
|
|
18
|
+
|
|
19
|
+
echo "OK Node.js $(node -v) detected"
|
|
20
|
+
npm install -g corex-ai
|
|
21
|
+
|
|
22
|
+
echo ""
|
|
23
|
+
echo "---------------------------------------"
|
|
24
|
+
echo " COREX installed successfully."
|
|
25
|
+
echo " Type 'corex' to launch."
|
|
26
|
+
echo "---------------------------------------"
|
package/package.json
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "corex-cli",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "COREX CLI — powerful AI terminal",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"corex": "./bin/corex.js"
|
|
8
|
+
},
|
|
9
|
+
"scripts": {
|
|
10
|
+
"dev": "node --experimental-strip-types --enable-source-maps -e \"process.stdin.setRawMode = () => {}; require('tsx').tsc();\" 2>/dev/null || tsx --inspect=0 src/index.ts",
|
|
11
|
+
"start": "node bin/corex.js",
|
|
12
|
+
"build": "tsup src/index.ts --out-dir dist --format esm --external @anthropic-ai/sdk --external @google/generative-ai --external openai"
|
|
13
|
+
},
|
|
14
|
+
"dependencies": {
|
|
15
|
+
"@anthropic-ai/sdk": "latest",
|
|
16
|
+
"@google/generative-ai": "latest",
|
|
17
|
+
"conf": "^12.0.0",
|
|
18
|
+
"dotenv": "^16.3.1",
|
|
19
|
+
"glob": "^10.0.0",
|
|
20
|
+
"gradient-string": "^2.0.2",
|
|
21
|
+
"ink": "^4.4.1",
|
|
22
|
+
"ink-text-input": "^5.0.1",
|
|
23
|
+
"openai": "latest",
|
|
24
|
+
"pdf-parse": "^1.1.1",
|
|
25
|
+
"react": "^18.2.0"
|
|
26
|
+
},
|
|
27
|
+
"devDependencies": {
|
|
28
|
+
"@types/node": "^20.10.0",
|
|
29
|
+
"@types/react": "^18.2.0",
|
|
30
|
+
"tsx": "^4.6.0",
|
|
31
|
+
"tsup": "^8.0.0",
|
|
32
|
+
"typescript": "^5.3.0"
|
|
33
|
+
}
|
|
34
|
+
}
|
package/src/app.tsx
ADDED
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
import React, { useState, useCallback } from 'react';
|
|
2
|
+
import { Box, Text, useApp, useInput } from 'ink';
|
|
3
|
+
import { CorexConfig, Message } from './types.js';
|
|
4
|
+
import { addMessage } from './lib/history.js';
|
|
5
|
+
import ChatHistory from './components/ChatHistory.js';
|
|
6
|
+
import InputBar from './components/InputBar.js';
|
|
7
|
+
import TopBar from './components/TopBar.js';
|
|
8
|
+
import StatusArea from './components/StatusArea.js';
|
|
9
|
+
import BootScreen from './components/BootScreen.js';
|
|
10
|
+
import ApiKeyScreen from './components/ApiKeyScreen.js';
|
|
11
|
+
import { theme } from './themes/themes.js';
|
|
12
|
+
import { detectProvider } from './core/providers/index.js';
|
|
13
|
+
import { chatAnthropic, chatOpenAI, chatGemini, ProviderConfig } from './core/providers/index.js';
|
|
14
|
+
import { NetworkError, parseApiError } from './core/network/request.js';
|
|
15
|
+
|
|
16
|
+
interface AppProps {
|
|
17
|
+
config: CorexConfig;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
type Screen = 'boot' | 'apiKey' | 'chat';
|
|
21
|
+
|
|
22
|
+
const App: React.FC<AppProps> = ({ config: initialConfig }: AppProps) => {
|
|
23
|
+
const { exit } = useApp();
|
|
24
|
+
const [screen, setScreen] = useState<Screen>('boot');
|
|
25
|
+
const [config, setConfig] = useState<CorexConfig>(initialConfig);
|
|
26
|
+
const [messages, setMessages] = useState<Message[]>([]);
|
|
27
|
+
const [inputValue, setInputValue] = useState('');
|
|
28
|
+
const [isThinking, setIsThinking] = useState(false);
|
|
29
|
+
const [streamingText, setStreamingText] = useState('');
|
|
30
|
+
const [status, setStatus] = useState<'idle' | 'typing' | 'thinking'>('idle');
|
|
31
|
+
const [providerName, setProviderName] = useState<string>('Not Connected');
|
|
32
|
+
|
|
33
|
+
useInput((input, key) => {
|
|
34
|
+
if (screen !== 'chat') return;
|
|
35
|
+
|
|
36
|
+
if (key.ctrl && input === 'c') {
|
|
37
|
+
process.stdout.write('\x1b[?25h');
|
|
38
|
+
exit();
|
|
39
|
+
process.exit(0);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
if (key.ctrl && input === 'l') {
|
|
43
|
+
setMessages([]);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
if (key.ctrl && input === 'k') {
|
|
47
|
+
setMessages([]);
|
|
48
|
+
setInputValue('');
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
if (key.tab) {
|
|
52
|
+
// Future command mode placeholder
|
|
53
|
+
}
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
const handleBootComplete = useCallback(() => {
|
|
57
|
+
setScreen('apiKey');
|
|
58
|
+
}, []);
|
|
59
|
+
|
|
60
|
+
const handleApiKeySubmit = useCallback(async (apiKey: string) => {
|
|
61
|
+
try {
|
|
62
|
+
const provider = await detectProvider(apiKey);
|
|
63
|
+
setProviderName(provider);
|
|
64
|
+
setConfig({ ...config, apiKey, provider: provider as any });
|
|
65
|
+
setScreen('chat');
|
|
66
|
+
} catch (error: any) {
|
|
67
|
+
setMessages([{
|
|
68
|
+
role: 'assistant',
|
|
69
|
+
content: `Error: ${error.message}`
|
|
70
|
+
}]);
|
|
71
|
+
setScreen('chat');
|
|
72
|
+
}
|
|
73
|
+
}, [config]);
|
|
74
|
+
|
|
75
|
+
const addSystemMessage = useCallback((text: string) => {
|
|
76
|
+
setMessages((prev: Message[]) => [...prev, { role: 'assistant', content: text }]);
|
|
77
|
+
}, []);
|
|
78
|
+
|
|
79
|
+
const handleSubmit = useCallback(
|
|
80
|
+
async (value: string) => {
|
|
81
|
+
const trimmed = value.trim();
|
|
82
|
+
if (!trimmed || isThinking) return;
|
|
83
|
+
|
|
84
|
+
setInputValue('');
|
|
85
|
+
setStatus('typing');
|
|
86
|
+
|
|
87
|
+
const userMsg: Message = { role: 'user', content: trimmed };
|
|
88
|
+
setMessages((prev: Message[]) => [...prev, userMsg]);
|
|
89
|
+
addMessage('user', trimmed);
|
|
90
|
+
|
|
91
|
+
setStatus('thinking');
|
|
92
|
+
setIsThinking(true);
|
|
93
|
+
setStreamingText('');
|
|
94
|
+
|
|
95
|
+
try {
|
|
96
|
+
const providerConfig: ProviderConfig = {
|
|
97
|
+
apiKey: config.apiKey,
|
|
98
|
+
model: config.model,
|
|
99
|
+
systemPrompt: config.systemPrompt,
|
|
100
|
+
temperature: config.temperature,
|
|
101
|
+
maxTokens: config.maxTokens,
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
let response: { content: string; usage?: any };
|
|
105
|
+
|
|
106
|
+
switch (config.provider) {
|
|
107
|
+
case 'anthropic':
|
|
108
|
+
response = await chatAnthropic(
|
|
109
|
+
messages,
|
|
110
|
+
trimmed,
|
|
111
|
+
providerConfig,
|
|
112
|
+
(token) => setStreamingText(prev => prev + token)
|
|
113
|
+
);
|
|
114
|
+
break;
|
|
115
|
+
case 'gemini':
|
|
116
|
+
response = await chatGemini(
|
|
117
|
+
messages,
|
|
118
|
+
trimmed,
|
|
119
|
+
providerConfig,
|
|
120
|
+
(token) => setStreamingText(prev => prev + token)
|
|
121
|
+
);
|
|
122
|
+
break;
|
|
123
|
+
case 'openai':
|
|
124
|
+
case 'openrouter':
|
|
125
|
+
case 'deepseek':
|
|
126
|
+
const baseURL = config.provider === 'openrouter'
|
|
127
|
+
? 'https://openrouter.ai/api/v1'
|
|
128
|
+
: config.provider === 'deepseek'
|
|
129
|
+
? 'https://api.deepseek.com'
|
|
130
|
+
: 'https://api.openai.com/v1';
|
|
131
|
+
response = await chatOpenAI(
|
|
132
|
+
messages,
|
|
133
|
+
trimmed,
|
|
134
|
+
providerConfig,
|
|
135
|
+
(token) => setStreamingText(prev => prev + token),
|
|
136
|
+
baseURL
|
|
137
|
+
);
|
|
138
|
+
break;
|
|
139
|
+
default:
|
|
140
|
+
throw new Error(`Unknown provider: ${config.provider}`);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
const assistantMsg: Message = {
|
|
144
|
+
role: 'assistant',
|
|
145
|
+
content: response.content
|
|
146
|
+
};
|
|
147
|
+
setMessages((prev: Message[]) => [...prev, assistantMsg]);
|
|
148
|
+
addMessage('assistant', response.content);
|
|
149
|
+
setStreamingText('');
|
|
150
|
+
|
|
151
|
+
} catch (error: any) {
|
|
152
|
+
let errorMessage: string;
|
|
153
|
+
|
|
154
|
+
if (error instanceof NetworkError) {
|
|
155
|
+
errorMessage = error.message;
|
|
156
|
+
} else if (error.status === 401) {
|
|
157
|
+
errorMessage = 'Invalid API key. Run /config to update.';
|
|
158
|
+
} else if (error.status === 429) {
|
|
159
|
+
errorMessage = 'Rate limit exceeded. Please wait and try again.';
|
|
160
|
+
} else if (error.status >= 500) {
|
|
161
|
+
errorMessage = 'Provider service unavailable.';
|
|
162
|
+
} else {
|
|
163
|
+
errorMessage = parseApiError(error, 'An unexpected error occurred.');
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
addSystemMessage(errorMessage);
|
|
167
|
+
setStreamingText('');
|
|
168
|
+
} finally {
|
|
169
|
+
setIsThinking(false);
|
|
170
|
+
setStatus('idle');
|
|
171
|
+
}
|
|
172
|
+
},
|
|
173
|
+
[config, messages, isThinking, addMessage, addSystemMessage]
|
|
174
|
+
);
|
|
175
|
+
|
|
176
|
+
const handleInputChange = useCallback(
|
|
177
|
+
(value: string) => {
|
|
178
|
+
setInputValue(value);
|
|
179
|
+
if (value.length > 0 && status === 'idle') {
|
|
180
|
+
setStatus('typing');
|
|
181
|
+
} else if (value.length === 0) {
|
|
182
|
+
setStatus('idle');
|
|
183
|
+
}
|
|
184
|
+
},
|
|
185
|
+
[status]
|
|
186
|
+
);
|
|
187
|
+
|
|
188
|
+
if (screen === 'boot') {
|
|
189
|
+
return <BootScreen onComplete={handleBootComplete} />;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
if (screen === 'apiKey') {
|
|
193
|
+
return <ApiKeyScreen onSubmit={handleApiKeySubmit} />;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
return (
|
|
197
|
+
<Box flexDirection="column" height="100%">
|
|
198
|
+
<TopBar provider={providerName} />
|
|
199
|
+
<Box flexDirection="row" justifyContent="flex-end" paddingX={1}>
|
|
200
|
+
<StatusArea status={status} />
|
|
201
|
+
</Box>
|
|
202
|
+
<ChatHistory
|
|
203
|
+
messages={messages}
|
|
204
|
+
isThinking={isThinking}
|
|
205
|
+
streamingText={streamingText}
|
|
206
|
+
/>
|
|
207
|
+
<InputBar
|
|
208
|
+
value={inputValue}
|
|
209
|
+
onChange={handleInputChange}
|
|
210
|
+
onSubmit={handleSubmit}
|
|
211
|
+
isDisabled={isThinking}
|
|
212
|
+
/>
|
|
213
|
+
</Box>
|
|
214
|
+
);
|
|
215
|
+
};
|
|
216
|
+
|
|
217
|
+
export default App;
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import React, { useState, useCallback } from 'react';
|
|
2
|
+
import { Box, Text } from 'ink';
|
|
3
|
+
import TextInput from 'ink-text-input';
|
|
4
|
+
import { useInput } from 'ink';
|
|
5
|
+
import { theme } from '../themes/themes.js';
|
|
6
|
+
|
|
7
|
+
interface ApiKeyScreenProps {
|
|
8
|
+
onSubmit: (apiKey: string) => void;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
const ApiKeyScreen: React.FC<ApiKeyScreenProps> = ({ onSubmit }: ApiKeyScreenProps) => {
|
|
12
|
+
const [apiKey, setApiKey] = useState('');
|
|
13
|
+
const [showKey, setShowKey] = useState(false);
|
|
14
|
+
|
|
15
|
+
useInput((input, key) => {
|
|
16
|
+
if (key.ctrl && input === 'v') {
|
|
17
|
+
setShowKey((prev) => !prev);
|
|
18
|
+
}
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
const handleSubmit = useCallback(() => {
|
|
22
|
+
if (apiKey.trim()) {
|
|
23
|
+
onSubmit(apiKey.trim());
|
|
24
|
+
}
|
|
25
|
+
}, [apiKey, onSubmit]);
|
|
26
|
+
|
|
27
|
+
const displayValue = showKey ? apiKey : apiKey.replace(/./g, '•');
|
|
28
|
+
|
|
29
|
+
return (
|
|
30
|
+
<Box
|
|
31
|
+
flexDirection="column"
|
|
32
|
+
justifyContent="center"
|
|
33
|
+
alignItems="center"
|
|
34
|
+
height="100%"
|
|
35
|
+
>
|
|
36
|
+
<Box flexDirection="column" alignItems="center">
|
|
37
|
+
<Text bold color={theme.primary}>COREX</Text>
|
|
38
|
+
</Box>
|
|
39
|
+
<Box marginTop={1} flexDirection="column" alignItems="center">
|
|
40
|
+
<Text color={theme.textDim}>AI GATEWAY</Text>
|
|
41
|
+
</Box>
|
|
42
|
+
<Box marginTop={6} flexDirection="column" alignItems="center">
|
|
43
|
+
<Text color={theme.textDim}>Enter an API key</Text>
|
|
44
|
+
</Box>
|
|
45
|
+
<Box marginTop={2} flexDirection="row" alignItems="center">
|
|
46
|
+
<Text color={theme.primary}>{'>'} </Text>
|
|
47
|
+
<TextInput
|
|
48
|
+
value={displayValue}
|
|
49
|
+
onChange={setApiKey}
|
|
50
|
+
onSubmit={handleSubmit}
|
|
51
|
+
placeholder=""
|
|
52
|
+
mask={!showKey ? '•' : undefined}
|
|
53
|
+
focus={true}
|
|
54
|
+
/>
|
|
55
|
+
</Box>
|
|
56
|
+
<Box marginTop={3} flexDirection="column" alignItems="center">
|
|
57
|
+
<Text color={theme.textDim} dimColor>
|
|
58
|
+
Ctrl+V to toggle visibility • Enter to continue
|
|
59
|
+
</Text>
|
|
60
|
+
</Box>
|
|
61
|
+
</Box>
|
|
62
|
+
);
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
export default ApiKeyScreen;
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import React, { useState, useEffect } from 'react';
|
|
2
|
+
import { Box, Text } from 'ink';
|
|
3
|
+
import { theme } from '../themes/themes.js';
|
|
4
|
+
|
|
5
|
+
interface BootScreenProps {
|
|
6
|
+
onComplete: () => void;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
const LOGO = `
|
|
10
|
+
██████╗██╗ ██╗ ██████╗ ██████╗ ██████╗ ████████╗███████╗ ██████╗ ██╗ ██╗ ██████╗
|
|
11
|
+
██╔════╝██║ ██║ ██╔══██╗██╔═══██╗██╔══██╗╚══██╔══╝██╔════╝██╔═══██╗██║ ██║██╔═══██╗
|
|
12
|
+
██║ ██║ ██║ ██████╔╝██║ ██║██████╔╝ ██║ █████╗ ██║ ██║██║ ██║██║ ██║
|
|
13
|
+
██║ ██║ ██║ ██╔═══╝ ██║ ██║██╔══██╗ ██║ ██╔══╝ ██║ ██║██║ ██║██║ ██║
|
|
14
|
+
╚██████╗███████╗██║ ██║ ╚██████╔╝██║ ██║ ██║ ██║ ╚██████╔╝███████╗██║╚██████╔╝
|
|
15
|
+
╚═════╝╚══════╝╚═╝ ╚═╝ ╚═════╝ ╚═╝ ╚═╝ ╚═╝ ╚═╝ ╚═════╝ ╚══════╝╚═╝ ╚═════╝
|
|
16
|
+
`;
|
|
17
|
+
|
|
18
|
+
const BootScreen: React.FC<BootScreenProps> = ({ onComplete }: BootScreenProps) => {
|
|
19
|
+
const [fadeIn, setFadeIn] = useState(0);
|
|
20
|
+
|
|
21
|
+
useEffect(() => {
|
|
22
|
+
const fadeInterval = setInterval(() => {
|
|
23
|
+
setFadeIn((prev) => {
|
|
24
|
+
if (prev >= 1) {
|
|
25
|
+
clearInterval(fadeInterval);
|
|
26
|
+
setTimeout(onComplete, 1500);
|
|
27
|
+
return 1;
|
|
28
|
+
}
|
|
29
|
+
return prev + 0.1;
|
|
30
|
+
});
|
|
31
|
+
}, 150);
|
|
32
|
+
|
|
33
|
+
return () => clearInterval(fadeInterval);
|
|
34
|
+
}, [onComplete]);
|
|
35
|
+
|
|
36
|
+
const opacity = Math.floor(fadeIn * 255)
|
|
37
|
+
.toString(16)
|
|
38
|
+
.padStart(2, '0');
|
|
39
|
+
|
|
40
|
+
return (
|
|
41
|
+
<Box
|
|
42
|
+
flexDirection="column"
|
|
43
|
+
justifyContent="center"
|
|
44
|
+
alignItems="center"
|
|
45
|
+
height="100%"
|
|
46
|
+
>
|
|
47
|
+
<Box flexDirection="column" alignItems="center">
|
|
48
|
+
<Text color={`#${opacity}93C5FD` as any}>{LOGO}</Text>
|
|
49
|
+
</Box>
|
|
50
|
+
<Box marginTop={1} flexDirection="column" alignItems="center">
|
|
51
|
+
<Text bold color={`#${opacity}3B82F6` as any}>
|
|
52
|
+
AI GATEWAY
|
|
53
|
+
</Text>
|
|
54
|
+
</Box>
|
|
55
|
+
<Box marginTop={3} flexDirection="column" alignItems="center">
|
|
56
|
+
<Text color={`#${opacity}6B7280` as any}>Universal AI Provider Gateway</Text>
|
|
57
|
+
</Box>
|
|
58
|
+
</Box>
|
|
59
|
+
);
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
export default BootScreen;
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { Box, Text } from 'ink';
|
|
3
|
+
import { Message } from '../types.js';
|
|
4
|
+
import { theme } from '../themes/themes.js';
|
|
5
|
+
import ThinkingDots from './ThinkingDots.js';
|
|
6
|
+
|
|
7
|
+
interface ChatHistoryProps {
|
|
8
|
+
messages: Message[];
|
|
9
|
+
isThinking: boolean;
|
|
10
|
+
streamingText: string;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const ChatHistory: React.FC<ChatHistoryProps> = ({
|
|
14
|
+
messages,
|
|
15
|
+
isThinking,
|
|
16
|
+
streamingText,
|
|
17
|
+
}: ChatHistoryProps) => {
|
|
18
|
+
return (
|
|
19
|
+
<Box flexDirection="column" flexGrow={1} paddingX={1}>
|
|
20
|
+
{messages.map((msg: Message, i: number) => (
|
|
21
|
+
<Box key={i} flexDirection="column" marginBottom={1}>
|
|
22
|
+
{msg.role === 'user' ? (
|
|
23
|
+
<Text color={theme.textDim}>{`> ${msg.content}`}</Text>
|
|
24
|
+
) : (
|
|
25
|
+
<Text color={theme.highlight}>{msg.content}</Text>
|
|
26
|
+
)}
|
|
27
|
+
</Box>
|
|
28
|
+
))}
|
|
29
|
+
|
|
30
|
+
{streamingText && (
|
|
31
|
+
<Box flexDirection="column" marginBottom={1}>
|
|
32
|
+
<Text color={theme.highlight}>{streamingText}</Text>
|
|
33
|
+
</Box>
|
|
34
|
+
)}
|
|
35
|
+
|
|
36
|
+
{isThinking && !streamingText && (
|
|
37
|
+
<Box marginBottom={1}>
|
|
38
|
+
<ThinkingDots />
|
|
39
|
+
</Box>
|
|
40
|
+
)}
|
|
41
|
+
</Box>
|
|
42
|
+
);
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
export default ChatHistory;
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import React, { useState, useEffect } from 'react';
|
|
2
|
+
import { Box, Text } from 'ink';
|
|
3
|
+
import * as fs from 'fs';
|
|
4
|
+
import * as path from 'path';
|
|
5
|
+
import gradient from 'gradient-string';
|
|
6
|
+
import { Theme } from '../types.js';
|
|
7
|
+
|
|
8
|
+
interface HeaderProps {
|
|
9
|
+
theme: Theme;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
const Header: React.FC<HeaderProps> = ({ theme }) => {
|
|
13
|
+
const [logoLines, setLogoLines] = useState<string[]>([]);
|
|
14
|
+
|
|
15
|
+
useEffect(() => {
|
|
16
|
+
try {
|
|
17
|
+
// Try multiple paths for logo.txt
|
|
18
|
+
const possiblePaths = [
|
|
19
|
+
path.join(__dirname, '..', 'assets', 'logo.txt'),
|
|
20
|
+
path.join(__dirname, '..', '..', 'assets', 'logo.txt'),
|
|
21
|
+
path.join(process.cwd(), 'assets', 'logo.txt'),
|
|
22
|
+
];
|
|
23
|
+
|
|
24
|
+
let logoText = '';
|
|
25
|
+
for (const p of possiblePaths) {
|
|
26
|
+
if (fs.existsSync(p)) {
|
|
27
|
+
logoText = fs.readFileSync(p, 'utf-8');
|
|
28
|
+
break;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
if (!logoText) {
|
|
33
|
+
// Fallback inline logo
|
|
34
|
+
logoText = `
|
|
35
|
+
██████╗ ██████╗ ██████╗ ███████╗██╗ ██╗
|
|
36
|
+
██╔════╝██╔═══██╗██╔══██╗██╔════╝╚██╗██╔╝
|
|
37
|
+
██║ ██║ ██║██████╔╝█████╗ ╚███╔╝
|
|
38
|
+
██║ ██║ ██║██╔══██╗██╔══╝ ██╔██╗
|
|
39
|
+
╚██████╗╚██████╔╝██║ ██║███████╗██╔╝ ██╗
|
|
40
|
+
╚═════╝ ╚═════╝ ╚═╝ ╚═╝╚══════╝╚═╝ ╚═╝`;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const grad = gradient(theme.headerGradient);
|
|
44
|
+
const lines = logoText.split('\n').map((line) => grad(line));
|
|
45
|
+
setLogoLines(lines);
|
|
46
|
+
} catch {
|
|
47
|
+
setLogoLines([' COREX']);
|
|
48
|
+
}
|
|
49
|
+
}, [theme]);
|
|
50
|
+
|
|
51
|
+
return (
|
|
52
|
+
<Box flexDirection="column" marginBottom={1}>
|
|
53
|
+
{logoLines.map((line, i) => (
|
|
54
|
+
<Text key={i}>{line}</Text>
|
|
55
|
+
))}
|
|
56
|
+
</Box>
|
|
57
|
+
);
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
export default Header;
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { Box, Text } from 'ink';
|
|
3
|
+
import TextInput from 'ink-text-input';
|
|
4
|
+
import { theme } from '../themes/themes.js';
|
|
5
|
+
|
|
6
|
+
interface InputBarProps {
|
|
7
|
+
value: string;
|
|
8
|
+
onChange: (value: string) => void;
|
|
9
|
+
onSubmit: (value: string) => void;
|
|
10
|
+
isDisabled: boolean;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const InputBar: React.FC<InputBarProps> = ({
|
|
14
|
+
value,
|
|
15
|
+
onChange,
|
|
16
|
+
onSubmit,
|
|
17
|
+
isDisabled,
|
|
18
|
+
}: InputBarProps) => {
|
|
19
|
+
const separator = '─'.repeat(process.stdout.columns || 80);
|
|
20
|
+
|
|
21
|
+
return (
|
|
22
|
+
<Box flexDirection="column">
|
|
23
|
+
<Box>
|
|
24
|
+
<Text color={theme.border}>{separator}</Text>
|
|
25
|
+
</Box>
|
|
26
|
+
<Box paddingX={1}>
|
|
27
|
+
<Text bold color={theme.primary}>
|
|
28
|
+
{'> '}
|
|
29
|
+
</Text>
|
|
30
|
+
<TextInput
|
|
31
|
+
value={value}
|
|
32
|
+
onChange={onChange}
|
|
33
|
+
onSubmit={onSubmit}
|
|
34
|
+
placeholder={isDisabled ? '' : ''}
|
|
35
|
+
focus={!isDisabled}
|
|
36
|
+
showCursor={true}
|
|
37
|
+
/>
|
|
38
|
+
</Box>
|
|
39
|
+
</Box>
|
|
40
|
+
);
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
export default InputBar;
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { Text } from 'ink';
|
|
3
|
+
import { theme } from '../themes/themes.js';
|
|
4
|
+
|
|
5
|
+
interface StatusAreaProps {
|
|
6
|
+
status: 'idle' | 'typing' | 'thinking';
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
const StatusArea: React.FC<StatusAreaProps> = ({ status }: StatusAreaProps) => {
|
|
10
|
+
const statusText = {
|
|
11
|
+
idle: 'Idle',
|
|
12
|
+
typing: 'Typing...',
|
|
13
|
+
thinking: 'Thinking...',
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
return (
|
|
17
|
+
<Text color={theme.textDim} dimColor>
|
|
18
|
+
{statusText[status]}
|
|
19
|
+
</Text>
|
|
20
|
+
);
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
export default StatusArea;
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { Box, Text } from 'ink';
|
|
3
|
+
import { Theme, ThemeName } from '../types.js';
|
|
4
|
+
|
|
5
|
+
interface StatusBarProps {
|
|
6
|
+
model: string;
|
|
7
|
+
totalTokens: number;
|
|
8
|
+
themeName: ThemeName;
|
|
9
|
+
theme: Theme;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
const StatusBar: React.FC<StatusBarProps> = ({
|
|
13
|
+
model,
|
|
14
|
+
totalTokens,
|
|
15
|
+
themeName,
|
|
16
|
+
theme,
|
|
17
|
+
}) => {
|
|
18
|
+
return (
|
|
19
|
+
<Box>
|
|
20
|
+
<Text color={theme.statusBar}>
|
|
21
|
+
{model} │ tokens: {totalTokens} │ theme: {themeName}
|
|
22
|
+
</Text>
|
|
23
|
+
</Box>
|
|
24
|
+
);
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
export default StatusBar;
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import React, { useState, useEffect } from 'react';
|
|
2
|
+
import { Text } from 'ink';
|
|
3
|
+
import { theme } from '../themes/themes.js';
|
|
4
|
+
|
|
5
|
+
const ThinkingDots: React.FC = () => {
|
|
6
|
+
const [dots, setDots] = useState('');
|
|
7
|
+
|
|
8
|
+
useEffect(() => {
|
|
9
|
+
const interval = setInterval(() => {
|
|
10
|
+
setDots((prev: string) => (prev.length < 3 ? prev + '.' : ''));
|
|
11
|
+
}, 400);
|
|
12
|
+
return () => clearInterval(interval);
|
|
13
|
+
}, []);
|
|
14
|
+
|
|
15
|
+
return (
|
|
16
|
+
<Text color={theme.textDim} dimColor>
|
|
17
|
+
Thinking{dots}
|
|
18
|
+
</Text>
|
|
19
|
+
);
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
export default ThinkingDots;
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { Box, Text } from 'ink';
|
|
3
|
+
import { theme } from '../themes/themes.js';
|
|
4
|
+
|
|
5
|
+
interface TopBarProps {
|
|
6
|
+
provider?: string;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
const TopBar: React.FC<TopBarProps> = ({ provider = 'Not Connected' }: TopBarProps) => {
|
|
10
|
+
return (
|
|
11
|
+
<Box
|
|
12
|
+
flexDirection="row"
|
|
13
|
+
justifyContent="space-between"
|
|
14
|
+
paddingX={1}
|
|
15
|
+
>
|
|
16
|
+
<Box flexDirection="row">
|
|
17
|
+
<Text bold color={theme.primary}>
|
|
18
|
+
COREX
|
|
19
|
+
</Text>
|
|
20
|
+
<Text color={theme.textDim}> | </Text>
|
|
21
|
+
<Text color={theme.textPrimary}>AI Gateway</Text>
|
|
22
|
+
</Box>
|
|
23
|
+
<Box flexDirection="row">
|
|
24
|
+
<Text color={theme.textDim}>Provider: </Text>
|
|
25
|
+
<Text color={theme.highlight}>{provider}</Text>
|
|
26
|
+
</Box>
|
|
27
|
+
</Box>
|
|
28
|
+
);
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
export default TopBar;
|