orch-code 0.1.3 → 0.1.5

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/CHANGELOG.md CHANGED
@@ -2,6 +2,12 @@
2
2
 
3
3
  All notable changes to this project will be documented in this file.
4
4
 
5
+ ## v0.1.5 - 2026-04-03
6
+
7
+ - feat: add OAuth account failover for OpenAI
8
+ ## v0.1.4 - 2026-04-03
9
+
10
+ - feat: add live probe mode for doctor
5
11
  ## v0.1.3 - 2026-04-03
6
12
 
7
13
  - ci: harden npm install and release flow
package/README.md CHANGED
@@ -327,14 +327,23 @@ Or account mode (OAuth):
327
327
 
328
328
  ```bash
329
329
  ./orch auth login openai --method account --flow auto
330
+ ./orch auth login openai --method account --flow auto # add another account
331
+ ./orch auth list
332
+ ./orch auth use <credential-id>
333
+ ./orch auth remove <credential-id>
330
334
  ```
331
335
 
336
+ When multiple OpenAI OAuth accounts are stored, Orch keeps one active account and can fail over to the next local account when the active one is rate-limited or rejected.
337
+
332
338
  Validate runtime readiness:
333
339
 
334
340
  ```bash
335
341
  ./orch doctor
342
+ ./orch doctor --probe
336
343
  ```
337
344
 
345
+ `--probe` runs a small live OpenAI chat check, which is useful for validating account-mode OAuth auth beyond local token shape checks.
346
+
338
347
  Generate a structured plan only:
339
348
 
