-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathhelpers.go
More file actions
431 lines (379 loc) · 11.8 KB
/
helpers.go
File metadata and controls
431 lines (379 loc) · 11.8 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
package main
import (
"bufio"
"context"
"fmt"
"os"
"regexp"
"strings"
)
// handleFirstRunPull handles the first-run container pull experience
func handleFirstRunPull(ctx context.Context, container *ContainerRuntime) error {
fmt.Println()
// Use simple ASCII box for cross-platform compatibility
fmt.Println("\033[93m+-------------------------------------------------------------+\033[0m")
fmt.Println("\033[93m| First-time Setup |\033[0m")
fmt.Println("\033[93m+-------------------------------------------------------------+\033[0m")
fmt.Println()
fmt.Println("bjarne requires a validation container to check your C/C++ code")
fmt.Println("for memory errors, undefined behavior, and data races.")
fmt.Println()
fmt.Printf("Container image: \033[96m%s\033[0m\n", container.imageName)
fmt.Printf("Size: ~500MB (Ubuntu-based with Clang 21 + sanitizers)\n")
fmt.Println()
fmt.Print("Pull the validation container now? [Y/n] ")
reader := bufio.NewReader(os.Stdin)
response, err := reader.ReadString('\n')
if err != nil {
return fmt.Errorf("failed to read response: %w", err)
}
response = strings.TrimSpace(strings.ToLower(response))
if response != "" && response != "y" && response != "yes" {
return fmt.Errorf("container pull declined")
}
fmt.Println()
fmt.Println("\033[93mPulling container image...\033[0m")
fmt.Println("(This may take a few minutes on first run)")
fmt.Println()
if err := container.PullImage(ctx); err != nil {
return fmt.Errorf("failed to pull container: %w", err)
}
fmt.Println()
fmt.Println("\033[92mContainer ready!\033[0m")
return nil
}
// parseDifficulty extracts the difficulty tag from bjarne's reflection
// Returns the difficulty level (EASY, MEDIUM, COMPLEX) and the text without the tag
func parseDifficulty(text string) (string, string) {
text = strings.TrimSpace(text)
// Check for difficulty tags at the start
for _, level := range []string{"EASY", "MEDIUM", "COMPLEX"} {
tag := "[" + level + "]"
if strings.HasPrefix(text, tag) {
// Remove the tag and any following whitespace/newline
remainder := strings.TrimPrefix(text, tag)
remainder = strings.TrimLeft(remainder, " \t\n")
return level, remainder
}
}
// No tag found - default to MEDIUM (requires confirmation)
return "MEDIUM", text
}
// CodeFile represents a single source file in a multi-file project
type CodeFile struct {
Filename string
Content string
}
// extractCode extracts code from a markdown code block
// For single file responses, returns the code content
// For multi-file responses, returns all files concatenated (use extractMultipleFiles instead)
func extractCode(response string) string {
files := extractMultipleFiles(response)
if len(files) == 0 {
return ""
}
if len(files) == 1 {
return files[0].Content
}
// For backwards compatibility, return all content if multiple files
var sb strings.Builder
for i, f := range files {
if i > 0 {
sb.WriteString("\n\n")
}
sb.WriteString("// FILE: " + f.Filename + "\n")
sb.WriteString(f.Content)
}
return sb.String()
}
// extractMultipleFiles extracts multiple code files from an LLM response
// Returns a slice of CodeFile, each with filename and content
// If no // FILE: markers are found, returns single file with default name
func extractMultipleFiles(response string) []CodeFile {
// Normalize line endings (Windows \r\n to \n)
response = strings.ReplaceAll(response, "\r\n", "\n")
var files []CodeFile
// Match all code blocks: ```cpp ... ``` or ```c ... ``` or ```c++ ... ```
re := regexp.MustCompile("(?s)```(?:cpp|c\\+\\+|c)?[ \t]*\n(.*?)\n?```")
matches := re.FindAllStringSubmatch(response, -1)
if len(matches) == 0 {
// Fallback: try truncated response (no closing ```)
reOpen := regexp.MustCompile("(?s)```(?:cpp|c\\+\\+|c)[ \t]*\n(.+)")
matches = reOpen.FindAllStringSubmatch(response, -1)
if len(matches) == 0 {
return nil
}
}
for _, match := range matches {
if len(match) < 2 {
continue
}
content := strings.TrimSpace(match[1])
if content == "" {
continue
}
// Check for // FILE: marker at the start
filename := detectFilename(content)
if filename != "" {
// Remove the FILE: line from content
lines := strings.SplitN(content, "\n", 2)
if len(lines) > 1 {
content = strings.TrimSpace(lines[1])
} else {
content = ""
}
}
if content != "" {
files = append(files, CodeFile{
Filename: filename,
Content: content,
})
}
}
// If no filenames detected, assign defaults
if len(files) == 1 && files[0].Filename == "" {
files[0].Filename = "code.cpp"
} else if len(files) > 1 {
hasMain := false
for i := range files {
if files[i].Filename == "" {
// Try to detect from content
files[i].Filename = inferFilename(files[i].Content, i)
}
if files[i].Filename == "main.cpp" || strings.Contains(files[i].Content, "int main(") {
hasMain = true
}
}
// If no main.cpp, assign it to the first file with main()
if !hasMain {
for i := range files {
if strings.Contains(files[i].Content, "int main(") {
files[i].Filename = "main.cpp"
break
}
}
}
}
return files
}
// detectFilename looks for // FILE: marker at the start of content
func detectFilename(content string) string {
lines := strings.SplitN(content, "\n", 2)
if len(lines) == 0 {
return ""
}
firstLine := strings.TrimSpace(lines[0])
// Match // FILE: filename.ext or /* FILE: filename.ext */
patterns := []string{
`^//\s*FILE:\s*(\S+)`,
`^/\*\s*FILE:\s*(\S+)\s*\*/`,
}
for _, pattern := range patterns {
re := regexp.MustCompile(pattern)
matches := re.FindStringSubmatch(firstLine)
if len(matches) >= 2 {
return matches[1]
}
}
return ""
}
// inferFilename tries to guess a filename from content
func inferFilename(content string, index int) string {
// Check for #pragma once or header guards -> it's a header
if strings.Contains(content, "#pragma once") || regexp.MustCompile(`#ifndef\s+\w+_H`).MatchString(content) {
// Try to find class/struct name
classRe := regexp.MustCompile(`(?:class|struct)\s+(\w+)`)
if match := classRe.FindStringSubmatch(content); len(match) >= 2 {
return strings.ToLower(match[1]) + ".h"
}
return fmt.Sprintf("header%d.h", index)
}
// Check for main function
if strings.Contains(content, "int main(") {
return "main.cpp"
}
// Check if it looks like implementation (includes local header)
if regexp.MustCompile(`#include\s*"[^"]+\.h"`).MatchString(content) {
// Find the first local include
includeRe := regexp.MustCompile(`#include\s*"([^"]+)\.h"`)
if match := includeRe.FindStringSubmatch(content); len(match) >= 2 {
return match[1] + ".cpp"
}
}
return fmt.Sprintf("file%d.cpp", index)
}
// IsMultiFileProject checks if the extracted files represent a multi-file project
func IsMultiFileProject(files []CodeFile) bool {
if len(files) <= 1 {
return false
}
// Check if any file is a header
for _, f := range files {
if strings.HasSuffix(f.Filename, ".h") || strings.HasSuffix(f.Filename, ".hpp") {
return true
}
}
return len(files) > 1
}
// saveToFile writes code to a file
func saveToFile(filename, code string) error {
return os.WriteFile(filename, []byte(code), 0600)
}
// stripMarkdown removes common markdown formatting from text for terminal display
func stripMarkdown(text string) string {
// Remove code blocks entirely (```...```) - handles various formats:
// ```cpp\ncode```, ```\ncode```, ```cpp code```, etc.
re := regexp.MustCompile("(?s)```[a-zA-Z]*\\s*.*?```")
text = re.ReplaceAllString(text, "[code block removed]")
// Remove headers (# ## ### etc) - keep the text
re = regexp.MustCompile(`(?m)^#{1,6}\s+`)
text = re.ReplaceAllString(text, "")
// Remove horizontal rules (--- or ***)
re = regexp.MustCompile(`(?m)^[-*]{3,}\s*$`)
text = re.ReplaceAllString(text, "")
// Remove table formatting - convert to simple lines
// First, remove table separator lines (|---|---|)
re = regexp.MustCompile(`(?m)^\|[-:|\s]+\|\s*$`)
text = re.ReplaceAllString(text, "")
// Then clean up table rows (| cell | cell |) -> cell, cell
re = regexp.MustCompile(`(?m)^\|\s*`)
text = re.ReplaceAllString(text, "")
re = regexp.MustCompile(`(?m)\s*\|$`)
text = re.ReplaceAllString(text, "")
re = regexp.MustCompile(`\s*\|\s*`)
text = re.ReplaceAllString(text, " | ")
// Remove bold (**text** or __text__)
re = regexp.MustCompile(`\*\*([^*]+)\*\*`)
text = re.ReplaceAllString(text, "$1")
re = regexp.MustCompile(`__([^_]+)__`)
text = re.ReplaceAllString(text, "$1")
// Remove italic (*text* or _text_) - be careful not to match bullet points
re = regexp.MustCompile(`(?:^|[^*])\*([^*\n]+)\*(?:[^*]|$)`)
text = re.ReplaceAllString(text, "$1")
// Remove inline code (`text`)
re = regexp.MustCompile("`([^`]+)`")
text = re.ReplaceAllString(text, "$1")
// Clean up multiple blank lines
re = regexp.MustCompile(`\n{3,}`)
text = re.ReplaceAllString(text, "\n\n")
return strings.TrimSpace(text)
}
// wrapText wraps text to a specified width, preserving paragraph breaks
func wrapText(text string, width int) []string {
var result []string
paragraphs := strings.Split(text, "\n")
for _, para := range paragraphs {
para = strings.TrimSpace(para)
if para == "" {
result = append(result, "")
continue
}
// Wrap this paragraph
words := strings.Fields(para)
if len(words) == 0 {
continue
}
var line string
for _, word := range words {
if line == "" {
line = word
} else if len(line)+1+len(word) <= width {
line += " " + word
} else {
result = append(result, line)
line = word
}
}
if line != "" {
result = append(result, line)
}
}
return result
}
// containsQuestion checks if text contains a question that needs user response
// Used to determine if we should wait for user input even for EASY tasks
func containsQuestion(text string) bool {
// Simple check: any question mark means we should wait for user input
// The LLM is asking something and we should let the user respond
if strings.Contains(text, "?") {
return true
}
// Also check for common question phrases without question marks
lower := strings.ToLower(text)
questionPatterns := []string{
"correct me if",
"any corrections",
"let me know",
"before i proceed",
"before proceeding",
}
for _, pattern := range questionPatterns {
if strings.Contains(lower, pattern) {
return true
}
}
return false
}
// containsRefusal checks if the analysis text indicates a refusal to generate code
// This catches cases where the LLM says "I won't generate this" or similar
func containsRefusal(text string) bool {
text = strings.ToLower(text)
// Patterns that indicate refusal to proceed
refusalPatterns := []string{
"i won't",
"i will not",
"i cannot",
"i can't",
"i refuse",
"i'm not going to",
"not going to generate",
"not going to write",
"not going to create",
"won't generate",
"won't write",
"won't create",
"cannot generate",
"cannot write",
"cannot create",
"not appropriate",
"not able to",
"unable to",
"this isn't something",
"this is not something",
"against my",
"beyond my scope",
"outside my scope",
"not within my",
"decline to",
"must decline",
"have to decline",
"deliberately buggy",
"intentionally buggy",
"deliberately broken",
"intentionally broken",
"malicious",
"harmful",
"dangerous code",
"unsafe by design",
}
for _, pattern := range refusalPatterns {
if strings.Contains(text, pattern) {
return true
}
}
return false
}
// shortModelName extracts a readable model name from the full ID
func shortModelName(modelID string) string {
// global.anthropic.claude-haiku-4-5-20251001-v1:0 -> claude-haiku-4-5
parts := strings.Split(modelID, ".")
if len(parts) >= 3 {
modelPart := parts[2] // claude-haiku-4-5-20251001-v1:0
// Remove version suffix like -20251001-v1:0
if idx := strings.Index(modelPart, "-202"); idx > 0 {
return modelPart[:idx]
}
return modelPart
}
return modelID
}