orch-code 0.1.2 → 0.1.4

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.4 - 2026-04-03
6
+
7
+ - feat: add live probe mode for doctor
8
+ ## v0.1.3 - 2026-04-03
9
+
10
+ - ci: harden npm install and release flow
5
11
  ## v0.1.2 - 2026-04-03
6
12
 
7
13
  - fix: send instructions for OpenAI account chat
package/README.md CHANGED
@@ -247,6 +247,9 @@ go run . <command>
247
247
 
248
248
  `npm i -g orch-code` becomes zero-Go for end users once GitHub Releases and npm publish are both live.
249
249
 
250
+ Canonical GitHub repo:
251
+ - `https://github.com/beydemirfurkan/orch`
252
+
250
253
  Repo automation now expects:
251
254
  - GitHub Actions secret: `NPM_TOKEN`
252
255
  - a git tag in the form `vX.Y.Z`
@@ -276,7 +279,13 @@ The release workflow will:
276
279
  - verify the tag matches package metadata
277
280
  - build darwin/linux/windows binaries with GoReleaser
278
281
  - publish a GitHub Release with raw binary assets
279
- - publish the npm package
282
+ - publish the npm package if that version is not already on npm
283
+ - run clean install smoke tests on macOS and Linux using `npm install -g orch-code`
284
+
285
+ Recommended publish path:
286
+ - treat tag push as the canonical release path
287
+ - avoid manual `npm publish` during normal releases
288
+ - keep manual npm publish only as a recovery path if CI is unavailable
280
289
 
281
290
  `release:prepare` updates these files together:
282
291
  - `package.json`
@@ -324,8 +333,11 @@ Validate runtime readiness:
324
333
 
325
334
  ```bash
326
335
  ./orch doctor
336
+ ./orch doctor --probe
327
337
  ```
328
338
 
339
+ `--probe` runs a small live OpenAI chat check, which is useful for validating account-mode OAuth auth beyond local token shape checks.
340
+
329
341
  Generate a structured plan only:
330
342
 
331
343
  ```bash
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,44 @@ 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
+ client.SetTokenResolver(func(ctx context.Context) (string, error) {
155
+ _ = ctx
156
+ if authMode == "api_key" {
157
+ if storedCred != nil && strings.TrimSpace(storedCred.Key) != "" {
158
+ return strings.TrimSpace(storedCred.Key), nil
159
+ }
160
+ return "", nil
161
+ }
162
+ resolved, resolveErr := auth.ResolveAccountAccessToken(cwd, "openai")
163
+ if resolveErr != nil {
164
+ return "", resolveErr
165
+ }
166
+ return resolved, nil
167
+ })
168
+ return client
169
+ }
170
+
171
+ func runOpenAIProbe(ctx context.Context, client *openai.Client, model string) error {
172
+ _, err := client.Chat(ctx, providers.ChatRequest{
173
+ Role: providers.RoleCoder,
174
+ Model: strings.TrimSpace(model),
175
+ SystemPrompt: "Reply with OK only.",
176
+ UserPrompt: "ping",
177
+ ReasoningEffort: "low",
178
+ })
179
+ return err
180
+ }
181
+
182
+ func doctorProbeTimeout(timeoutSeconds int) time.Duration {
183
+ if timeoutSeconds <= 0 {
184
+ return 20 * time.Second
185
+ }
186
+ timeout := time.Duration(timeoutSeconds) * time.Second
187
+ if timeout > 20*time.Second {
188
+ return 20 * time.Second
189
+ }
190
+ return timeout
191
+ }
@@ -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.2"
4
+ var version = "0.1.4"
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "orch-code",
3
- "version": "0.1.2",
3
+ "version": "0.1.4",
4
4
  "description": "Local-first control plane for deterministic AI coding",
5
5
  "license": "MIT",
6
6
  "bin": {
@@ -29,12 +29,12 @@
29
29
  ],
30
30
  "repository": {
31
31
  "type": "git",
32
- "url": "git+https://github.com/furkanbeydemir/orch.git"
32
+ "url": "git+https://github.com/beydemirfurkan/orch.git"
33
33
  },
34
34
  "bugs": {
35
- "url": "https://github.com/furkanbeydemir/orch/issues"
35
+ "url": "https://github.com/beydemirfurkan/orch/issues"
36
36
  },
37
- "homepage": "https://github.com/furkanbeydemir/orch#readme",
37
+ "homepage": "https://github.com/beydemirfurkan/orch#readme",
38
38
  "engines": {
39
39
  "node": ">=18"
40
40
  }
@@ -0,0 +1,33 @@
1
+ #!/usr/bin/env node
2
+
3
+ const https = require('node:https');
4
+ const path = require('node:path');
5
+
6
+ const packageRoot = path.resolve(__dirname, '..');
7
+ const pkg = require(path.join(packageRoot, 'package.json'));
8
+
9
+ const version = process.argv[2] || pkg.version;
10
+ const encodedName = encodeURIComponent(pkg.name);
11
+ const url = `https://registry.npmjs.org/${encodedName}/${encodeURIComponent(version)}`;
12
+
13
+ https
14
+ .get(url, (response) => {
15
+ response.resume();
16
+
17
+ if (response.statusCode === 200) {
18
+ console.log('true');
19
+ process.exit(0);
20
+ }
21
+
22
+ if (response.statusCode === 404) {
23
+ console.log('false');
24
+ process.exit(0);
25
+ }
26
+
27
+ console.error(`[orch] unexpected npm registry status: ${response.statusCode || 'unknown'}`);
28
+ process.exit(1);
29
+ })
30
+ .on('error', (error) => {
31
+ console.error(`[orch] failed to query npm registry: ${error.message}`);
32
+ process.exit(1);
33
+ });
@@ -45,7 +45,7 @@ function main() {
45
45
 
46
46
  async function install() {
47
47
  const assetName = resolveAssetName();
48
- const baseUrl = process.env.ORCH_BINARY_BASE_URL || `https://github.com/furkanbeydemir/orch/releases/download/v${pkg.version}`;
48
+ const baseUrl = process.env.ORCH_BINARY_BASE_URL || `https://github.com/beydemirfurkan/orch/releases/download/v${pkg.version}`;
49
49
  const assetUrl = `${baseUrl}/${assetName}`;
50
50
 
51
51
  try {