@dcode-dev/dcode-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.
Files changed (70) hide show
  1. package/NPM_README.md +78 -0
  2. package/README.md +341 -0
  3. package/bin/dcode-bin +0 -0
  4. package/bin/dcode.js +44 -0
  5. package/cmd/agent_v2.go +448 -0
  6. package/cmd/analyze.go +97 -0
  7. package/cmd/auth.go +338 -0
  8. package/cmd/compose.go +284 -0
  9. package/cmd/context.go +111 -0
  10. package/cmd/edit.go +116 -0
  11. package/cmd/env.go +10 -0
  12. package/cmd/fix.go +145 -0
  13. package/cmd/gemini.go +20 -0
  14. package/cmd/generate.go +47 -0
  15. package/cmd/interactive.go +33 -0
  16. package/cmd/mcp.go +196 -0
  17. package/cmd/patch.go +19 -0
  18. package/cmd/providers.go +67 -0
  19. package/cmd/root.go +41 -0
  20. package/cmd/search.go +61 -0
  21. package/cmd/server.go +36 -0
  22. package/cmd/switch.go +122 -0
  23. package/cmd/terminal.go +277 -0
  24. package/go.mod +42 -0
  25. package/go.sum +86 -0
  26. package/internal/agent/agent.go +332 -0
  27. package/internal/agent/parse.go +25 -0
  28. package/internal/agents/base.go +154 -0
  29. package/internal/agents/documenter.go +77 -0
  30. package/internal/agents/generalist.go +266 -0
  31. package/internal/agents/investigator.go +60 -0
  32. package/internal/agents/registry.go +34 -0
  33. package/internal/agents/reviewer.go +67 -0
  34. package/internal/agents/tester.go +73 -0
  35. package/internal/ai/client.go +634 -0
  36. package/internal/ai/tools.go +332 -0
  37. package/internal/auth/adc.go +108 -0
  38. package/internal/auth/apikey.go +67 -0
  39. package/internal/auth/factory.go +145 -0
  40. package/internal/auth/oauth2.go +227 -0
  41. package/internal/auth/store.go +216 -0
  42. package/internal/auth/types.go +79 -0
  43. package/internal/auth/vertex.go +138 -0
  44. package/internal/config/config.go +428 -0
  45. package/internal/config/policy.go +251 -0
  46. package/internal/context/builder.go +312 -0
  47. package/internal/detector/detector.go +204 -0
  48. package/internal/diffutil/diffutil.go +30 -0
  49. package/internal/fsutil/fsutil.go +35 -0
  50. package/internal/mcp/client.go +314 -0
  51. package/internal/mcp/manager.go +221 -0
  52. package/internal/policy/policy.go +89 -0
  53. package/internal/prompt/interactive.go +338 -0
  54. package/internal/registry/agent.go +201 -0
  55. package/internal/registry/tool.go +181 -0
  56. package/internal/scheduler/scheduler.go +250 -0
  57. package/internal/server/server.go +167 -0
  58. package/internal/tools/file.go +183 -0
  59. package/internal/tools/filesystem.go +286 -0
  60. package/internal/tools/git.go +355 -0
  61. package/internal/tools/memory.go +269 -0
  62. package/internal/tools/registry.go +49 -0
  63. package/internal/tools/search.go +230 -0
  64. package/internal/tools/shell.go +84 -0
  65. package/internal/websearch/search.go +40 -0
  66. package/internal/websearch/tavily.go +79 -0
  67. package/main.go +19 -0
  68. package/package.json +57 -0
  69. package/scripts/install.js +59 -0
  70. package/scripts/uninstall.js +28 -0