340
349
  ```bash
package/cmd/auth.go CHANGED
@@ -4,7 +4,6 @@ import (
4
4
  "bufio"
5
5
  "fmt"
6
6
  "os"
7
- "sort"
8
7
  "strings"
9
8
  "time"
10
9
 
@@ -56,6 +55,21 @@ var authLogoutCmd = &cobra.Command{
56
55
  RunE: runAuthLogout,
57
56
  }
58
57
 
58
+ var authUseCmd = &cobra.Command{
59
+ Use: "use <credential-id>",
60
+ Short: "Set the active stored credential",
61
+ Args: cobra.ExactArgs(1),
62
+ RunE: runAuthUse,
63
+ }
64
+
65
+ var authRemoveCmd = &cobra.Command{
66
+ Use: "remove <credential-id>",
67
+ Aliases: []string{"rm"},
68
+ Short: "Remove one stored credential",
69
+ Args: cobra.ExactArgs(1),
70
+ RunE: runAuthRemove,
71
+ }
72
+
59
73
  var authOpenAICmd = &cobra.Command{
60
74
  Use: "openai",
61
75
  Hidden: true,
@@ -71,10 +85,14 @@ func init() {
71
85
  authLoginCmd.Flags().StringVar(&authAPIKeyFlag, "api-key", "", "API key")
72
86
 
73
87
  authLogoutCmd.Flags().StringVarP(&authProviderFlag, "provider", "p", "openai", "Provider id")
88
+ authUseCmd.Flags().StringVarP(&authProviderFlag, "provider", "p", "openai", "Provider id")
89
+ authRemoveCmd.Flags().StringVarP(&authProviderFlag, "provider", "p", "openai", "Provider id")
74
90
 
75
91
  authCmd.AddCommand(authLoginCmd)
76
92
  authCmd.AddCommand(authStatusCmd)
77
93
  authCmd.AddCommand(authListCmd)
94
+ authCmd.AddCommand(authUseCmd)
95
+ authCmd.AddCommand(authRemoveCmd)
78
96
  authCmd.AddCommand(authLogoutCmd)
79
97
  authOpenAICmd.AddCommand(newAuthCompatLoginCmd())
80
98
  authOpenAICmd.AddCommand(newAuthCompatLogoutCmd())
@@ -173,6 +191,9 @@ func runAuthLogin(cmd *cobra.Command, args []string) error {
173
191
 
174
192
  fmt.Println("Credential saved to .orch/auth.json (0600).")
175
193
  fmt.Printf("Auth mode set to api_key. Env %s is still supported with higher priority.\n", cfg.Provider.OpenAI.APIKeyEnv)
194
+ if active, activeErr := auth.Get(cwd, provider); activeErr == nil && active != nil {
195
+ fmt.Printf("Active credential id: %s\n", active.ID)
196
+ }
176
197
  return nil
177
198
  }
178
199
 
@@ -213,6 +234,9 @@ func runAuthLogin(cmd *cobra.Command, args []string) error {
213
234
 
214
235
  fmt.Println("Credential saved to .orch/auth.json (0600).")
215
236
  fmt.Printf("Auth mode set to account. You can also use %s.\n", cfg.Provider.OpenAI.AccountTokenEnv)
237
+ if active, activeErr := auth.Get(cwd, provider); activeErr == nil && active != nil {
238
+ fmt.Printf("Active credential id: %s\n", active.ID)
239
+ }
216
240
  if !result.ExpiresAt.IsZero() {
217
241
  fmt.Printf("Token expires at: %s\n", result.ExpiresAt.UTC().Format(time.RFC3339))
218
242
  }
@@ -234,6 +258,10 @@ func runAuthStatus(cmd *cobra.Command, args []string) error {
234
258
  if err != nil {
235
259
  return err
236
260
  }
261
+ credentials, activeID, err := auth.List(cwd, "openai")
262
+ if err != nil {
263
+ return err
264
+ }
237
265
 
238
266
  fmt.Println("Auth Status")
239
267
  fmt.Println("-----------")
@@ -251,6 +279,10 @@ func runAuthStatus(cmd *cobra.Command, args []string) error {
251
279
  fmt.Printf("stored_api_key: %t\n", storedAPIKey)
252
280
  fmt.Printf("stored_account_token: %t\n", storedAccount)
253
281
  fmt.Printf("stored_account_refresh: %t\n", storedRefresh)
282
+ fmt.Printf("stored_credentials: %d\n", len(credentials))
283
+ if activeID != "" {
284
+ fmt.Printf("active_credential_id: %s\n", activeID)
285
+ }
254
286
  if cred != nil && cred.Type == "oauth" && !cred.ExpiresAt.IsZero() {
255
287
  fmt.Printf("account_expires_at: %s\n", cred.ExpiresAt.UTC().Format(time.RFC3339))
256
288
  }
@@ -270,29 +302,76 @@ func runAuthList(cmd *cobra.Command, args []string) error {
270
302
  return fmt.Errorf("failed to get working directory: %w", err)
271
303
  }
272
304
 
273
- all, err := auth.LoadAll(cwd)
305
+ provider := resolveProviderArg(args)
306
+ if provider != "openai" {
307
+ return fmt.Errorf("unsupported provider: %s (supported: openai)", provider)
308
+ }
309
+
310
+ credentials, activeID, err := auth.List(cwd, provider)
274
311
  if err != nil {
275
312
  return err
276
313
  }
277
314
 
278
315
  fmt.Println("Stored Credentials")
279
316
  fmt.Println("------------------")
280
- if len(all) == 0 {
317
+ if len(credentials) == 0 {
281
318
  fmt.Println("No stored credentials found.")
282
319
  return nil
283
320
  }
284
321
 
285
- providers := make([]string, 0, len(all))
286
- for provider := range all {
287
- providers = append(providers, provider)
322
+ for _, cred := range credentials {
323
+ marker := " "
324
+ if cred.ID == activeID {
325
+ marker = "*"
326
+ }
327
+ line := fmt.Sprintf("%s %s (%s)", marker, cred.ID, cred.Type)
328
+ if cred.Email != "" {
329
+ line += " " + cred.Email
330
+ }
331
+ if cred.AccountID != "" {
332
+ line += " account=" + cred.AccountID
333
+ }
334
+ fmt.Println(line)
335
+ }
336
+
337
+ return nil
338
+ }
339
+
340
+ func runAuthUse(cmd *cobra.Command, args []string) error {
341
+ cwd, err := os.Getwd()
342
+ if err != nil {
343
+ return fmt.Errorf("failed to get working directory: %w", err)
344
+ }
345
+
346
+ provider := resolveProviderArg(nil)
347
+ if provider != "openai" {
348
+ return fmt.Errorf("unsupported provider: %s (supported: openai)", provider)
349
+ }
350
+ credentialID := strings.TrimSpace(args[0])
351
+ if err := auth.SetActive(cwd, provider, credentialID); err != nil {
352
+ return err
353
+ }
354
+
355
+ fmt.Printf("Active credential set to %s for %s.\n", credentialID, provider)
356
+ return nil
357
+ }
358
+
359
+ func runAuthRemove(cmd *cobra.Command, args []string) error {
360
+ cwd, err := os.Getwd()
361
+ if err != nil {
362
+ return fmt.Errorf("failed to get working directory: %w", err)
288
363
  }
289
- sort.Strings(providers)
290
364
 
291
- for _, provider := range providers {
292
- cred := all[provider]
293
- fmt.Printf("%s (%s)\n", provider, cred.Type)
365
+ provider := resolveProviderArg(nil)
366
+ if provider != "openai" {
367
+ return fmt.Errorf("unsupported provider: %s (supported: openai)", provider)
368
+ }
369
+ credentialID := strings.TrimSpace(args[0])
370
+ if err := auth.RemoveCredential(cwd, provider, credentialID); err != nil {
371
+ return err
294
372
  }
295
373
 
374
+ fmt.Printf("Stored credential %s removed for %s.\n", credentialID, provider)
296
375
  return nil
297
376
  }
298
377
 
package/cmd/auth_test.go CHANGED
@@ -19,15 +19,27 @@ func TestAuthLoginAccountAndLogout(t *testing.T) {
19
19
  t.Fatalf("save config: %v", err)
20
20
  }
21
21
 
22
- originalOAuthRunner := runOAuthLoginFlow
23
- runOAuthLoginFlow = func(flow string) (auth.OAuthResult, error) {
24
- return auth.OAuthResult{
22
+ results := []auth.OAuthResult{
23
+ {
25
24
  AccessToken: "token-123",
26
25
  RefreshToken: "refresh-123",
27
26
  ExpiresAt: time.Now().UTC().Add(1 * time.Hour),
28
27
  AccountID: "acc-123",
29
28
  Email: "oauth@example.com",
30
- }, nil
29
+ },
30
+ {
31
+ AccessToken: "token-456",
32
+ RefreshToken: "refresh-456",
33
+ ExpiresAt: time.Now().UTC().Add(2 * time.Hour),
34
+ AccountID: "acc-456",
35
+ Email: "second@example.com",
36
+ },
37
+ }
38
+ originalOAuthRunner := runOAuthLoginFlow
39
+ runOAuthLoginFlow = func(flow string) (auth.OAuthResult, error) {
40
+ result := results[0]
41
+ results = results[1:]
42
+ return result, nil
31
43
  }
32
44
  defer func() {
33
45
  runOAuthLoginFlow = originalOAuthRunner
@@ -57,6 +69,47 @@ func TestAuthLoginAccountAndLogout(t *testing.T) {
57
69
  t.Fatalf("expected stored account id")
58
70
  }
59
71
 
72
+ authEmailFlag = "second@example.com"
73
+ if err := runAuthLogin(nil, nil); err != nil {
74
+ t.Fatalf("auth login second account: %v", err)
75
+ }
76
+
77
+ credentials, activeID, err := auth.List(repoRoot, "openai")
78
+ if err != nil {
79
+ t.Fatalf("list credentials: %v", err)
80
+ }
81
+ if len(credentials) != 2 {
82
+ t.Fatalf("expected 2 credentials, got %d", len(credentials))
83
+ }
84
+ if activeID != "acc-456" {
85
+ t.Fatalf("expected second account to become active, got %s", activeID)
86
+ }
87
+
88
+ if err := runAuthUse(nil, []string{"acc-123"}); err != nil {
89
+ t.Fatalf("auth use: %v", err)
90
+ }
91
+ active, err := auth.Get(repoRoot, "openai")
92
+ if err != nil {
93
+ t.Fatalf("get active credential: %v", err)
94
+ }
95
+ if active == nil || active.ID != "acc-123" {
96
+ t.Fatalf("expected acc-123 active, got %#v", active)
97
+ }
98
+
99
+ if err := runAuthRemove(nil, []string{"acc-456"}); err != nil {
100
+ t.Fatalf("auth remove: %v", err)
101
+ }
102
+ credentials, activeID, err = auth.List(repoRoot, "openai")
103
+ if err != nil {
104
+ t.Fatalf("list credentials after remove: %v", err)
105
+ }
106
+ if len(credentials) != 1 {
107
+ t.Fatalf("expected 1 credential after remove, got %d", len(credentials))
108
+ }
109
+ if activeID != "acc-123" {
110
+ t.Fatalf("expected acc-123 to remain active, got %s", activeID)
111
+ }
112
+
60
113
  if err := runAuthLogout(nil, nil); err != nil {
61
114
  t.Fatalf("auth logout: %v", err)
62
115
  }
package/cmd/doctor.go CHANGED
@@ -9,10 +9,13 @@ import (
9
9
 
10
10
  "github.com/furkanbeydemir/orch/internal/auth"
11
11
  "github.com/furkanbeydemir/orch/internal/config"
12
+ "github.com/furkanbeydemir/orch/internal/providers"
12
13
  "github.com/furkanbeydemir/orch/internal/providers/openai"
13
14
  "github.com/spf13/cobra"
14
15
  )
15
16
 
17
+ var doctorProbeFlag bool
18
+
16
19
  var doctorCmd = &cobra.Command{
17
20
  Use: "doctor",
18
21
  Short: "Validate Orch runtime readiness",
@@ -21,6 +24,7 @@ var doctorCmd = &cobra.Command{
21
24
 
22
25
  func init() {
23
26
  rootCmd.AddCommand(doctorCmd)
27
+ doctorCmd.Flags().BoolVar(&doctorProbeFlag, "probe", false, "Run a live provider chat probe")
24
28
  }
25
29
 
26
30
  func runDoctor(cmd *cobra.Command, args []string) error {
@@ -85,8 +89,8 @@ func runDoctor(cmd *cobra.Command, args []string) error {
85
89
  })
86
90
 
87
91
  checks = append(checks, check{
88
- name: "openai.account_refresh",
89
- ok: authMode != "account" || accountToken != "" || !storedAccount || storedRefresh || (storedCred != nil && storedCred.ExpiresAt.IsZero()) || time.Now().UTC().Before(storedCred.ExpiresAt),
92
+ name: "openai.account_refresh",
93
+ ok: authMode != "account" || accountToken != "" || !storedAccount || storedRefresh || (storedCred != nil && storedCred.ExpiresAt.IsZero()) || time.Now().UTC().Before(storedCred.ExpiresAt),
90
94
  detail: fmt.Sprintf("required_when_expired=%t", authMode == "account" && accountToken == ""),
91
95
  })
92
96
 
@@ -95,30 +99,27 @@ func runDoctor(cmd *cobra.Command, args []string) error {
95
99
  checks = append(checks, check{name: "openai.model.reviewer", ok: strings.TrimSpace(cfg.Provider.OpenAI.Models.Reviewer) != "", detail: cfg.Provider.OpenAI.Models.Reviewer})
96
100
 
97
101
  if cfg.Provider.Flags.OpenAIEnabled && defaultProvider == "openai" {
98
- client := openai.New(cfg.Provider.OpenAI)
99
- client.SetTokenResolver(func(ctx context.Context) (string, error) {
100
- _ = ctx
101
- if authMode == "api_key" {
102
- if storedCred != nil && strings.TrimSpace(storedCred.Key) != "" {
103
- return strings.TrimSpace(storedCred.Key), nil
104
- }
105
- return "", nil
106
- }
107
- resolved, resolveErr := auth.ResolveAccountAccessToken(cwd, "openai")
108
- if resolveErr != nil {
109
- return "", resolveErr
110
- }
111
- return resolved, nil
112
- })
102
+ client := newDoctorOpenAIClient(cwd, cfg.Provider.OpenAI, authMode, storedCred)
113
103
 
114
104
  ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
115
105
  defer cancel()
116
106
  validateErr := client.Validate(ctx)
117
107
  checks = append(checks, check{
118
- name: "openai.auth",
108
+ name: "openai.auth.local",
119
109
  ok: validateErr == nil,
120
110
  detail: errDetail(validateErr, "ok"),
121
111
  })
112
+
113
+ if doctorProbeFlag {
114
+ probeCtx, probeCancel := context.WithTimeout(context.Background(), doctorProbeTimeout(cfg.Provider.OpenAI.TimeoutSeconds))
115
+ defer probeCancel()
116
+ probeErr := runOpenAIProbe(probeCtx, client, cfg.Provider.OpenAI.Models.Coder)
117
+ checks = append(checks, check{
118
+ name: "openai.auth.probe",
119
+ ok: probeErr == nil,
120
+ detail: errDetail(probeErr, "ok"),
121
+ })
122
+ }
122
123
  }
123
124
 
124
125
  failed := 0
@@ -147,3 +148,52 @@ func errDetail(err error, fallback string) string {
147
148
  }
148
149
  return err.Error()
149
150
  }
151
+
152
+ func newDoctorOpenAIClient(cwd string, cfg config.OpenAIProviderConfig, authMode string, storedCred *auth.Credential) *openai.Client {
153
+ client := openai.New(cfg)
154
+ var accountSession *auth.AccountSession
155
+ if authMode == "account" && strings.TrimSpace(os.Getenv(cfg.AccountTokenEnv)) == "" {
156
+ accountSession = auth.NewAccountSession(cwd, "openai")
157
+ client.SetAccountFailoverHandler(func(ctx context.Context, err error) (string, bool, error) {
158
+ return accountSession.Failover(ctx, openai.AccountFailoverCooldown(err), err.Error())
159
+ })
160
+ client.SetAccountSuccessHandler(func(ctx context.Context) {
161
+ accountSession.MarkSuccess(ctx)
162
+ })
163
+ }
164
+ client.SetTokenResolver(func(ctx context.Context) (string, error) {
165
+ if authMode == "api_key" {
166
+ if storedCred != nil && strings.TrimSpace(storedCred.Key) != "" {
167
+ return strings.TrimSpace(storedCred.Key), nil
168
+ }
169
+ return "", nil
170
+ }
171
+ if accountSession == nil {
172
+ return "", nil
173
+ }
174
+ return accountSession.ResolveToken(ctx)
175
+ })
176
+ return client
177
+ }
178
+
179
+ func runOpenAIProbe(ctx context.Context, client *openai.Client, model string) error {
180
+ _, err := client.Chat(ctx, providers.ChatRequest{
181
+ Role: providers.RoleCoder,
182
+ Model: strings.TrimSpace(model),
183
+ SystemPrompt: "Reply with OK only.",
184
+ UserPrompt: "ping",
185
+ ReasoningEffort: "low",
186
+ })
187
+ return err
188
+ }
189
+
190
+ func doctorProbeTimeout(timeoutSeconds int) time.Duration {
191
+ if timeoutSeconds <= 0 {
192
+ return 20 * time.Second
193
+ }
194
+ timeout := time.Duration(timeoutSeconds) * time.Second
195
+ if timeout > 20*time.Second {
196
+ return 20 * time.Second
197
+ }
198
+ return timeout
199
+ }
@@ -961,8 +961,17 @@ func executeChatPrompt(prompt string) (*chatExecutionResult, error) {
961
961
  }
962
962
 
963
963
  client := openai.New(cfg.Provider.OpenAI)
964
+ var accountSession *auth.AccountSession
965
+ if strings.ToLower(strings.TrimSpace(cfg.Provider.OpenAI.AuthMode)) == "account" && strings.TrimSpace(os.Getenv(cfg.Provider.OpenAI.AccountTokenEnv)) == "" {
966
+ accountSession = auth.NewAccountSession(cwd, "openai")
967
+ client.SetAccountFailoverHandler(func(ctx context.Context, err error) (string, bool, error) {
968
+ return accountSession.Failover(ctx, openai.AccountFailoverCooldown(err), err.Error())
969
+ })
970
+ client.SetAccountSuccessHandler(func(ctx context.Context) {
971
+ accountSession.MarkSuccess(ctx)
972
+ })
973
+ }
964
974
  client.SetTokenResolver(func(ctx context.Context) (string, error) {
965
- _ = ctx
966
975
  mode := strings.ToLower(strings.TrimSpace(cfg.Provider.OpenAI.AuthMode))
967
976
  if mode == "api_key" {
968
977
  cred, credErr := auth.Get(cwd, "openai")
@@ -974,7 +983,10 @@ func executeChatPrompt(prompt string) (*chatExecutionResult, error) {
974
983
  }
975
984
  return "", nil
976
985
  }
977
- return auth.ResolveAccountAccessToken(cwd, "openai")
986
+ if accountSession == nil {
987
+ return "", nil
988
+ }
989
+ return accountSession.ResolveToken(ctx)
978
990
  })
979
991
 
980
992
  ctx, cancel := context.WithTimeout(context.Background(), time.Duration(cfg.Provider.OpenAI.TimeoutSeconds)*time.Second)
@@ -1,7 +1,11 @@
1
1
  package cmd
2
2
 
3
3
  import (
4
+ "encoding/base64"
4
5
  "encoding/json"
6
+ "fmt"
7
+ "net/http"
8
+ "net/http/httptest"
5
9
  "strings"
6
10
  "testing"
7
11
 
@@ -56,6 +60,76 @@ func TestDoctorFailsWithoutAPIKey(t *testing.T) {
56
60
  }
57
61
  }
58
62
 
63
+ func TestDoctorProbeAccountModeSucceeds(t *testing.T) {
64
+ repoRoot := t.TempDir()
65
+ t.Chdir(repoRoot)
66
+
67
+ server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
68
+ if r.URL.Path != "/codex/responses" {
69
+ t.Fatalf("unexpected path: %s", r.URL.Path)
70
+ }
71
+ if got := r.Header.Get("ChatGPT-Account-Id"); got != "acc-123" {
72
+ t.Fatalf("unexpected account header: %s", got)
73
+ }
74
+ w.Header().Set("Content-Type", "application/json")
75
+ _, _ = w.Write([]byte(`{"output_text":"OK","status":"completed","usage":{"input_tokens":1,"output_tokens":1,"total_tokens":2}}`))
76
+ }))
77
+ defer server.Close()
78
+
79
+ if err := config.EnsureOrchDir(repoRoot); err != nil {
80
+ t.Fatalf("ensure orch dir: %v", err)
81
+ }
82
+ cfg := config.DefaultConfig()
83
+ cfg.Provider.OpenAI.AuthMode = "account"
84
+ cfg.Provider.OpenAI.BaseURL = server.URL
85
+ cfg.Provider.OpenAI.TimeoutSeconds = 5
86
+ if err := config.Save(repoRoot, cfg); err != nil {
87
+ t.Fatalf("save config: %v", err)
88
+ }
89
+
90
+ t.Setenv("OPENAI_ACCOUNT_TOKEN", testDoctorAccountToken("acc-123"))
91
+ doctorProbeFlag = true
92
+ defer func() { doctorProbeFlag = false }()
93
+
94
+ if err := runDoctor(nil, nil); err != nil {
95
+ t.Fatalf("expected doctor probe to succeed: %v", err)
96
+ }
97
+ }
98
+
99
+ func TestDoctorProbeAccountModeFailsWhenProviderRejects(t *testing.T) {
100
+ repoRoot := t.TempDir()
101
+ t.Chdir(repoRoot)
102
+
103
+ server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
104
+ w.WriteHeader(http.StatusUnauthorized)
105
+ _, _ = w.Write([]byte(`{"error":"unauthorized"}`))
106
+ }))
107
+ defer server.Close()
108
+
109
+ if err := config.EnsureOrchDir(repoRoot); err != nil {
110
+ t.Fatalf("ensure orch dir: %v", err)
111
+ }
112
+ cfg := config.DefaultConfig()
113
+ cfg.Provider.OpenAI.AuthMode = "account"
114
+ cfg.Provider.OpenAI.BaseURL = server.URL
115
+ cfg.Provider.OpenAI.TimeoutSeconds = 5
116
+ if err := config.Save(repoRoot, cfg); err != nil {
117
+ t.Fatalf("save config: %v", err)
118
+ }
119
+
120
+ t.Setenv("OPENAI_ACCOUNT_TOKEN", testDoctorAccountToken("acc-123"))
121
+ doctorProbeFlag = true
122
+ defer func() { doctorProbeFlag = false }()
123
+
124
+ err := runDoctor(nil, nil)
125
+ if err == nil {
126
+ t.Fatalf("expected doctor probe failure")
127
+ }
128
+ if !strings.Contains(err.Error(), "doctor failed") {
129
+ t.Fatalf("unexpected doctor error: %v", err)
130
+ }
131
+ }
132
+
59
133
  func TestProviderListJSONOutput(t *testing.T) {
60
134
  repoRoot := t.TempDir()
61
135
  t.Chdir(repoRoot)
@@ -89,3 +163,10 @@ func TestProviderListJSONOutput(t *testing.T) {
89
163
  t.Fatalf("expected openai in all providers, got: %#v", all)
90
164
  }
91
165
  }
166
+
167
+ func testDoctorAccountToken(accountID string) string {
168
+ header := base64.RawURLEncoding.EncodeToString([]byte(`{"alg":"none"}`))
169
+ payload := fmt.Sprintf(`{"https://api.openai.com/auth":{"chatgpt_account_id":"%s"}}`, accountID)
170
+ body := base64.RawURLEncoding.EncodeToString([]byte(payload))
171
+ return header + "." + body + ".sig"
172
+ }
package/cmd/version.go CHANGED
@@ -1,4 +1,4 @@
1
1
  package cmd
2
2
 
3
3
  // version is overridden in release builds via GoReleaser ldflags.
4
- var version = "0.1.3"
4
+ var version = "0.1.5"
@@ -9,12 +9,34 @@ import (
9
9
  const refreshSkew = 30 * time.Second
10
10
 
11
11
  func ResolveAccountCredential(repoRoot, provider string) (*Credential, error) {
12
+ return resolveAccountCredentialByID(repoRoot, provider, "")
13
+ }
14
+
15
+ func resolveAccountCredentialByID(repoRoot, provider, credentialID string) (*Credential, error) {
12
16
  provider = strings.ToLower(strings.TrimSpace(provider))
13
17
  if provider == "" {
14
18
  return nil, fmt.Errorf("provider is required")
15
19
  }
16
20
 
17
- cred, err := Get(repoRoot, provider)
21
+ var (
22
+ cred *Credential
23
+ err error
24
+ )
25
+ if strings.TrimSpace(credentialID) == "" {
26
+ cred, err = Get(repoRoot, provider)
27
+ } else {
28
+ credentials, _, listErr := List(repoRoot, provider)
29
+ if listErr != nil {
30
+ return nil, listErr
31
+ }
32
+ for i := range credentials {
33
+ if credentials[i].ID == credentialID {
34
+ copy := credentials[i]
35
+ cred = &copy
36
+ break
37
+ }
38
+ }
39
+ }
18
40
  if err != nil {
19
41
  return nil, err
20
42
  }
@@ -56,7 +78,10 @@ func ResolveAccountCredential(repoRoot, provider string) (*Credential, error) {
56
78
  return nil, err
57
79
  }
58
80
 
59
- return Get(repoRoot, provider)
81
+ if strings.TrimSpace(credentialID) == "" {
82
+ return Get(repoRoot, provider)
83
+ }
84
+ return resolveAccountCredentialByID(repoRoot, provider, credentialID)
60
85
  }
61
86
 
62
87
  func ResolveAccountAccessToken(repoRoot, provider string) (string, error) {