Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
kardolus
GitHub Repository: kardolus/chatgpt-cli
Path: blob/main/cmd/chatgpt/utils/utils.go
2649 views
1
package utils
2
3
import (
4
"encoding/json"
5
"errors"
6
"fmt"
7
"github.com/kardolus/chatgpt-cli/api"
8
"github.com/kardolus/chatgpt-cli/internal"
9
"os"
10
"path/filepath"
11
"strings"
12
"time"
13
"unicode/utf8"
14
)
15
16
const (
17
AudioPattern = "-audio"
18
TranscribePattern = "-transcribe"
19
TTSPattern = "-tts"
20
ImagePattern = "-image"
21
O1ProPattern = "o1-pro"
22
GPT5Pattern = "gpt-5"
23
InvalidMCPPatter = "the MCP pattern has to be of the form <provider>/<plugin>[@<version>]"
24
ApifyProvider = "apify"
25
UnsupportedProvider = "only apify is currently supported"
26
LatestVersion = "latest"
27
InvalidParams = "params need to be pairs or a JSON object"
28
InvalidApifyFunction = "apify functions need to be of the form user~actor"
29
InteractiveHistoryFile = "interactive_history.txt"
30
)
31
32
func ColorToAnsi(color string) (string, string) {
33
if color == "" {
34
return "", ""
35
}
36
37
color = strings.ToLower(strings.TrimSpace(color))
38
39
reset := "\033[0m"
40
41
switch color {
42
case "red":
43
return "\033[31m", reset
44
case "green":
45
return "\033[32m", reset
46
case "yellow":
47
return "\033[33m", reset
48
case "blue":
49
return "\033[34m", reset
50
case "magenta":
51
return "\033[35m", reset
52
default:
53
return "", ""
54
}
55
}
56
57
func CreateHistoryFile(history []string) (string, error) {
58
dataHome, err := internal.GetDataHome()
59
if err != nil {
60
return "", err
61
}
62
63
fullPath := filepath.Join(dataHome, InteractiveHistoryFile)
64
65
content := strings.Join(history, "\n") + "\n"
66
if err := os.WriteFile(fullPath, []byte(content), 0644); err != nil {
67
return "", err
68
}
69
70
return fullPath, nil
71
}
72
73
func FileToString(fileName string) (string, error) {
74
bytes, err := os.ReadFile(fileName)
75
if err != nil {
76
return "", err
77
}
78
79
return string(bytes), nil
80
}
81
82
func FormatPrompt(str string, counter, usage int, now time.Time) string {
83
variables := map[string]string{
84
"%datetime": now.Format("2006-01-02 15:04:05"),
85
"%date": now.Format("2006-01-02"),
86
"%time": now.Format("15:04:05"),
87
"%counter": fmt.Sprintf("%d", counter),
88
"%usage": fmt.Sprintf("%d", usage),
89
}
90
91
// Replace placeholders in the order of longest to shortest
92
for _, key := range []string{"%datetime", "%date", "%time", "%counter", "%usage"} {
93
str = strings.ReplaceAll(str, key, variables[key])
94
}
95
96
// Ensure the last character is a space
97
if str != "" && !strings.HasSuffix(str, " ") {
98
str += " "
99
}
100
101
str = strings.ReplaceAll(str, "\\n", "\n")
102
103
return str
104
}
105
106
func IsBinary(data []byte) bool {
107
if len(data) == 0 {
108
return false
109
}
110
111
// Only check up to 512KB to avoid memory issues with large files
112
const maxBytes = 512 * 1024
113
checkSize := len(data)
114
if checkSize > maxBytes {
115
checkSize = maxBytes
116
}
117
118
// Check if the sample is valid UTF-8
119
if !utf8.Valid(data[:checkSize]) {
120
return true
121
}
122
123
// Count suspicious bytes in the sample
124
binaryCount := 0
125
for _, b := range data[:checkSize] {
126
if b == 0 {
127
return true
128
}
129
130
if b < 32 && b != 9 && b != 10 && b != 13 {
131
binaryCount++
132
}
133
}
134
135
threshold := checkSize * 10 / 100
136
return binaryCount > threshold
137
}
138
139
func ValidateFlags(model string, flags map[string]bool) error {
140
if flags["new-thread"] && (flags["set-thread"] || flags["thread"]) {
141
return errors.New("the --new-thread flag cannot be used with the --set-thread or --thread flags")
142
}
143
if flags["speak"] && !flags["output"] {
144
return errors.New("the --speak flag cannot be used without the --output flag")
145
}
146
if flags["draw"] && !flags["output"] {
147
return errors.New("the --draw flag cannot be used without the --output flag")
148
}
149
if !flags["speak"] && !flags["draw"] && flags["output"] {
150
return errors.New("the --output flag cannot be used without the --speak or --draw flag")
151
}
152
if !flags["mcp"] && flags["param"] {
153
return errors.New("the --param flag cannot be used without the --mcp flag")
154
}
155
if !flags["mcp"] && flags["params"] {
156
return errors.New("the --params flag cannot be used without the --mcp flag")
157
}
158
if flags["audio"] && !strings.Contains(model, AudioPattern) {
159
return errors.New("the --audio flag cannot be used without a compatible model, ie gpt-4o-audio-preview (see --list-models)")
160
}
161
if flags["transcribe"] && !strings.Contains(model, TranscribePattern) {
162
return errors.New("the --transcribe flag cannot be used without a compatible model, ie gpt-4o-transcribe (see --list-models)")
163
}
164
if flags["speak"] && flags["output"] && !strings.Contains(model, TTSPattern) {
165
return errors.New("the --speak and --output flags cannot be used without a compatible model, ie gpt-4o-mini-tts (see --list-models)")
166
}
167
if flags["draw"] && flags["output"] && !strings.Contains(model, ImagePattern) {
168
return errors.New("the --draw and --output flags cannot be used without a compatible model, ie gpt-image-1 (see --list-models)")
169
}
170
if flags["voice"] && !strings.Contains(model, TTSPattern) {
171
return errors.New("the --voice flag cannot be used without a compatible model, ie gpt-4o-mini-tts (see --list-models)")
172
}
173
if flags["effort"] && !(strings.Contains(model, O1ProPattern) || strings.Contains(model, GPT5Pattern)) {
174
return errors.New("the --effort flag cannot be used with non o1-pro or gpt-5 models (see --list-models)")
175
}
176
177
return nil
178
}
179
180
// ParseMCPPlugin expects input for the apify provider of the form [provider]/[user]~[actor]@[version]
181
func ParseMCPPlugin(input string) (api.MCPRequest, error) {
182
var result api.MCPRequest
183
184
fields := strings.Split(input, "/")
185
if len(fields) != 2 || fields[0] == "" || fields[1] == "" {
186
return api.MCPRequest{}, errors.New(InvalidMCPPatter)
187
}
188
189
validProviders := map[string]bool{
190
ApifyProvider: true,
191
}
192
193
if validProviders[strings.ToLower(fields[0])] {
194
result.Provider = fields[0]
195
} else {
196
return api.MCPRequest{}, errors.New(UnsupportedProvider)
197
}
198
199
function := strings.Split(fields[1], "@")
200
201
result.Function = function[0]
202
203
if result.Provider == ApifyProvider {
204
parts := strings.Split(result.Function, "~")
205
if len(parts) != 2 || parts[0] == "" || parts[1] == "" {
206
return api.MCPRequest{}, errors.New(InvalidApifyFunction)
207
}
208
}
209
210
if len(function) == 1 {
211
result.Version = LatestVersion
212
} else if len(function) == 2 {
213
result.Version = function[1]
214
}
215
216
return result, nil
217
}
218
219
func ParseParams(params ...string) (map[string]interface{}, error) {
220
result := make(map[string]interface{})
221
222
if len(params) == 1 {
223
if !isJSONObject(params[0]) && !isValidPair(params[0]) {
224
return nil, errors.New(InvalidParams)
225
}
226
if isValidPair(params[0]) {
227
k, v := parseTypedValue(params[0])
228
result[k] = v
229
return result, nil
230
}
231
// the input is valid json
232
if err := json.Unmarshal([]byte(params[0]), &result); err != nil {
233
return nil, err
234
}
235
return result, nil
236
}
237
238
for _, param := range params {
239
if !isValidPair(param) {
240
return nil, errors.New(InvalidParams)
241
}
242
k, v := parseTypedValue(param)
243
result[k] = v
244
}
245
246
return result, nil
247
}
248
249
func parseTypedValue(param string) (string, interface{}) {
250
k, raw := parsePair(param)
251
252
// Try to unmarshal the value as JSON
253
var parsed interface{}
254
if err := json.Unmarshal([]byte(raw), &parsed); err == nil {
255
return k, parsed
256
}
257
258
// Fallback to treating it as a string
259
return k, raw
260
}
261
262
func isJSONObject(s string) bool {
263
var js map[string]interface{}
264
return json.Unmarshal([]byte(s), &js) == nil
265
}
266
267
func isValidPair(s string) bool {
268
pairs := strings.Split(s, "=")
269
270
if len(pairs) == 2 && pairs[0] != "" && pairs[1] != "" {
271
return true
272
}
273
274
return false
275
}
276
277
func parsePair(s string) (string, string) {
278
pairs := strings.Split(s, "=")
279
return pairs[0], pairs[1]
280
}
281
282