@@ -0,0 +1,634 @@
1
+ package ai
2
+
3
+ import (
4
+ "bytes"
5
+ "context"
6
+ "encoding/json"
7
+ "fmt"
8
+ "io"
9
+ "net/http"
10
+ "os"
11
+ "os/exec"
12
+ "strings"
13
+ "time"
14
+ )
15
+
16
+ type Provider string
17
+
18
+ const (
19
+ ProviderOpenAI Provider = "openai"
20
+ ProviderGemini Provider = "gemini"
21
+ ProviderClaude Provider = "claude"
22
+ ProviderGitHub Provider = "github"
23
+ ProviderCopilot Provider = "copilot"
24
+ )
25
+
26
+ type Message struct {
27
+ Role string `json:"role"`
28
+ Content string `json:"content"`
29
+ }
30
+
31
+ type ImageInput struct {
32
+ Mime string `json:"mime"`
33
+ DataBase64 string `json:"data_base64"` // may be raw base64 or data-url
34
+ }
35
+
36
+ type Client struct {
37
+ Provider Provider
38
+ Model string
39
+ APIKey string
40
+ }
41
+
42
+ func NewClient() (*Client, error) {
43
+ provider := os.Getenv("AI_PROVIDER")
44
+ if provider == "" {
45
+ provider = "openai" // Default to OpenAI
46
+ }
47
+
48
+ var apiKey string
49
+ var model string
50
+
51
+ switch Provider(provider) {
52
+ case ProviderOpenAI:
53
+ apiKey = os.Getenv("OPENAI_API_KEY")
54
+ model = os.Getenv("AI_MODEL")
55
+ if model == "" {
56
+ model = "gpt-5-mini" // Latest fast model
57
+ }
58
+ case ProviderGemini:
59
+ apiKey = os.Getenv("GEMINI_API_KEY")
60
+ model = os.Getenv("AI_MODEL")
61
+ if model == "" {
62
+ model = "gemini-3-flash-preview" // Latest fast model
63
+ }
64
+ case ProviderClaude:
65
+ apiKey = os.Getenv("CLAUDE_API_KEY")
66
+ model = os.Getenv("AI_MODEL")
67
+ if model == "" {
68
+ model = "claude-3-5-sonnet-20241022" // Latest model
69
+ }
70
+ case ProviderGitHub:
71
+ // Get GitHub token from gh CLI
72
+ apiKey = os.Getenv("GITHUB_TOKEN")
73
+ if apiKey == "" {
74
+ // Try to get token from gh CLI
75
+ apiKey = getGitHubToken()
76
+ }
77
+ model = os.Getenv("AI_MODEL")
78
+ if model == "" {
79
+ model = "gpt-4o" // Default GitHub Models model
80
+ }
81
+ case ProviderCopilot:
82
+ // Get Copilot token from config
83
+ apiKey = os.Getenv("COPILOT_TOKEN")
84
+ if apiKey == "" {
85
+ // Try to get token from Copilot CLI config
86
+ apiKey = GetCopilotToken()
87
+ }
88
+ model = os.Getenv("AI_MODEL")
89
+ if model == "" {
90
+ model = "claude-sonnet-4.5" // Default Copilot model
91
+ }
92
+ default:
93
+ return nil, fmt.Errorf("unsupported provider: %s", provider)
94
+ }
95
+
96
+ if apiKey == "" {
97
+ return nil, fmt.Errorf("API key not found for provider %s", provider)
98
+ }
99
+
100
+ return &Client{
101
+ Provider: Provider(provider),
102
+ Model: model,
103
+ APIKey: apiKey,
104
+ }, nil
105
+ }
106
+
107
+ func (c *Client) Chat(messages []Message) (string, error) {
108
+ switch c.Provider {
109
+ case ProviderOpenAI:
110
+ return c.chatOpenAI(context.Background(), messages)
111
+ case ProviderGemini:
112
+ return c.chatGemini(messages)
113
+ case ProviderClaude:
114
+ return c.chatClaude(messages)
115
+ default:
116
+ return "", fmt.Errorf("unsupported provider: %s", c.Provider)
117
+ }
118
+ }
119
+
120
+ // ChatWithContext is the preferred entrypoint for new code (supports context).
121
+ func (c *Client) ChatWithContext(ctx context.Context, messages []Message) (string, error) {
122
+ switch c.Provider {
123
+ case ProviderOpenAI:
124
+ return c.chatOpenAI(ctx, messages)
125
+ case ProviderGemini:
126
+ return c.chatGemini(messages)
127
+ case ProviderClaude:
128
+ return c.chatClaude(messages)
129
+ case ProviderGitHub:
130
+ return c.chatGitHub(ctx, messages)
131
+ case ProviderCopilot:
132
+ return c.chatCopilot(ctx, messages)
133
+ default:
134
+ return "", fmt.Errorf("unsupported provider: %s", c.Provider)
135
+ }
136
+ }
137
+
138
+ // ChatWithImages supports OpenAI multimodal chat by attaching images to the last user message.
139
+ func (c *Client) ChatWithImages(ctx context.Context, messages []Message, images []ImageInput) (string, error) {
140
+ if c.Provider != ProviderOpenAI {
141
+ // other providers not yet wired for multimodal; fallback to text.
142
+ return c.ChatWithContext(ctx, messages)
143
+ }
144
+ return c.chatOpenAIWithImages(ctx, messages, images)
145
+ }
146
+
147
+ func (c *Client) chatOpenAI(ctx context.Context, messages []Message) (string, error) {
148
+ type Request struct {
149
+ Model string `json:"model"`
150
+ Messages []Message `json:"messages"`
151
+ }
152
+
153
+ type Response struct {
154
+ Choices []struct {
155
+ Message Message `json:"message"`
156
+ } `json:"choices"`
157
+ }
158
+
159
+ reqBody := Request{
160
+ Model: c.Model,
161
+ Messages: messages,
162
+ }
163
+
164
+ jsonData, err := json.Marshal(reqBody)
165
+ if err != nil {
166
+ return "", err
167
+ }
168
+
169
+ req, err := http.NewRequestWithContext(ctx, "POST", "https://api.openai.com/v1/chat/completions", bytes.NewBuffer(jsonData))
170
+ if err != nil {
171
+ return "", err
172
+ }
173
+
174
+ req.Header.Set("Content-Type", "application/json")
175
+ req.Header.Set("Authorization", "Bearer "+c.APIKey)
176
+
177
+ client := &http.Client{Timeout: 60 * time.Second}
178
+ resp, err := client.Do(req)
179
+ if err != nil {
180
+ return "", err
181
+ }
182
+ defer resp.Body.Close()
183
+
184
+ body, err := io.ReadAll(resp.Body)
185
+ if err != nil {
186
+ return "", err
187
+ }
188
+
189
+ if resp.StatusCode != http.StatusOK {
190
+ return "", fmt.Errorf("API error (%d): %s", resp.StatusCode, string(body))
191
+ }
192
+
193
+ var response Response
194
+ if err := json.Unmarshal(body, &response); err != nil {
195
+ return "", err
196
+ }
197
+
198
+ if len(response.Choices) == 0 {
199
+ return "", fmt.Errorf("no response from API")
200
+ }
201
+
202
+ return response.Choices[0].Message.Content, nil
203
+ }
204
+
205
+ func (c *Client) chatOpenAIWithImages(ctx context.Context, messages []Message, images []ImageInput) (string, error) {
206
+ // Use chat.completions multimodal format: message.content as array.
207
+ // We attach images to the last user message (or create a new one if none).
208
+ type imageURL struct {
209
+ URL string `json:"url"`
210
+ }
211
+ type contentPart struct {
212
+ Type string `json:"type"`
213
+ Text string `json:"text,omitempty"`
214
+ ImageURL *imageURL `json:"image_url,omitempty"`
215
+ }
216
+ type mmMessage struct {
217
+ Role string `json:"role"`
218
+ Content []contentPart `json:"content"`
219
+ }
220
+ type request struct {
221
+ Model string `json:"model"`
222
+ Messages []mmMessage `json:"messages"`
223
+ }
224
+ type response struct {
225
+ Choices []struct {
226
+ Message struct {
227
+ Content string `json:"content"`
228
+ } `json:"message"`
229
+ } `json:"choices"`
230
+ }
231
+
232
+ mm := make([]mmMessage, 0, len(messages))
233
+ for _, m := range messages {
234
+ mm = append(mm, mmMessage{Role: m.Role, Content: []contentPart{{Type: "text", Text: m.Content}}})
235
+ }
236
+ idx := -1
237
+ for i := len(mm) - 1; i >= 0; i-- {
238
+ if mm[i].Role == "user" {
239
+ idx = i
240
+ break
241
+ }
242
+ }
243
+ if idx == -1 {
244
+ mm = append(mm, mmMessage{Role: "user", Content: []contentPart{{Type: "text", Text: "(image attached)"}}})
245
+ idx = len(mm) - 1
246
+ }
247
+
248
+ for _, img := range images {
249
+ mime := img.Mime
250
+ if mime == "" {
251
+ mime = "image/png"
252
+ }
253
+ url := img.DataBase64
254
+ // Allow raw base64 (convert to data-url)
255
+ if !strings.HasPrefix(url, "data:") {
256
+ url = fmt.Sprintf("data:%s;base64,%s", mime, url)
257
+ }
258
+ mm[idx].Content = append(mm[idx].Content, contentPart{Type: "image_url", ImageURL: &imageURL{URL: url}})
259
+ }
260
+
261
+ reqBody := request{Model: c.Model, Messages: mm}
262
+ jsonData, err := json.Marshal(reqBody)
263
+ if err != nil {
264
+ return "", err
265
+ }
266
+
267
+ req, err := http.NewRequestWithContext(ctx, "POST", "https://api.openai.com/v1/chat/completions", bytes.NewBuffer(jsonData))
268
+ if err != nil {
269
+ return "", err
270
+ }
271
+ req.Header.Set("Content-Type", "application/json")
272
+ req.Header.Set("Authorization", "Bearer "+c.APIKey)
273
+
274
+ client := &http.Client{Timeout: 120 * time.Second}
275
+ resp, err := client.Do(req)
276
+ if err != nil {
277
+ return "", err
278
+ }
279
+ defer resp.Body.Close()
280
+
281
+ body, err := io.ReadAll(resp.Body)
282
+ if err != nil {
283
+ return "", err
284
+ }
285
+ if resp.StatusCode != http.StatusOK {
286
+ return "", fmt.Errorf("API error (%d): %s", resp.StatusCode, string(body))
287
+ }
288
+
289
+ var decoded response
290
+ if err := json.Unmarshal(body, &decoded); err != nil {
291
+ return "", err
292
+ }
293
+ if len(decoded.Choices) == 0 {
294
+ return "", fmt.Errorf("no response from API")
295
+ }
296
+ return decoded.Choices[0].Message.Content, nil
297
+ }
298
+
299
+ func (c *Client) chatGemini(messages []Message) (string, error) {
300
+ type Content struct {
301
+ Parts []struct {
302
+ Text string `json:"text"`
303
+ } `json:"parts"`
304
+ Role string `json:"role,omitempty"`
305
+ }
306
+
307
+ type Request struct {
308
+ Contents []Content `json:"contents"`
309
+ }
310
+
311
+ type Response struct {
312
+ Candidates []struct {
313
+ Content Content `json:"content"`
314
+ } `json:"candidates"`
315
+ }
316
+
317
+ // Convert messages to Gemini format
318
+ var contents []Content
319
+ for _, msg := range messages {
320
+ role := msg.Role
321
+ if role == "assistant" {
322
+ role = "model"
323
+ }
324
+ contents = append(contents, Content{
325
+ Parts: []struct {
326
+ Text string `json:"text"`
327
+ }{{Text: msg.Content}},
328
+ Role: role,
329
+ })
330
+ }
331
+
332
+ reqBody := Request{
333
+ Contents: contents,
334
+ }
335
+
336
+ jsonData, err := json.Marshal(reqBody)
337
+ if err != nil {
338
+ return "", err
339
+ }
340
+
341
+ url := fmt.Sprintf("https://generativelanguage.googleapis.com/v1beta/models/%s:generateContent?key=%s", c.Model, c.APIKey)
342
+ req, err := http.NewRequest("POST", url, bytes.NewBuffer(jsonData))
343
+ if err != nil {
344
+ return "", err
345
+ }
346
+
347
+ req.Header.Set("Content-Type", "application/json")
348
+
349
+ client := &http.Client{Timeout: 60 * time.Second}
350
+ resp, err := client.Do(req)
351
+ if err != nil {
352
+ return "", err
353
+ }
354
+ defer resp.Body.Close()
355
+
356
+ body, err := io.ReadAll(resp.Body)
357
+ if err != nil {
358
+ return "", err
359
+ }
360
+
361
+ if resp.StatusCode != http.StatusOK {
362
+ return "", fmt.Errorf("API error (%d): %s", resp.StatusCode, string(body))
363
+ }
364
+
365
+ var response Response
366
+ if err := json.Unmarshal(body, &response); err != nil {
367
+ return "", err
368
+ }
369
+
370
+ if len(response.Candidates) == 0 || len(response.Candidates[0].Content.Parts) == 0 {
371
+ return "", fmt.Errorf("no response from API")
372
+ }
373
+
374
+ return response.Candidates[0].Content.Parts[0].Text, nil
375
+ }
376
+
377
+ func (c *Client) chatClaude(messages []Message) (string, error) {
378
+ type Request struct {
379
+ Model string `json:"model"`
380
+ MaxTokens int `json:"max_tokens"`
381
+ Messages []Message `json:"messages"`
382
+ }
383
+
384
+ type Response struct {
385
+ Content []struct {
386
+ Text string `json:"text"`
387
+ } `json:"content"`
388
+ }
389
+
390
+ reqBody := Request{
391
+ Model: c.Model,
392
+ MaxTokens: 4096,
393
+ Messages: messages,
394
+ }
395
+
396
+ jsonData, err := json.Marshal(reqBody)
397
+ if err != nil {
398
+ return "", err
399
+ }
400
+
401
+ req, err := http.NewRequest("POST", "https://api.anthropic.com/v1/messages", bytes.NewBuffer(jsonData))
402
+ if err != nil {
403
+ return "", err
404
+ }
405
+
406
+ req.Header.Set("Content-Type", "application/json")
407
+ req.Header.Set("x-api-key", c.APIKey)
408
+ req.Header.Set("anthropic-version", "2023-06-01")
409
+
410
+ client := &http.Client{Timeout: 60 * time.Second}
411
+ resp, err := client.Do(req)
412
+ if err != nil {
413
+ return "", err
414
+ }
415
+ defer resp.Body.Close()
416
+
417
+ body, err := io.ReadAll(resp.Body)
418
+ if err != nil {
419
+ return "", err
420
+ }
421
+
422
+ if resp.StatusCode != http.StatusOK {
423
+ return "", fmt.Errorf("API error (%d): %s", resp.StatusCode, string(body))
424
+ }
425
+
426
+ var response Response
427
+ if err := json.Unmarshal(body, &response); err != nil {
428
+ return "", err
429
+ }
430
+
431
+ if len(response.Content) == 0 {
432
+ return "", fmt.Errorf("no response from API")
433
+ }
434
+
435
+ return response.Content[0].Text, nil
436
+ }
437
+
438
+ // GenerateText is a simple text generation method for agents
439
+ func (c *Client) GenerateText(ctx context.Context, prompt string) (string, error) {
440
+ messages := []Message{
441
+ {
442
+ Role: "user",
443
+ Content: prompt,
444
+ },
445
+ }
446
+ return c.ChatWithContext(ctx, messages)
447
+ }
448
+
449
+ // getGitHubToken retrieves the GitHub token from gh CLI
450
+ func getGitHubToken() string {
451
+ cmd := exec.Command("gh", "auth", "token")
452
+ output, err := cmd.Output()
453
+ if err != nil {
454
+ return ""
455
+ }
456
+ return strings.TrimSpace(string(output))
457
+ }
458
+
459
+ // chatGitHub calls GitHub Models API
460
+ func (c *Client) chatGitHub(ctx context.Context, messages []Message) (string, error) {
461
+ type Request struct {
462
+ Model string `json:"model"`
463
+ Messages []Message `json:"messages"`
464
+ }
465
+
466
+ type Response struct {
467
+ Choices []struct {
468
+ Message Message `json:"message"`
469
+ } `json:"choices"`
470
+ Error *struct {
471
+ Message string `json:"message"`
472
+ Type string `json:"type"`
473
+ } `json:"error,omitempty"`
474
+ }
475
+
476
+ reqBody := Request{
477
+ Model: c.Model,
478
+ Messages: messages,
479
+ }
480
+
481
+ jsonData, err := json.Marshal(reqBody)
482
+ if err != nil {
483
+ return "", err
484
+ }
485
+
486
+ // GitHub Models API endpoint
487
+ apiURL := "https://models.inference.ai.azure.com/chat/completions"
488
+
489
+ req, err := http.NewRequestWithContext(ctx, "POST", apiURL, bytes.NewBuffer(jsonData))
490
+ if err != nil {
491
+ return "", err
492
+ }
493
+
494
+ req.Header.Set("Content-Type", "application/json")
495
+ req.Header.Set("Authorization", "Bearer "+c.APIKey)
496
+
497
+ client := &http.Client{Timeout: 120 * time.Second}
498
+ resp, err := client.Do(req)
499
+ if err != nil {
500
+ return "", fmt.Errorf("GitHub API request failed: %w", err)
501
+ }
502
+ defer resp.Body.Close()
503
+
504
+ body, err := io.ReadAll(resp.Body)
505
+ if err != nil {
506
+ return "", fmt.Errorf("failed to read GitHub API response: %w", err)
507
+ }
508
+
509
+ if resp.StatusCode != http.StatusOK {
510
+ return "", fmt.Errorf("GitHub API error (%d): %s", resp.StatusCode, string(body))
511
+ }
512
+
513
+ var response Response
514
+ if err := json.Unmarshal(body, &response); err != nil {
515
+ return "", fmt.Errorf("failed to parse GitHub API response: %w", err)
516
+ }
517
+
518
+ if response.Error != nil {
519
+ return "", fmt.Errorf("GitHub API error: %s", response.Error.Message)
520
+ }
521
+
522
+ if len(response.Choices) == 0 {
523
+ return "", fmt.Errorf("no response from GitHub API")
524
+ }
525
+
526
+ return response.Choices[0].Message.Content, nil
527
+ }
528
+
529
+ // GetCopilotToken retrieves the Copilot token from ~/.copilot/config.json
530
+ func GetCopilotToken() string {
531
+ homeDir, err := os.UserHomeDir()
532
+ if err != nil {
533
+ return ""
534
+ }
535
+
536
+ configPath := homeDir + "/.copilot/config.json"
537
+ data, err := os.ReadFile(configPath)
538
+ if err != nil {
539
+ return ""
540
+ }
541
+
542
+ var config map[string]interface{}
543
+ if err := json.Unmarshal(data, &config); err != nil {
544
+ return ""
545
+ }
546
+
547
+ copilotTokens, ok := config["copilot_tokens"].(map[string]interface{})
548
+ if !ok {
549
+ return ""
550
+ }
551
+
552
+ // Get token for github.com
553
+ for key, value := range copilotTokens {
554
+ if strings.Contains(key, "github.com") {
555
+ if token, ok := value.(string); ok {
556
+ return token
557
+ }
558
+ }
559
+ }
560
+
561
+ return ""
562
+ }
563
+
564
+ // chatCopilot calls GitHub Copilot Premium API
565
+ func (c *Client) chatCopilot(ctx context.Context, messages []Message) (string, error) {
566
+ type Request struct {
567
+ Model string `json:"model"`
568
+ Messages []Message `json:"messages"`
569
+ Stream bool `json:"stream"`
570
+ }
571
+
572
+ type Response struct {
573
+ Choices []struct {
574
+ Message Message `json:"message"`
575
+ } `json:"choices"`
576
+ Error *struct {
577
+ Message string `json:"message"`
578
+ Type string `json:"type"`
579
+ } `json:"error,omitempty"`
580
+ }
581
+
582
+ reqBody := Request{
583
+ Model: c.Model,
584
+ Messages: messages,
585
+ Stream: false,
586
+ }
587
+
588
+ jsonData, err := json.Marshal(reqBody)
589
+ if err != nil {
590
+ return "", err
591
+ }
592
+
593
+ // GitHub Copilot Premium API endpoint
594
+ apiURL := "https://api.githubcopilot.com/chat/completions"
595
+
596
+ req, err := http.NewRequestWithContext(ctx, "POST", apiURL, bytes.NewBuffer(jsonData))
597
+ if err != nil {
598
+ return "", err
599
+ }
600
+
601
+ req.Header.Set("Content-Type", "application/json")
602
+ req.Header.Set("Authorization", "Bearer "+c.APIKey)
603
+
604
+ client := &http.Client{Timeout: 120 * time.Second}
605
+ resp, err := client.Do(req)
606
+ if err != nil {
607
+ return "", fmt.Errorf("Copilot API request failed: %w", err)
608
+ }
609
+ defer resp.Body.Close()
610
+
611
+ body, err := io.ReadAll(resp.Body)
612
+ if err != nil {
613
+ return "", fmt.Errorf("failed to read Copilot API response: %w", err)
614
+ }
615
+
616
+ if resp.StatusCode != http.StatusOK {
617
+ return "", fmt.Errorf("Copilot API error (%d): %s", resp.StatusCode, string(body))
618
+ }
619
+
620
+ var response Response
621
+ if err := json.Unmarshal(body, &response); err != nil {
622
+ return "", fmt.Errorf("failed to parse Copilot API response: %w", err)
623
+ }
624
+
625
+ if response.Error != nil {
626
+ return "", fmt.Errorf("Copilot API error: %s", response.Error.Message)
627
+ }
628
+
629
+ if len(response.Choices) == 0 {
630
+ return "", fmt.Errorf("no response from Copilot API")
631
+ }
632
+
633
+ return response.Choices[0].Message.Content, nil
634
+ }