snow-ai 0.1.12
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/dist/api/chat.d.ts +29 -0
- package/dist/api/chat.js +88 -0
- package/dist/api/models.d.ts +12 -0
- package/dist/api/models.js +40 -0
- package/dist/app.d.ts +6 -0
- package/dist/app.js +47 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +19 -0
- package/dist/constants/index.d.ts +18 -0
- package/dist/constants/index.js +18 -0
- package/dist/hooks/useGlobalExit.d.ts +5 -0
- package/dist/hooks/useGlobalExit.js +32 -0
- package/dist/types/index.d.ts +15 -0
- package/dist/types/index.js +1 -0
- package/dist/ui/components/ChatInput.d.ts +9 -0
- package/dist/ui/components/ChatInput.js +206 -0
- package/dist/ui/components/CommandPanel.d.ts +13 -0
- package/dist/ui/components/CommandPanel.js +22 -0
- package/dist/ui/components/Menu.d.ts +14 -0
- package/dist/ui/components/Menu.js +32 -0
- package/dist/ui/components/MessageList.d.ts +15 -0
- package/dist/ui/components/MessageList.js +16 -0
- package/dist/ui/components/PendingMessages.d.ts +6 -0
- package/dist/ui/components/PendingMessages.js +19 -0
- package/dist/ui/pages/ApiConfigScreen.d.ts +7 -0
- package/dist/ui/pages/ApiConfigScreen.js +126 -0
- package/dist/ui/pages/ChatScreen.d.ts +5 -0
- package/dist/ui/pages/ChatScreen.js +287 -0
- package/dist/ui/pages/ModelConfigScreen.d.ts +7 -0
- package/dist/ui/pages/ModelConfigScreen.js +239 -0
- package/dist/ui/pages/WelcomeScreen.d.ts +7 -0
- package/dist/ui/pages/WelcomeScreen.js +48 -0
- package/dist/utils/apiConfig.d.ts +17 -0
- package/dist/utils/apiConfig.js +86 -0
- package/dist/utils/commandExecutor.d.ts +11 -0
- package/dist/utils/commandExecutor.js +26 -0
- package/dist/utils/commands/clear.d.ts +2 -0
- package/dist/utils/commands/clear.js +12 -0
- package/dist/utils/index.d.ts +7 -0
- package/dist/utils/index.js +12 -0
- package/dist/utils/textBuffer.d.ts +52 -0
- package/dist/utils/textBuffer.js +310 -0
- package/dist/utils/textUtils.d.ts +33 -0
- package/dist/utils/textUtils.js +83 -0
- package/package.json +86 -0
- package/readme.md +9 -0
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
import React, { useState, useEffect } from 'react';
|
|
2
|
+
import { Box, Text, useInput } from 'ink';
|
|
3
|
+
import Gradient from 'ink-gradient';
|
|
4
|
+
import { Select, Alert } from '@inkjs/ui';
|
|
5
|
+
import { fetchAvailableModels, filterModels } from '../../api/models.js';
|
|
6
|
+
import { getOpenAiConfig, updateOpenAiConfig, } from '../../utils/apiConfig.js';
|
|
7
|
+
export default function ModelConfigScreen({ onBack, onSave }) {
|
|
8
|
+
const [advancedModel, setAdvancedModel] = useState('');
|
|
9
|
+
const [basicModel, setBasicModel] = useState('');
|
|
10
|
+
const [maxContextTokens, setMaxContextTokens] = useState(4000);
|
|
11
|
+
const [currentField, setCurrentField] = useState('advancedModel');
|
|
12
|
+
const [isEditing, setIsEditing] = useState(false);
|
|
13
|
+
const [models, setModels] = useState([]);
|
|
14
|
+
const [loading, setLoading] = useState(false);
|
|
15
|
+
const [error, setError] = useState('');
|
|
16
|
+
const [baseUrlMissing, setBaseUrlMissing] = useState(false);
|
|
17
|
+
const [searchTerm, setSearchTerm] = useState('');
|
|
18
|
+
useEffect(() => {
|
|
19
|
+
const config = getOpenAiConfig();
|
|
20
|
+
setAdvancedModel(config.advancedModel || '');
|
|
21
|
+
setBasicModel(config.basicModel || '');
|
|
22
|
+
setMaxContextTokens(config.maxContextTokens || 4000);
|
|
23
|
+
if (!config.baseUrl) {
|
|
24
|
+
setBaseUrlMissing(true);
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
}, []);
|
|
28
|
+
const loadModels = async () => {
|
|
29
|
+
setLoading(true);
|
|
30
|
+
setError('');
|
|
31
|
+
try {
|
|
32
|
+
const fetchedModels = await fetchAvailableModels();
|
|
33
|
+
setModels(fetchedModels);
|
|
34
|
+
}
|
|
35
|
+
catch (err) {
|
|
36
|
+
setError(err instanceof Error ? err.message : 'Failed to load models');
|
|
37
|
+
}
|
|
38
|
+
finally {
|
|
39
|
+
setLoading(false);
|
|
40
|
+
}
|
|
41
|
+
};
|
|
42
|
+
const getCurrentOptions = () => {
|
|
43
|
+
const filteredModels = filterModels(models, searchTerm);
|
|
44
|
+
return filteredModels.map(model => ({
|
|
45
|
+
label: model.id,
|
|
46
|
+
value: model.id,
|
|
47
|
+
}));
|
|
48
|
+
};
|
|
49
|
+
const getCurrentValue = () => {
|
|
50
|
+
if (currentField === 'advancedModel')
|
|
51
|
+
return advancedModel;
|
|
52
|
+
if (currentField === 'basicModel')
|
|
53
|
+
return basicModel;
|
|
54
|
+
return maxContextTokens.toString();
|
|
55
|
+
};
|
|
56
|
+
const handleModelChange = (value) => {
|
|
57
|
+
if (currentField === 'advancedModel') {
|
|
58
|
+
setAdvancedModel(value);
|
|
59
|
+
}
|
|
60
|
+
else if (currentField === 'basicModel') {
|
|
61
|
+
setBasicModel(value);
|
|
62
|
+
}
|
|
63
|
+
else if (currentField === 'maxContextTokens') {
|
|
64
|
+
const numValue = parseInt(value, 10);
|
|
65
|
+
if (!isNaN(numValue) && numValue > 0) {
|
|
66
|
+
setMaxContextTokens(numValue);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
setIsEditing(false);
|
|
70
|
+
setSearchTerm(''); // Reset search when selection is made
|
|
71
|
+
};
|
|
72
|
+
useInput((input, key) => {
|
|
73
|
+
if (baseUrlMissing) {
|
|
74
|
+
if (key.escape) {
|
|
75
|
+
onBack();
|
|
76
|
+
}
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
// Don't handle input when Select component is active
|
|
80
|
+
if (isEditing) {
|
|
81
|
+
if (currentField === 'maxContextTokens') {
|
|
82
|
+
// Handle numeric input for maxContextTokens
|
|
83
|
+
if (input && input.match(/[0-9]/)) {
|
|
84
|
+
const newValue = parseInt(maxContextTokens.toString() + input, 10);
|
|
85
|
+
if (!isNaN(newValue)) {
|
|
86
|
+
setMaxContextTokens(newValue);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
else if (key.backspace || key.delete) {
|
|
90
|
+
const currentStr = maxContextTokens.toString();
|
|
91
|
+
const newStr = currentStr.slice(0, -1);
|
|
92
|
+
const newValue = parseInt(newStr, 10);
|
|
93
|
+
if (!isNaN(newValue)) {
|
|
94
|
+
setMaxContextTokens(newValue);
|
|
95
|
+
}
|
|
96
|
+
else {
|
|
97
|
+
setMaxContextTokens(0);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
else if (key.return) {
|
|
101
|
+
// Save value, but enforce minimum of 4000
|
|
102
|
+
const finalValue = maxContextTokens < 4000 ? 4000 : maxContextTokens;
|
|
103
|
+
setMaxContextTokens(finalValue);
|
|
104
|
+
setIsEditing(false);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
else {
|
|
108
|
+
// Allow typing to filter in edit mode for model selection
|
|
109
|
+
if (input && input.match(/[a-zA-Z0-9-_.]/)) {
|
|
110
|
+
setSearchTerm(prev => prev + input);
|
|
111
|
+
}
|
|
112
|
+
else if (key.backspace || key.delete) {
|
|
113
|
+
setSearchTerm(prev => prev.slice(0, -1));
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
// Handle save/exit globally
|
|
119
|
+
if (input === 's' && (key.ctrl || key.meta)) {
|
|
120
|
+
const config = {
|
|
121
|
+
advancedModel,
|
|
122
|
+
basicModel,
|
|
123
|
+
maxContextTokens,
|
|
124
|
+
};
|
|
125
|
+
updateOpenAiConfig(config);
|
|
126
|
+
onSave();
|
|
127
|
+
}
|
|
128
|
+
else if (key.escape) {
|
|
129
|
+
const config = {
|
|
130
|
+
advancedModel,
|
|
131
|
+
basicModel,
|
|
132
|
+
maxContextTokens,
|
|
133
|
+
};
|
|
134
|
+
updateOpenAiConfig(config);
|
|
135
|
+
onBack();
|
|
136
|
+
}
|
|
137
|
+
else if (key.return) {
|
|
138
|
+
// Load models first for model fields, or enter edit mode directly for maxContextTokens
|
|
139
|
+
setSearchTerm(''); // Reset search when entering edit mode
|
|
140
|
+
if (currentField === 'maxContextTokens') {
|
|
141
|
+
setIsEditing(true);
|
|
142
|
+
}
|
|
143
|
+
else {
|
|
144
|
+
loadModels().then(() => {
|
|
145
|
+
setIsEditing(true);
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
else if (key.upArrow) {
|
|
150
|
+
if (currentField === 'basicModel') {
|
|
151
|
+
setCurrentField('advancedModel');
|
|
152
|
+
}
|
|
153
|
+
else if (currentField === 'maxContextTokens') {
|
|
154
|
+
setCurrentField('basicModel');
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
else if (key.downArrow) {
|
|
158
|
+
if (currentField === 'advancedModel') {
|
|
159
|
+
setCurrentField('basicModel');
|
|
160
|
+
}
|
|
161
|
+
else if (currentField === 'basicModel') {
|
|
162
|
+
setCurrentField('maxContextTokens');
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
});
|
|
166
|
+
if (baseUrlMissing) {
|
|
167
|
+
return (React.createElement(Box, { flexDirection: "column", padding: 1 },
|
|
168
|
+
React.createElement(Box, { marginBottom: 2, borderStyle: "double", borderColor: "cyan", paddingX: 2, paddingY: 1 },
|
|
169
|
+
React.createElement(Box, { flexDirection: "column" },
|
|
170
|
+
React.createElement(Gradient, { name: 'rainbow' }, "Model Configuration"),
|
|
171
|
+
React.createElement(Text, { color: "gray", dimColor: true }, "Configure AI models for different tasks"))),
|
|
172
|
+
React.createElement(Box, { marginBottom: 2 },
|
|
173
|
+
React.createElement(Alert, { variant: "error" }, "Base URL not configured. Please configure API settings first before setting up models.")),
|
|
174
|
+
React.createElement(Box, { flexDirection: "column" },
|
|
175
|
+
React.createElement(Alert, { variant: "info" }, "Press Esc to return to main menu"))));
|
|
176
|
+
}
|
|
177
|
+
if (loading) {
|
|
178
|
+
return (React.createElement(Box, { flexDirection: "column", padding: 1 },
|
|
179
|
+
React.createElement(Box, { marginBottom: 2, borderStyle: "double", paddingX: 2, paddingY: 1 },
|
|
180
|
+
React.createElement(Box, { flexDirection: "column" },
|
|
181
|
+
React.createElement(Gradient, { name: "rainbow" }, "Model Configuration"),
|
|
182
|
+
React.createElement(Text, { color: "gray", dimColor: true }, "Loading available models...")))));
|
|
183
|
+
}
|
|
184
|
+
if (error) {
|
|
185
|
+
return (React.createElement(Box, { flexDirection: "column", padding: 1 },
|
|
186
|
+
React.createElement(Box, { marginBottom: 2, borderStyle: "double", borderColor: "red", paddingX: 2, paddingY: 1 },
|
|
187
|
+
React.createElement(Box, { flexDirection: "column" },
|
|
188
|
+
React.createElement(Gradient, { name: 'rainbow' }, "Model Configuration"),
|
|
189
|
+
React.createElement(Text, { color: "gray", dimColor: true }, "Configure AI models for different tasks"))),
|
|
190
|
+
React.createElement(Box, { marginBottom: 2 },
|
|
191
|
+
React.createElement(Alert, { variant: "error" }, error)),
|
|
192
|
+
React.createElement(Box, { flexDirection: "column" },
|
|
193
|
+
React.createElement(Alert, { variant: "info" }, "Press Esc to return to main menu"))));
|
|
194
|
+
}
|
|
195
|
+
return (React.createElement(Box, { flexDirection: "column", padding: 1 },
|
|
196
|
+
React.createElement(Box, { marginBottom: 2, borderStyle: "double", borderColor: "cyan", paddingX: 2, paddingY: 1 },
|
|
197
|
+
React.createElement(Box, { flexDirection: "column" },
|
|
198
|
+
React.createElement(Gradient, { name: 'rainbow' }, "Model Configuration"),
|
|
199
|
+
React.createElement(Text, { color: "gray", dimColor: true }, "Configure AI models for different tasks"))),
|
|
200
|
+
React.createElement(Box, { flexDirection: "column", marginBottom: 2 },
|
|
201
|
+
React.createElement(Box, { marginBottom: 1 },
|
|
202
|
+
React.createElement(Box, { flexDirection: "column" },
|
|
203
|
+
React.createElement(Text, { color: currentField === 'advancedModel' ? 'green' : 'white' },
|
|
204
|
+
currentField === 'advancedModel' ? '➣ ' : ' ',
|
|
205
|
+
"Advanced Model (Main Work):"),
|
|
206
|
+
currentField === 'advancedModel' && isEditing && (React.createElement(Box, { marginLeft: 3 }, loading ? (React.createElement(Text, { color: "yellow" }, "Loading models...")) : (React.createElement(Box, { flexDirection: "column" },
|
|
207
|
+
searchTerm && (React.createElement(Text, { color: "cyan" },
|
|
208
|
+
"Filter: ",
|
|
209
|
+
searchTerm)),
|
|
210
|
+
React.createElement(Select, { options: getCurrentOptions(), defaultValue: getCurrentValue(), onChange: handleModelChange }))))),
|
|
211
|
+
(!isEditing || currentField !== 'advancedModel') && (React.createElement(Box, { marginLeft: 3 },
|
|
212
|
+
React.createElement(Text, { color: "gray" }, advancedModel || 'Not set'))))),
|
|
213
|
+
React.createElement(Box, { marginBottom: 1 },
|
|
214
|
+
React.createElement(Box, { flexDirection: "column" },
|
|
215
|
+
React.createElement(Text, { color: currentField === 'basicModel' ? 'green' : 'white' },
|
|
216
|
+
currentField === 'basicModel' ? '➣ ' : ' ',
|
|
217
|
+
"Basic Model (Summary & Analysis):"),
|
|
218
|
+
currentField === 'basicModel' && isEditing && (React.createElement(Box, { marginLeft: 3 }, loading ? (React.createElement(Text, { color: "yellow" }, "Loading models...")) : (React.createElement(Box, { flexDirection: "column" },
|
|
219
|
+
searchTerm && (React.createElement(Text, { color: "cyan" },
|
|
220
|
+
"Filter: ",
|
|
221
|
+
searchTerm)),
|
|
222
|
+
React.createElement(Select, { options: getCurrentOptions(), defaultValue: getCurrentValue(), onChange: handleModelChange }))))),
|
|
223
|
+
(!isEditing || currentField !== 'basicModel') && (React.createElement(Box, { marginLeft: 3 },
|
|
224
|
+
React.createElement(Text, { color: "gray" }, basicModel || 'Not set'))))),
|
|
225
|
+
React.createElement(Box, { marginBottom: 1 },
|
|
226
|
+
React.createElement(Box, { flexDirection: "column" },
|
|
227
|
+
React.createElement(Text, { color: currentField === 'maxContextTokens' ? 'green' : 'white' },
|
|
228
|
+
currentField === 'maxContextTokens' ? '➣ ' : ' ',
|
|
229
|
+
"Max Context Tokens (Auto-compress when reached):"),
|
|
230
|
+
currentField === 'maxContextTokens' && isEditing && (React.createElement(Box, { marginLeft: 3 },
|
|
231
|
+
React.createElement(Text, { color: "cyan" },
|
|
232
|
+
"Enter value: ",
|
|
233
|
+
maxContextTokens))),
|
|
234
|
+
(!isEditing || currentField !== 'maxContextTokens') && (React.createElement(Box, { marginLeft: 3 },
|
|
235
|
+
React.createElement(Text, { color: "gray" }, maxContextTokens)))))),
|
|
236
|
+
React.createElement(Box, { flexDirection: "column" }, isEditing ? (React.createElement(React.Fragment, null,
|
|
237
|
+
React.createElement(Alert, { variant: "info" }, "Editing mode: Type to filter models, \u2191\u2193 to select, Enter to confirm"))) : (React.createElement(React.Fragment, null,
|
|
238
|
+
React.createElement(Alert, { variant: "info" }, "Use \u2191\u2193 to navigate, Enter to edit, Ctrl+S or Esc to save"))))));
|
|
239
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import React, { useState } from 'react';
|
|
2
|
+
import { Box, Text } from 'ink';
|
|
3
|
+
import { Alert } from '@inkjs/ui';
|
|
4
|
+
import Gradient from 'ink-gradient';
|
|
5
|
+
import Menu from '../components/Menu.js';
|
|
6
|
+
export default function WelcomeScreen({ version = '1.0.0', onMenuSelect, }) {
|
|
7
|
+
const [infoText, setInfoText] = useState('Start a new chat conversation');
|
|
8
|
+
const menuOptions = [
|
|
9
|
+
{
|
|
10
|
+
label: 'Start',
|
|
11
|
+
value: 'chat',
|
|
12
|
+
infoText: 'Start a new chat conversation'
|
|
13
|
+
},
|
|
14
|
+
{
|
|
15
|
+
label: 'API Settings',
|
|
16
|
+
value: 'config',
|
|
17
|
+
infoText: 'Configure OpenAI API settings',
|
|
18
|
+
},
|
|
19
|
+
{
|
|
20
|
+
label: 'Model Settings',
|
|
21
|
+
value: 'models',
|
|
22
|
+
infoText: 'Configure AI models for different tasks',
|
|
23
|
+
},
|
|
24
|
+
{
|
|
25
|
+
label: 'Exit',
|
|
26
|
+
value: 'exit',
|
|
27
|
+
color: 'rgb(232, 131, 136)',
|
|
28
|
+
infoText: 'Exit the application',
|
|
29
|
+
}
|
|
30
|
+
];
|
|
31
|
+
const handleSelectionChange = (newInfoText) => {
|
|
32
|
+
setInfoText(newInfoText);
|
|
33
|
+
};
|
|
34
|
+
return (React.createElement(Box, { flexDirection: "column", padding: 1 },
|
|
35
|
+
React.createElement(Box, { marginBottom: 2, borderStyle: "double", paddingX: 2, paddingY: 1, borderColor: 'cyan' },
|
|
36
|
+
React.createElement(Box, { flexDirection: "column" },
|
|
37
|
+
React.createElement(Gradient, { name: "rainbow" },
|
|
38
|
+
React.createElement(Text, { bold: true }, "A I B O T P R O")),
|
|
39
|
+
React.createElement(Text, { color: "blue" }, "C L I"),
|
|
40
|
+
React.createElement(Text, { color: "gray", dimColor: true }, "Intelligent Command Line Assistant"),
|
|
41
|
+
React.createElement(Text, { color: "magenta", dimColor: true },
|
|
42
|
+
"Version ",
|
|
43
|
+
version))),
|
|
44
|
+
onMenuSelect && (React.createElement(Box, { marginBottom: 0 },
|
|
45
|
+
React.createElement(Menu, { options: menuOptions, onSelect: onMenuSelect, onSelectionChange: handleSelectionChange }))),
|
|
46
|
+
React.createElement(Box, { justifyContent: "space-between" },
|
|
47
|
+
React.createElement(Alert, { variant: 'info' }, infoText))));
|
|
48
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
export type RequestMethod = 'chat' | 'responses';
|
|
2
|
+
export interface ApiConfig {
|
|
3
|
+
baseUrl: string;
|
|
4
|
+
apiKey: string;
|
|
5
|
+
requestMethod: RequestMethod;
|
|
6
|
+
advancedModel?: string;
|
|
7
|
+
basicModel?: string;
|
|
8
|
+
maxContextTokens?: number;
|
|
9
|
+
}
|
|
10
|
+
export interface AppConfig {
|
|
11
|
+
openai: ApiConfig;
|
|
12
|
+
}
|
|
13
|
+
export declare function loadConfig(): AppConfig;
|
|
14
|
+
export declare function saveConfig(config: AppConfig): void;
|
|
15
|
+
export declare function updateOpenAiConfig(apiConfig: Partial<ApiConfig>): void;
|
|
16
|
+
export declare function getOpenAiConfig(): ApiConfig;
|
|
17
|
+
export declare function validateApiConfig(config: Partial<ApiConfig>): string[];
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import { homedir } from 'os';
|
|
2
|
+
import { join } from 'path';
|
|
3
|
+
import { readFileSync, writeFileSync, existsSync, mkdirSync } from 'fs';
|
|
4
|
+
const DEFAULT_CONFIG = {
|
|
5
|
+
openai: {
|
|
6
|
+
baseUrl: 'https://api.openai.com/v1',
|
|
7
|
+
apiKey: '',
|
|
8
|
+
requestMethod: 'chat',
|
|
9
|
+
advancedModel: '',
|
|
10
|
+
basicModel: '',
|
|
11
|
+
maxContextTokens: 4000,
|
|
12
|
+
},
|
|
13
|
+
};
|
|
14
|
+
const CONFIG_DIR = join(homedir(), '.aibotpro');
|
|
15
|
+
const CONFIG_FILE = join(CONFIG_DIR, 'config.json');
|
|
16
|
+
function ensureConfigDirectory() {
|
|
17
|
+
if (!existsSync(CONFIG_DIR)) {
|
|
18
|
+
mkdirSync(CONFIG_DIR, { recursive: true });
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
export function loadConfig() {
|
|
22
|
+
ensureConfigDirectory();
|
|
23
|
+
if (!existsSync(CONFIG_FILE)) {
|
|
24
|
+
saveConfig(DEFAULT_CONFIG);
|
|
25
|
+
return DEFAULT_CONFIG;
|
|
26
|
+
}
|
|
27
|
+
try {
|
|
28
|
+
const configData = readFileSync(CONFIG_FILE, 'utf8');
|
|
29
|
+
const config = JSON.parse(configData);
|
|
30
|
+
// Ensure backward compatibility by adding default requestMethod if missing
|
|
31
|
+
const mergedConfig = {
|
|
32
|
+
...DEFAULT_CONFIG,
|
|
33
|
+
...config,
|
|
34
|
+
openai: {
|
|
35
|
+
...DEFAULT_CONFIG.openai,
|
|
36
|
+
...config.openai,
|
|
37
|
+
requestMethod: (config.openai?.requestMethod === 'completions' ? 'chat' : config.openai?.requestMethod) || DEFAULT_CONFIG.openai.requestMethod,
|
|
38
|
+
},
|
|
39
|
+
};
|
|
40
|
+
return mergedConfig;
|
|
41
|
+
}
|
|
42
|
+
catch (error) {
|
|
43
|
+
return DEFAULT_CONFIG;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
export function saveConfig(config) {
|
|
47
|
+
ensureConfigDirectory();
|
|
48
|
+
try {
|
|
49
|
+
const configData = JSON.stringify(config, null, 2);
|
|
50
|
+
writeFileSync(CONFIG_FILE, configData, 'utf8');
|
|
51
|
+
}
|
|
52
|
+
catch (error) {
|
|
53
|
+
throw new Error(`Failed to save configuration: ${error}`);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
export function updateOpenAiConfig(apiConfig) {
|
|
57
|
+
const currentConfig = loadConfig();
|
|
58
|
+
const updatedConfig = {
|
|
59
|
+
...currentConfig,
|
|
60
|
+
openai: { ...currentConfig.openai, ...apiConfig },
|
|
61
|
+
};
|
|
62
|
+
saveConfig(updatedConfig);
|
|
63
|
+
}
|
|
64
|
+
export function getOpenAiConfig() {
|
|
65
|
+
const config = loadConfig();
|
|
66
|
+
return config.openai;
|
|
67
|
+
}
|
|
68
|
+
export function validateApiConfig(config) {
|
|
69
|
+
const errors = [];
|
|
70
|
+
if (config.baseUrl && !isValidUrl(config.baseUrl)) {
|
|
71
|
+
errors.push('Invalid base URL format');
|
|
72
|
+
}
|
|
73
|
+
if (config.apiKey && config.apiKey.trim().length === 0) {
|
|
74
|
+
errors.push('API key cannot be empty');
|
|
75
|
+
}
|
|
76
|
+
return errors;
|
|
77
|
+
}
|
|
78
|
+
function isValidUrl(url) {
|
|
79
|
+
try {
|
|
80
|
+
new URL(url);
|
|
81
|
+
return true;
|
|
82
|
+
}
|
|
83
|
+
catch {
|
|
84
|
+
return false;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export interface CommandResult {
|
|
2
|
+
success: boolean;
|
|
3
|
+
message?: string;
|
|
4
|
+
action?: 'clear' | 'info';
|
|
5
|
+
}
|
|
6
|
+
export interface CommandHandler {
|
|
7
|
+
execute: () => Promise<CommandResult> | CommandResult;
|
|
8
|
+
}
|
|
9
|
+
export declare function registerCommand(name: string, handler: CommandHandler): void;
|
|
10
|
+
export declare function executeCommand(commandName: string): Promise<CommandResult>;
|
|
11
|
+
export declare function getAvailableCommands(): string[];
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
const commandHandlers = {};
|
|
2
|
+
export function registerCommand(name, handler) {
|
|
3
|
+
commandHandlers[name] = handler;
|
|
4
|
+
}
|
|
5
|
+
export async function executeCommand(commandName) {
|
|
6
|
+
const handler = commandHandlers[commandName];
|
|
7
|
+
if (!handler) {
|
|
8
|
+
return {
|
|
9
|
+
success: false,
|
|
10
|
+
message: `Unknown command: ${commandName}`
|
|
11
|
+
};
|
|
12
|
+
}
|
|
13
|
+
try {
|
|
14
|
+
const result = await handler.execute();
|
|
15
|
+
return result;
|
|
16
|
+
}
|
|
17
|
+
catch (error) {
|
|
18
|
+
return {
|
|
19
|
+
success: false,
|
|
20
|
+
message: error instanceof Error ? error.message : 'Command execution failed'
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
export function getAvailableCommands() {
|
|
25
|
+
return Object.keys(commandHandlers);
|
|
26
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { Command } from '../types/index.js';
|
|
2
|
+
export declare function formatCommand(command: Command): string;
|
|
3
|
+
export declare function parseInput(input: string): {
|
|
4
|
+
command: string;
|
|
5
|
+
args: string[];
|
|
6
|
+
};
|
|
7
|
+
export declare function sanitizeInput(input: string): string;
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export function formatCommand(command) {
|
|
2
|
+
return `${command.name.padEnd(12)} ${command.description}`;
|
|
3
|
+
}
|
|
4
|
+
export function parseInput(input) {
|
|
5
|
+
const parts = input.trim().split(' ');
|
|
6
|
+
const command = parts[0] || '';
|
|
7
|
+
const args = parts.slice(1);
|
|
8
|
+
return { command, args };
|
|
9
|
+
}
|
|
10
|
+
export function sanitizeInput(input) {
|
|
11
|
+
return input.trim().replace(/[<>]/g, '');
|
|
12
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
export interface Viewport {
|
|
2
|
+
width: number;
|
|
3
|
+
height: number;
|
|
4
|
+
}
|
|
5
|
+
export interface PastePlaceholder {
|
|
6
|
+
id: string;
|
|
7
|
+
content: string;
|
|
8
|
+
lineCount: number;
|
|
9
|
+
index: number;
|
|
10
|
+
placeholder: string;
|
|
11
|
+
}
|
|
12
|
+
export declare class TextBuffer {
|
|
13
|
+
private lines;
|
|
14
|
+
private cursorRow;
|
|
15
|
+
private cursorCol;
|
|
16
|
+
private viewport;
|
|
17
|
+
private pendingUpdates;
|
|
18
|
+
private pasteStorage;
|
|
19
|
+
private pasteCounter;
|
|
20
|
+
private lastPasteTime;
|
|
21
|
+
constructor(viewport: Viewport);
|
|
22
|
+
get text(): string;
|
|
23
|
+
/**
|
|
24
|
+
* 获取完整文本,包括替换占位符为原始内容
|
|
25
|
+
*/
|
|
26
|
+
getFullText(): string;
|
|
27
|
+
get visualCursor(): [number, number];
|
|
28
|
+
get viewportVisualLines(): string[];
|
|
29
|
+
get maxWidth(): number;
|
|
30
|
+
private getCurrentLine;
|
|
31
|
+
private scheduleUpdate;
|
|
32
|
+
setText(text: string): void;
|
|
33
|
+
insert(input: string): void;
|
|
34
|
+
private findBreakPoint;
|
|
35
|
+
backspace(): void;
|
|
36
|
+
delete(): void;
|
|
37
|
+
moveLeft(): void;
|
|
38
|
+
moveRight(): void;
|
|
39
|
+
moveUp(): void;
|
|
40
|
+
moveDown(): void;
|
|
41
|
+
/**
|
|
42
|
+
* Update the viewport dimensions, useful for terminal resize handling.
|
|
43
|
+
*/
|
|
44
|
+
updateViewport(viewport: Viewport): void;
|
|
45
|
+
/**
|
|
46
|
+
* Get the character and its visual info at cursor position for proper rendering.
|
|
47
|
+
*/
|
|
48
|
+
getCharAtCursor(): {
|
|
49
|
+
char: string;
|
|
50
|
+
isWideChar: boolean;
|
|
51
|
+
};
|
|
52
|
+
}
|