@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.
- package/NPM_README.md +78 -0
- package/README.md +341 -0
- package/bin/dcode-bin +0 -0
- package/bin/dcode.js +44 -0
- package/cmd/agent_v2.go +448 -0
- package/cmd/analyze.go +97 -0
- package/cmd/auth.go +338 -0
- package/cmd/compose.go +284 -0
- package/cmd/context.go +111 -0
- package/cmd/edit.go +116 -0
- package/cmd/env.go +10 -0
- package/cmd/fix.go +145 -0
- package/cmd/gemini.go +20 -0
- package/cmd/generate.go +47 -0
- package/cmd/interactive.go +33 -0
- package/cmd/mcp.go +196 -0
- package/cmd/patch.go +19 -0
- package/cmd/providers.go +67 -0
- package/cmd/root.go +41 -0
- package/cmd/search.go +61 -0
- package/cmd/server.go +36 -0
- package/cmd/switch.go +122 -0
- package/cmd/terminal.go +277 -0
- package/go.mod +42 -0
- package/go.sum +86 -0
- package/internal/agent/agent.go +332 -0
- package/internal/agent/parse.go +25 -0
- package/internal/agents/base.go +154 -0
- package/internal/agents/documenter.go +77 -0
- package/internal/agents/generalist.go +266 -0
- package/internal/agents/investigator.go +60 -0
- package/internal/agents/registry.go +34 -0
- package/internal/agents/reviewer.go +67 -0
- package/internal/agents/tester.go +73 -0
- package/internal/ai/client.go +634 -0
- package/internal/ai/tools.go +332 -0
- package/internal/auth/adc.go +108 -0
- package/internal/auth/apikey.go +67 -0
- package/internal/auth/factory.go +145 -0
- package/internal/auth/oauth2.go +227 -0
- package/internal/auth/store.go +216 -0
- package/internal/auth/types.go +79 -0
- package/internal/auth/vertex.go +138 -0
- package/internal/config/config.go +428 -0
- package/internal/config/policy.go +251 -0
- package/internal/context/builder.go +312 -0
- package/internal/detector/detector.go +204 -0
- package/internal/diffutil/diffutil.go +30 -0
- package/internal/fsutil/fsutil.go +35 -0
- package/internal/mcp/client.go +314 -0
- package/internal/mcp/manager.go +221 -0
- package/internal/policy/policy.go +89 -0
- package/internal/prompt/interactive.go +338 -0
- package/internal/registry/agent.go +201 -0
- package/internal/registry/tool.go +181 -0
- package/internal/scheduler/scheduler.go +250 -0
- package/internal/server/server.go +167 -0
- package/internal/tools/file.go +183 -0
- package/internal/tools/filesystem.go +286 -0
- package/internal/tools/git.go +355 -0
- package/internal/tools/memory.go +269 -0
- package/internal/tools/registry.go +49 -0
- package/internal/tools/search.go +230 -0
- package/internal/tools/shell.go +84 -0
- package/internal/websearch/search.go +40 -0
- package/internal/websearch/tavily.go +79 -0
- package/main.go +19 -0
- package/package.json +57 -0
- package/scripts/install.js +59 -0
- package/scripts/uninstall.js +28 -0
|
@@ -0,0 +1,332 @@
|
|
|
1
|
+
package ai
|
|
2
|
+
|
|
3
|
+
import (
|
|
4
|
+
"bufio"
|
|
5
|
+
"bytes"
|
|
6
|
+
"context"
|
|
7
|
+
"encoding/json"
|
|
8
|
+
"fmt"
|
|
9
|
+
"io"
|
|
10
|
+
"net/http"
|
|
11
|
+
"time"
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
// Tool represents a function/tool schema for Gemini
|
|
15
|
+
type Tool struct {
|
|
16
|
+
FunctionDeclarations []FunctionDeclaration `json:"functionDeclarations"`
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
// FunctionDeclaration defines a function schema
|
|
20
|
+
type FunctionDeclaration struct {
|
|
21
|
+
Name string `json:"name"`
|
|
22
|
+
Description string `json:"description"`
|
|
23
|
+
Parameters map[string]interface{} `json:"parameters"`
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// FunctionCall represents a function call from the AI
|
|
27
|
+
type FunctionCall struct {
|
|
28
|
+
Name string `json:"name"`
|
|
29
|
+
Args map[string]interface{} `json:"args"`
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// FunctionResponse represents the result of a function call
|
|
33
|
+
type FunctionResponse struct {
|
|
34
|
+
Name string `json:"name"`
|
|
35
|
+
Response map[string]interface{} `json:"response"`
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// ChatWithTools calls Gemini with function calling support
|
|
39
|
+
func (c *Client) ChatWithTools(ctx context.Context, messages []Message, tools []Tool) (*ToolResponse, error) {
|
|
40
|
+
if c.Provider != ProviderGemini {
|
|
41
|
+
// Fallback for non-Gemini providers
|
|
42
|
+
resp, err := c.ChatWithContext(ctx, messages)
|
|
43
|
+
return &ToolResponse{
|
|
44
|
+
Text: resp,
|
|
45
|
+
FunctionCalls: nil,
|
|
46
|
+
}, err
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
type Part struct {
|
|
50
|
+
Text string `json:"text,omitempty"`
|
|
51
|
+
FunctionCall *FunctionCall `json:"functionCall,omitempty"`
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
type Content struct {
|
|
55
|
+
Parts []Part `json:"parts"`
|
|
56
|
+
Role string `json:"role,omitempty"`
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
type Request struct {
|
|
60
|
+
Contents []Content `json:"contents"`
|
|
61
|
+
Tools []Tool `json:"tools,omitempty"`
|
|
62
|
+
SystemInstruction *Content `json:"systemInstruction,omitempty"`
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
type Response struct {
|
|
66
|
+
Candidates []struct {
|
|
67
|
+
Content Content `json:"content"`
|
|
68
|
+
FinishReason string `json:"finishReason,omitempty"`
|
|
69
|
+
SafetyRatings []struct {
|
|
70
|
+
Category string `json:"category"`
|
|
71
|
+
Probability string `json:"probability"`
|
|
72
|
+
} `json:"safetyRatings,omitempty"`
|
|
73
|
+
} `json:"candidates"`
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// Convert messages to Gemini format
|
|
77
|
+
var contents []Content
|
|
78
|
+
var systemInstruction *Content
|
|
79
|
+
|
|
80
|
+
for _, msg := range messages {
|
|
81
|
+
role := msg.Role
|
|
82
|
+
if role == "system" {
|
|
83
|
+
// System messages go in systemInstruction field
|
|
84
|
+
systemInstruction = &Content{
|
|
85
|
+
Parts: []Part{{Text: msg.Content}},
|
|
86
|
+
}
|
|
87
|
+
continue
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
if role == "assistant" {
|
|
91
|
+
role = "model"
|
|
92
|
+
} else if role == "tool" || role == "function" {
|
|
93
|
+
// Tool results become user role (Gemini doesn't accept "tool" or "function")
|
|
94
|
+
role = "user"
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
contents = append(contents, Content{
|
|
98
|
+
Parts: []Part{{Text: msg.Content}},
|
|
99
|
+
Role: role,
|
|
100
|
+
})
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
reqBody := Request{
|
|
104
|
+
Contents: contents,
|
|
105
|
+
Tools: tools,
|
|
106
|
+
SystemInstruction: systemInstruction,
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
jsonData, err := json.Marshal(reqBody)
|
|
110
|
+
if err != nil {
|
|
111
|
+
return nil, fmt.Errorf("failed to marshal request: %w", err)
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
url := fmt.Sprintf("https://generativelanguage.googleapis.com/v1beta/models/%s:generateContent?key=%s", c.Model, c.APIKey)
|
|
115
|
+
req, err := http.NewRequestWithContext(ctx, "POST", url, bytes.NewBuffer(jsonData))
|
|
116
|
+
if err != nil {
|
|
117
|
+
return nil, err
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
req.Header.Set("Content-Type", "application/json")
|
|
121
|
+
|
|
122
|
+
client := &http.Client{Timeout: 90 * time.Second}
|
|
123
|
+
resp, err := client.Do(req)
|
|
124
|
+
if err != nil {
|
|
125
|
+
return nil, err
|
|
126
|
+
}
|
|
127
|
+
defer resp.Body.Close()
|
|
128
|
+
|
|
129
|
+
body, err := io.ReadAll(resp.Body)
|
|
130
|
+
if err != nil {
|
|
131
|
+
return nil, err
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
if resp.StatusCode != http.StatusOK {
|
|
135
|
+
return nil, fmt.Errorf("API error (%d): %s", resp.StatusCode, string(body))
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
var response Response
|
|
139
|
+
if err := json.Unmarshal(body, &response); err != nil {
|
|
140
|
+
return nil, fmt.Errorf("failed to unmarshal response: %w", err)
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
if len(response.Candidates) == 0 {
|
|
144
|
+
return nil, fmt.Errorf("no response from API")
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
candidate := response.Candidates[0]
|
|
148
|
+
|
|
149
|
+
// Extract text and function calls
|
|
150
|
+
var text string
|
|
151
|
+
var functionCalls []FunctionCall
|
|
152
|
+
|
|
153
|
+
for _, part := range candidate.Content.Parts {
|
|
154
|
+
if part.Text != "" {
|
|
155
|
+
text += part.Text
|
|
156
|
+
}
|
|
157
|
+
if part.FunctionCall != nil {
|
|
158
|
+
functionCalls = append(functionCalls, *part.FunctionCall)
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
return &ToolResponse{
|
|
163
|
+
Text: text,
|
|
164
|
+
FunctionCalls: functionCalls,
|
|
165
|
+
FinishReason: candidate.FinishReason,
|
|
166
|
+
}, nil
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// ToolResponse contains the response with potential function calls
|
|
170
|
+
type ToolResponse struct {
|
|
171
|
+
Text string
|
|
172
|
+
FunctionCalls []FunctionCall
|
|
173
|
+
FinishReason string
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// ChatWithToolsStream calls Gemini with streaming and function calling
|
|
177
|
+
func (c *Client) ChatWithToolsStream(ctx context.Context, messages []Message, tools []Tool, onChunk func(string)) (*ToolResponse, error) {
|
|
178
|
+
if c.Provider != ProviderGemini {
|
|
179
|
+
// Fallback for non-Gemini providers
|
|
180
|
+
resp, err := c.ChatWithContext(ctx, messages)
|
|
181
|
+
if onChunk != nil && resp != "" {
|
|
182
|
+
onChunk(resp)
|
|
183
|
+
}
|
|
184
|
+
return &ToolResponse{
|
|
185
|
+
Text: resp,
|
|
186
|
+
FunctionCalls: nil,
|
|
187
|
+
}, err
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
type Part struct {
|
|
191
|
+
Text string `json:"text,omitempty"`
|
|
192
|
+
FunctionCall *FunctionCall `json:"functionCall,omitempty"`
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
type Content struct {
|
|
196
|
+
Parts []Part `json:"parts"`
|
|
197
|
+
Role string `json:"role,omitempty"`
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
type Request struct {
|
|
201
|
+
Contents []Content `json:"contents"`
|
|
202
|
+
Tools []Tool `json:"tools,omitempty"`
|
|
203
|
+
SystemInstruction *Content `json:"systemInstruction,omitempty"`
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
// Convert messages to Gemini format
|
|
207
|
+
var contents []Content
|
|
208
|
+
var systemInstruction *Content
|
|
209
|
+
|
|
210
|
+
for _, msg := range messages {
|
|
211
|
+
role := msg.Role
|
|
212
|
+
if role == "system" {
|
|
213
|
+
// System messages go in systemInstruction field
|
|
214
|
+
systemInstruction = &Content{
|
|
215
|
+
Parts: []Part{{Text: msg.Content}},
|
|
216
|
+
}
|
|
217
|
+
continue
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
if role == "assistant" {
|
|
221
|
+
role = "model"
|
|
222
|
+
} else if role == "tool" || role == "function" {
|
|
223
|
+
// Tool results become user role (Gemini doesn't accept "tool" or "function")
|
|
224
|
+
role = "user"
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
contents = append(contents, Content{
|
|
228
|
+
Parts: []Part{{Text: msg.Content}},
|
|
229
|
+
Role: role,
|
|
230
|
+
})
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
reqBody := Request{
|
|
234
|
+
Contents: contents,
|
|
235
|
+
Tools: tools,
|
|
236
|
+
SystemInstruction: systemInstruction,
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
jsonData, err := json.Marshal(reqBody)
|
|
240
|
+
if err != nil {
|
|
241
|
+
return nil, fmt.Errorf("failed to marshal request: %w", err)
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
// Use streaming endpoint
|
|
245
|
+
url := fmt.Sprintf("https://generativelanguage.googleapis.com/v1beta/models/%s:streamGenerateContent?key=%s&alt=sse", c.Model, c.APIKey)
|
|
246
|
+
req, err := http.NewRequestWithContext(ctx, "POST", url, bytes.NewBuffer(jsonData))
|
|
247
|
+
if err != nil {
|
|
248
|
+
return nil, err
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
req.Header.Set("Content-Type", "application/json")
|
|
252
|
+
req.Header.Set("Accept", "text/event-stream")
|
|
253
|
+
|
|
254
|
+
client := &http.Client{Timeout: 120 * time.Second}
|
|
255
|
+
resp, err := client.Do(req)
|
|
256
|
+
if err != nil {
|
|
257
|
+
return nil, err
|
|
258
|
+
}
|
|
259
|
+
defer resp.Body.Close()
|
|
260
|
+
|
|
261
|
+
if resp.StatusCode != http.StatusOK {
|
|
262
|
+
body, _ := io.ReadAll(resp.Body)
|
|
263
|
+
return nil, fmt.Errorf("API error (%d): %s", resp.StatusCode, string(body))
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
// Parse SSE stream
|
|
267
|
+
var fullText string
|
|
268
|
+
var functionCalls []FunctionCall
|
|
269
|
+
var finishReason string
|
|
270
|
+
|
|
271
|
+
scanner := bufio.NewScanner(resp.Body)
|
|
272
|
+
for scanner.Scan() {
|
|
273
|
+
line := scanner.Text()
|
|
274
|
+
|
|
275
|
+
// Skip empty lines and comments
|
|
276
|
+
if line == "" || line[0] == ':' {
|
|
277
|
+
continue
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
// Parse data: lines
|
|
281
|
+
if len(line) > 6 && line[:6] == "data: " {
|
|
282
|
+
data := line[6:]
|
|
283
|
+
|
|
284
|
+
// Parse JSON chunk
|
|
285
|
+
var chunk struct {
|
|
286
|
+
Candidates []struct {
|
|
287
|
+
Content struct {
|
|
288
|
+
Parts []Part `json:"parts"`
|
|
289
|
+
} `json:"content"`
|
|
290
|
+
FinishReason string `json:"finishReason,omitempty"`
|
|
291
|
+
} `json:"candidates"`
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
if err := json.Unmarshal([]byte(data), &chunk); err != nil {
|
|
295
|
+
continue
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
if len(chunk.Candidates) == 0 {
|
|
299
|
+
continue
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
candidate := chunk.Candidates[0]
|
|
303
|
+
|
|
304
|
+
// Extract text and function calls
|
|
305
|
+
for _, part := range candidate.Content.Parts {
|
|
306
|
+
if part.Text != "" {
|
|
307
|
+
fullText += part.Text
|
|
308
|
+
if onChunk != nil {
|
|
309
|
+
onChunk(part.Text)
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
if part.FunctionCall != nil {
|
|
313
|
+
functionCalls = append(functionCalls, *part.FunctionCall)
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
if candidate.FinishReason != "" {
|
|
318
|
+
finishReason = candidate.FinishReason
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
if err := scanner.Err(); err != nil {
|
|
324
|
+
return nil, fmt.Errorf("error reading stream: %w", err)
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
return &ToolResponse{
|
|
328
|
+
Text: fullText,
|
|
329
|
+
FunctionCalls: functionCalls,
|
|
330
|
+
FinishReason: finishReason,
|
|
331
|
+
}, nil
|
|
332
|
+
}
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
package auth
|
|
2
|
+
|
|
3
|
+
import (
|
|
4
|
+
"context"
|
|
5
|
+
"fmt"
|
|
6
|
+
|
|
7
|
+
"cloud.google.com/go/compute/metadata"
|
|
8
|
+
"golang.org/x/oauth2/google"
|
|
9
|
+
)
|
|
10
|
+
|
|
11
|
+
// ADCAuth implements Application Default Credentials authentication
|
|
12
|
+
type ADCAuth struct {
|
|
13
|
+
provider string
|
|
14
|
+
scopes []string
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
// NewADCAuth creates a new ADC authenticator
|
|
18
|
+
func NewADCAuth(provider string, scopes []string) *ADCAuth {
|
|
19
|
+
if len(scopes) == 0 {
|
|
20
|
+
scopes = []string{
|
|
21
|
+
"https://www.googleapis.com/auth/cloud-platform",
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
return &ADCAuth{
|
|
26
|
+
provider: provider,
|
|
27
|
+
scopes: scopes,
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// GetToken returns a valid access token from ADC
|
|
32
|
+
func (a *ADCAuth) GetToken(ctx context.Context) (string, error) {
|
|
33
|
+
// Try to get credentials from ADC
|
|
34
|
+
creds, err := google.FindDefaultCredentials(ctx, a.scopes...)
|
|
35
|
+
if err != nil {
|
|
36
|
+
return "", fmt.Errorf("failed to find default credentials: %w", err)
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// Get token
|
|
40
|
+
token, err := creds.TokenSource.Token()
|
|
41
|
+
if err != nil {
|
|
42
|
+
return "", fmt.Errorf("failed to get token: %w", err)
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
return token.AccessToken, nil
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// Refresh is handled automatically by the Google libraries
|
|
49
|
+
func (a *ADCAuth) Refresh(ctx context.Context) error {
|
|
50
|
+
// ADC handles refresh automatically
|
|
51
|
+
return nil
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// Validate checks if ADC credentials are available
|
|
55
|
+
func (a *ADCAuth) Validate(ctx context.Context) error {
|
|
56
|
+
_, err := google.FindDefaultCredentials(ctx, a.scopes...)
|
|
57
|
+
if err != nil {
|
|
58
|
+
return fmt.Errorf("ADC not available: %w", err)
|
|
59
|
+
}
|
|
60
|
+
return nil
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// GetType returns the auth type
|
|
64
|
+
func (a *ADCAuth) GetType() AuthType {
|
|
65
|
+
return AuthTypeADC
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// GetProvider returns the provider name
|
|
69
|
+
func (a *ADCAuth) GetProvider() string {
|
|
70
|
+
return a.provider
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// Revoke is not applicable for ADC
|
|
74
|
+
func (a *ADCAuth) Revoke(ctx context.Context) error {
|
|
75
|
+
return fmt.Errorf("cannot revoke ADC credentials")
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// GetTokenInfo returns token information
|
|
79
|
+
func (a *ADCAuth) GetTokenInfo(ctx context.Context) (*TokenInfo, error) {
|
|
80
|
+
creds, err := google.FindDefaultCredentials(ctx, a.scopes...)
|
|
81
|
+
if err != nil {
|
|
82
|
+
return &TokenInfo{Valid: false, Provider: a.provider}, nil
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
token, err := creds.TokenSource.Token()
|
|
86
|
+
if err != nil {
|
|
87
|
+
return &TokenInfo{Valid: false, Provider: a.provider}, nil
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// Try to get user info from metadata server (if on GCE)
|
|
91
|
+
var user string
|
|
92
|
+
if metadata.OnGCE() {
|
|
93
|
+
user, _ = metadata.Email("")
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
return &TokenInfo{
|
|
97
|
+
Valid: token.Valid(),
|
|
98
|
+
ExpiresAt: token.Expiry,
|
|
99
|
+
Provider: a.provider,
|
|
100
|
+
User: user,
|
|
101
|
+
}, nil
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// IsAvailable checks if ADC is available in the current environment
|
|
105
|
+
func IsADCAvailable(ctx context.Context) bool {
|
|
106
|
+
_, err := google.FindDefaultCredentials(ctx)
|
|
107
|
+
return err == nil
|
|
108
|
+
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
package auth
|
|
2
|
+
|
|
3
|
+
import (
|
|
4
|
+
"context"
|
|
5
|
+
"fmt"
|
|
6
|
+
"time"
|
|
7
|
+
)
|
|
8
|
+
|
|
9
|
+
// APIKeyAuth implements authentication using API keys
|
|
10
|
+
type APIKeyAuth struct {
|
|
11
|
+
apiKey string
|
|
12
|
+
provider string
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
// NewAPIKeyAuth creates a new API key authenticator
|
|
16
|
+
func NewAPIKeyAuth(provider, apiKey string) *APIKeyAuth {
|
|
17
|
+
return &APIKeyAuth{
|
|
18
|
+
provider: provider,
|
|
19
|
+
apiKey: apiKey,
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// GetToken returns the API key
|
|
24
|
+
func (a *APIKeyAuth) GetToken(ctx context.Context) (string, error) {
|
|
25
|
+
if a.apiKey == "" {
|
|
26
|
+
return "", fmt.Errorf("API key not set for provider: %s", a.provider)
|
|
27
|
+
}
|
|
28
|
+
return a.apiKey, nil
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// Refresh is a no-op for API keys
|
|
32
|
+
func (a *APIKeyAuth) Refresh(ctx context.Context) error {
|
|
33
|
+
return nil // API keys don't need refresh
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// Validate checks if the API key is set
|
|
37
|
+
func (a *APIKeyAuth) Validate(ctx context.Context) error {
|
|
38
|
+
if a.apiKey == "" {
|
|
39
|
+
return fmt.Errorf("API key not set")
|
|
40
|
+
}
|
|
41
|
+
return nil
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// GetType returns the auth type
|
|
45
|
+
func (a *APIKeyAuth) GetType() AuthType {
|
|
46
|
+
return AuthTypeAPIKey
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// GetProvider returns the provider name
|
|
50
|
+
func (a *APIKeyAuth) GetProvider() string {
|
|
51
|
+
return a.provider
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// Revoke is a no-op for API keys
|
|
55
|
+
func (a *APIKeyAuth) Revoke(ctx context.Context) error {
|
|
56
|
+
a.apiKey = ""
|
|
57
|
+
return nil
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// GetTokenInfo returns token information
|
|
61
|
+
func (a *APIKeyAuth) GetTokenInfo(ctx context.Context) (*TokenInfo, error) {
|
|
62
|
+
return &TokenInfo{
|
|
63
|
+
Valid: a.apiKey != "",
|
|
64
|
+
Provider: a.provider,
|
|
65
|
+
ExpiresAt: time.Time{}, // API keys don't expire
|
|
66
|
+
}, nil
|
|
67
|
+
}
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
package auth
|
|
2
|
+
|
|
3
|
+
import (
|
|
4
|
+
"context"
|
|
5
|
+
"fmt"
|
|
6
|
+
"os"
|
|
7
|
+
|
|
8
|
+
"github.com/ddhanush1/dcode/internal/config"
|
|
9
|
+
)
|
|
10
|
+
|
|
11
|
+
// Factory creates authenticators based on configuration
|
|
12
|
+
type Factory struct {
|
|
13
|
+
store CredentialStore
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
// NewFactory creates a new auth factory
|
|
17
|
+
func NewFactory(store CredentialStore) *Factory {
|
|
18
|
+
if store == nil {
|
|
19
|
+
store = NewKeyringStore()
|
|
20
|
+
}
|
|
21
|
+
return &Factory{
|
|
22
|
+
store: store,
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// CreateFromConfig creates an authenticator from config
|
|
27
|
+
func (f *Factory) CreateFromConfig(cfg *config.Config) (Authenticator, error) {
|
|
28
|
+
authType := AuthType(cfg.Auth.Type)
|
|
29
|
+
provider := cfg.Auth.Provider
|
|
30
|
+
|
|
31
|
+
switch authType {
|
|
32
|
+
case AuthTypeAPIKey:
|
|
33
|
+
apiKey := cfg.Auth.APIKey
|
|
34
|
+
if apiKey == "" {
|
|
35
|
+
// Try to get from environment
|
|
36
|
+
apiKey = getAPIKeyFromEnv(provider)
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
if apiKey == "" {
|
|
40
|
+
return nil, fmt.Errorf("API key not found for provider: %s", provider)
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
return NewAPIKeyAuth(provider, apiKey), nil
|
|
44
|
+
|
|
45
|
+
case AuthTypeOAuth2:
|
|
46
|
+
// For OAuth2, we need client credentials
|
|
47
|
+
clientID := os.Getenv("GOOGLE_CLIENT_ID")
|
|
48
|
+
clientSecret := os.Getenv("GOOGLE_CLIENT_SECRET")
|
|
49
|
+
|
|
50
|
+
if clientID == "" || clientSecret == "" {
|
|
51
|
+
return nil, fmt.Errorf("OAuth2 requires GOOGLE_CLIENT_ID and GOOGLE_CLIENT_SECRET")
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
scopes := []string{
|
|
55
|
+
"https://www.googleapis.com/auth/userinfo.email",
|
|
56
|
+
"https://www.googleapis.com/auth/cloud-platform",
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
return NewOAuth2Auth(provider, clientID, clientSecret, scopes, f.store), nil
|
|
60
|
+
|
|
61
|
+
case AuthTypeADC:
|
|
62
|
+
scopes := []string{
|
|
63
|
+
"https://www.googleapis.com/auth/cloud-platform",
|
|
64
|
+
}
|
|
65
|
+
return NewADCAuth(provider, scopes), nil
|
|
66
|
+
|
|
67
|
+
case AuthTypeVertexAI:
|
|
68
|
+
project := cfg.Auth.Project
|
|
69
|
+
if project == "" {
|
|
70
|
+
project = os.Getenv("GOOGLE_CLOUD_PROJECT")
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
location := cfg.Auth.Location
|
|
74
|
+
if location == "" {
|
|
75
|
+
location = os.Getenv("GOOGLE_CLOUD_LOCATION")
|
|
76
|
+
if location == "" {
|
|
77
|
+
location = "us-central1"
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
apiKey := os.Getenv("GOOGLE_API_KEY")
|
|
82
|
+
|
|
83
|
+
return NewVertexAIAuth(provider, project, location, apiKey, f.store), nil
|
|
84
|
+
|
|
85
|
+
default:
|
|
86
|
+
return nil, fmt.Errorf("unsupported auth type: %s", authType)
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// CreateFromProvider creates an authenticator for a specific provider
|
|
91
|
+
func (f *Factory) CreateFromProvider(provider string) (Authenticator, error) {
|
|
92
|
+
// Try to load existing credentials
|
|
93
|
+
creds, err := f.store.Load(provider)
|
|
94
|
+
if err == nil {
|
|
95
|
+
// Create authenticator based on stored credentials
|
|
96
|
+
switch creds.Type {
|
|
97
|
+
case AuthTypeAPIKey:
|
|
98
|
+
return NewAPIKeyAuth(provider, creds.Token), nil
|
|
99
|
+
case AuthTypeOAuth2:
|
|
100
|
+
// OAuth2 will load token from store
|
|
101
|
+
clientID := os.Getenv("GOOGLE_CLIENT_ID")
|
|
102
|
+
clientSecret := os.Getenv("GOOGLE_CLIENT_SECRET")
|
|
103
|
+
scopes := []string{"https://www.googleapis.com/auth/cloud-platform"}
|
|
104
|
+
return NewOAuth2Auth(provider, clientID, clientSecret, scopes, f.store), nil
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// Fall back to API key from environment
|
|
109
|
+
apiKey := getAPIKeyFromEnv(provider)
|
|
110
|
+
if apiKey != "" {
|
|
111
|
+
return NewAPIKeyAuth(provider, apiKey), nil
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// Try ADC
|
|
115
|
+
ctx := context.Background()
|
|
116
|
+
if IsADCAvailable(ctx) {
|
|
117
|
+
return NewADCAuth(provider, []string{"https://www.googleapis.com/auth/cloud-platform"}), nil
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
return nil, fmt.Errorf("no credentials found for provider: %s", provider)
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// getAPIKeyFromEnv gets API key from environment based on provider
|
|
124
|
+
func getAPIKeyFromEnv(provider string) string {
|
|
125
|
+
switch provider {
|
|
126
|
+
case "openai":
|
|
127
|
+
return os.Getenv("OPENAI_API_KEY")
|
|
128
|
+
case "gemini":
|
|
129
|
+
key := os.Getenv("GEMINI_API_KEY")
|
|
130
|
+
if key == "" {
|
|
131
|
+
key = os.Getenv("GOOGLE_API_KEY")
|
|
132
|
+
}
|
|
133
|
+
return key
|
|
134
|
+
case "claude", "anthropic":
|
|
135
|
+
return os.Getenv("ANTHROPIC_API_KEY")
|
|
136
|
+
default:
|
|
137
|
+
return ""
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// GetDefaultStore returns the default credential store
|
|
142
|
+
func GetDefaultStore() CredentialStore {
|
|
143
|
+
// Try keyring first, fall back to file
|
|
144
|
+
return NewKeyringStore()
|
|
145
|
+
}
|