Skip to content

Commit e7a7b39

Browse files
authored
Fixes
1 parent f656e13 commit e7a7b39

File tree

10 files changed

+119
-50
lines changed

10 files changed

+119
-50
lines changed

cli/cmd/engine-cli/display.go

Lines changed: 14 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -264,35 +264,36 @@ func containsError(s string) bool {
264264
func wrapText(text string, width int) []string {
265265
var lines []string
266266
for _, paragraph := range strings.Split(text, "\n") {
267-
if len(paragraph) <= width {
267+
runes := []rune(paragraph)
268+
if len(runes) <= width {
268269
lines = append(lines, paragraph)
269270
continue
270271
}
271-
remaining := paragraph
272-
for len(remaining) > width {
273-
idx := width
274-
for idx > 0 && remaining[idx] != ' ' {
272+
for len(runes) > width {
273+
idx := width - 1
274+
for idx > 0 && runes[idx] != ' ' {
275275
idx--
276276
}
277277
if idx == 0 {
278278
idx = width
279279
}
280-
lines = append(lines, remaining[:idx])
281-
remaining = remaining[idx:]
282-
if len(remaining) > 0 && remaining[0] == ' ' {
283-
remaining = remaining[1:]
280+
lines = append(lines, string(runes[:idx]))
281+
runes = runes[idx:]
282+
if len(runes) > 0 && runes[0] == ' ' {
283+
runes = runes[1:]
284284
}
285285
}
286-
if len(remaining) > 0 {
287-
lines = append(lines, remaining)
286+
if len(runes) > 0 {
287+
lines = append(lines, string(runes))
288288
}
289289
}
290290
return lines
291291
}
292292

293293
func truncate(s string, maxLen int) string {
294-
if len(s) <= maxLen {
294+
runes := []rune(s)
295+
if len(runes) <= maxLen {
295296
return s
296297
}
297-
return s[:maxLen-3] + "..."
298+
return string(runes[:maxLen-3]) + "..."
298299
}

cli/cmd/engine-cli/main.go

Lines changed: 18 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import (
77
"context"
88
"encoding/json"
99
"fmt"
10+
"net/url"
1011
"os"
1112
"os/signal"
1213
"strings"
@@ -112,8 +113,9 @@ func runEngine(cmd *cobra.Command, args []string) error {
112113
// Determine owner/repo and API base URL
113114
parts := strings.SplitN(repoNWO, "/", 2)
114115
owner, repo := parts[0], parts[1]
116+
parsedURL, _ := url.Parse(serverURL)
115117
apiBaseURL := serverURL + "/api/v3"
116-
if strings.Contains(serverURL, "github.com") {
118+
if parsedURL != nil && (parsedURL.Host == "github.com" || parsedURL.Host == "www.github.com") {
117119
apiBaseURL = "https://api.github.com"
118120
}
119121

@@ -125,13 +127,15 @@ func runEngine(cmd *cobra.Command, args []string) error {
125127

126128
branchName := fmt.Sprintf("engine-cli-test-%d", time.Now().UnixNano())
127129

128-
// Create branch with empty commit and draft PR
130+
// Create branch with empty commit and PR (best-effort — may fail if token lacks permissions)
129131
setup, err := setupBranchAndPR(apiBaseURL, githubToken, owner, repo, branchName, defaultBranch,
130132
fmt.Sprintf("[engine-cli] %s", truncate(problemStatement, 60)),
131133
fmt.Sprintf("**Problem:** %s\n\n_Created by engine-cli test harness._", problemStatement),
132134
)
133135
if err != nil {
134-
return fmt.Errorf("failed to set up branch and PR: %w", err)
136+
fmt.Fprintf(os.Stderr, " warning: could not pre-create branch/PR: %v\n", err)
137+
fmt.Fprintf(os.Stderr, " (the engine will create the branch on push)\n")
138+
setup = &SetupResult{BranchName: branchName}
135139
}
136140

137141
// Create mock server
@@ -168,17 +172,20 @@ func runEngine(cmd *cobra.Command, args []string) error {
168172
PRDescription string `json:"pr_description"`
169173
}
170174
if json.Unmarshal(event.Content, &ev) == nil && (ev.PRTitle != "" || ev.PRDescription != "") {
171-
_ = updatePullRequest(apiBaseURL, githubToken, owner, repo, prNumber, ev.PRTitle, ev.PRDescription)
175+
if err := updatePullRequest(apiBaseURL, githubToken, owner, repo, prNumber, ev.PRTitle, ev.PRDescription); err != nil {
176+
fmt.Fprintf(os.Stderr, " warning: failed to update PR: %v\n", err)
177+
}
172178
}
173179
}
174180

175181
if engineLogs {
182+
// Track all events as suppressed when in engine-logs mode
183+
suppressedCounts[eventKey{event.Namespace, kind}]++
176184
return
177185
}
178186

179187
// Try to print the event; if nothing was printed, track it
180188
if !printEvent(event) {
181-
kind := resolveKind(event.Kind, event.Content)
182189
suppressedCounts[eventKey{event.Namespace, kind}]++
183190
}
184191
},
@@ -245,12 +252,18 @@ func runEngine(cmd *cobra.Command, args []string) error {
245252
},
246253
}
247254

255+
inferenceURL := os.Getenv("GITHUB_INFERENCE_URL")
256+
if inferenceURL == "" {
257+
inferenceURL = "https://api.githubcopilot.com"
258+
}
259+
248260
env := runner.Environment{
249261
JobID: jobID,
250262
APIToken: githubToken,
251263
APIURL: apiURL,
252264
JobNonce: mockServer.Nonce(),
253265
InferenceToken: githubToken,
266+
InferenceURL: inferenceURL,
254267
GitToken: githubToken,
255268
}
256269

cli/cmd/engine-cli/repo.go

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,15 +10,22 @@ import (
1010
"net/http"
1111
"net/url"
1212
"strings"
13+
"time"
1314
)
1415

16+
// HTTP client with a reasonable timeout for GitHub API calls.
17+
var apiClient = &http.Client{Timeout: 30 * time.Second}
18+
1519
// parseRepoURL splits a full repo URL into server URL and owner/repo.
1620
// e.g. "https://github.com/josebalius/dotfiles" → ("https://github.com", "josebalius/dotfiles")
1721
func parseRepoURL(raw string) (string, string, error) {
1822
u, err := url.Parse(strings.TrimSuffix(raw, ".git"))
1923
if err != nil {
2024
return "", "", err
2125
}
26+
if u.Scheme == "" || u.Host == "" {
27+
return "", "", fmt.Errorf("invalid repo URL %q: must include scheme and host (e.g. https://github.com/owner/repo)", raw)
28+
}
2229
path := strings.TrimPrefix(u.Path, "/")
2330
parts := strings.SplitN(path, "/", 3)
2431
if len(parts) < 2 || parts[0] == "" || parts[1] == "" {
@@ -172,7 +179,7 @@ func githubAPI(method, url, token string, payload any) ([]byte, error) {
172179
req.Header.Set("Content-Type", "application/json")
173180
}
174181

175-
resp, err := http.DefaultClient.Do(req)
182+
resp, err := apiClient.Do(req)
176183
if err != nil {
177184
return nil, err
178185
}

cli/engine-cli

660 Bytes
Binary file not shown.

cli/go.mod

Lines changed: 4 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2,28 +2,24 @@ module github.com/github/copilot-engine-sdk/cli
22

33
go 1.24.0
44

5-
require github.com/spf13/cobra v1.8.0
5+
require (
6+
github.com/charmbracelet/lipgloss v1.1.0
7+
github.com/spf13/cobra v1.8.0
8+
)
69

710
require (
811
github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect
9-
github.com/charmbracelet/bubbletea v1.3.10 // indirect
1012
github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc // indirect
11-
github.com/charmbracelet/lipgloss v1.1.0 // indirect
1213
github.com/charmbracelet/x/ansi v0.10.1 // indirect
1314
github.com/charmbracelet/x/cellbuf v0.0.13-0.20250311204145-2c3ea96c31dd // indirect
1415
github.com/charmbracelet/x/term v0.2.1 // indirect
15-
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect
1616
github.com/inconshreveable/mousetrap v1.1.0 // indirect
1717
github.com/lucasb-eyer/go-colorful v1.2.0 // indirect
1818
github.com/mattn/go-isatty v0.0.20 // indirect
19-
github.com/mattn/go-localereader v0.0.1 // indirect
2019
github.com/mattn/go-runewidth v0.0.16 // indirect
21-
github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 // indirect
22-
github.com/muesli/cancelreader v0.2.2 // indirect
2320
github.com/muesli/termenv v0.16.0 // indirect
2421
github.com/rivo/uniseg v0.4.7 // indirect
2522
github.com/spf13/pflag v1.0.5 // indirect
2623
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect
2724
golang.org/x/sys v0.36.0 // indirect
28-
golang.org/x/text v0.3.8 // indirect
2925
)

cli/go.sum

Lines changed: 2 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,36 +1,24 @@
11
github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k=
22
github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8=
3-
github.com/charmbracelet/bubbletea v1.3.10 h1:otUDHWMMzQSB0Pkc87rm691KZ3SWa4KUlvF9nRvCICw=
4-
github.com/charmbracelet/bubbletea v1.3.10/go.mod h1:ORQfo0fk8U+po9VaNvnV95UPWA1BitP1E0N6xJPlHr4=
53
github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc h1:4pZI35227imm7yK2bGPcfpFEmuY1gc2YSTShr4iJBfs=
64
github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc/go.mod h1:X4/0JoqgTIPSFcRA/P6INZzIuyqdFY5rm8tb41s9okk=
75
github.com/charmbracelet/lipgloss v1.1.0 h1:vYXsiLHVkK7fp74RkV7b2kq9+zDLoEU4MZoFqR/noCY=
86
github.com/charmbracelet/lipgloss v1.1.0/go.mod h1:/6Q8FR2o+kj8rz4Dq0zQc3vYf7X+B0binUUBwA0aL30=
9-
github.com/charmbracelet/x/ansi v0.8.0 h1:9GTq3xq9caJW8ZrBTe0LIe2fvfLR/bYXKTx2llXn7xE=
10-
github.com/charmbracelet/x/ansi v0.8.0/go.mod h1:wdYl/ONOLHLIVmQaxbIYEC/cRKOQyjTkowiI4blgS9Q=
117
github.com/charmbracelet/x/ansi v0.10.1 h1:rL3Koar5XvX0pHGfovN03f5cxLbCF2YvLeyz7D2jVDQ=
128
github.com/charmbracelet/x/ansi v0.10.1/go.mod h1:3RQDQ6lDnROptfpWuUVIUG64bD2g2BgntdxH0Ya5TeE=
139
github.com/charmbracelet/x/cellbuf v0.0.13-0.20250311204145-2c3ea96c31dd h1:vy0GVL4jeHEwG5YOXDmi86oYw2yuYUGqz6a8sLwg0X8=
1410
github.com/charmbracelet/x/cellbuf v0.0.13-0.20250311204145-2c3ea96c31dd/go.mod h1:xe0nKWGd3eJgtqZRaN9RjMtK7xUYchjzPr7q6kcvCCs=
1511
github.com/charmbracelet/x/term v0.2.1 h1:AQeHeLZ1OqSXhrAWpYUtZyX1T3zVxfpZuEQMIQaGIAQ=
1612
github.com/charmbracelet/x/term v0.2.1/go.mod h1:oQ4enTYFV7QN4m0i9mzHrViD7TQKvNEEkHUMCmsxdUg=
1713
github.com/cpuguy83/go-md2man/v2 v2.0.3/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
18-
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f h1:Y/CXytFA4m6baUTXGLOoWe4PQhGxaX0KpnayAqC48p4=
19-
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f/go.mod h1:vw97MGsxSvLiUE2X8qFplwetxpGLQrlU1Q9AUEIzCaM=
2014
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
2115
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
2216
github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY=
2317
github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
2418
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
2519
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
26-
github.com/mattn/go-localereader v0.0.1 h1:ygSAOl7ZXTx4RdPYinUpg6W99U8jWvWi9Ye2JC/oIi4=
27-
github.com/mattn/go-localereader v0.0.1/go.mod h1:8fBrzywKY7BI3czFoHkuzRoWE9C+EiG4R1k4Cjx5p88=
2820
github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc=
2921
github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
30-
github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 h1:ZK8zHtRHOkbHy6Mmr5D264iyp3TiX5OmNcI5cIARiQI=
31-
github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6/go.mod h1:CJlz5H+gyd6CUWT45Oy4q24RdLyn7Md9Vj2/ldJBSIo=
32-
github.com/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELUXHmA=
33-
github.com/muesli/cancelreader v0.2.2/go.mod h1:3XuTXfFS2VjM+HTLZY9Ak0l6eUKfijIfMUZ4EgX0QYo=
3422
github.com/muesli/termenv v0.16.0 h1:S5AlUN9dENB57rsbnkPyfdGuWIlkmzJjbFf0Tf5FWUc=
3523
github.com/muesli/termenv v0.16.0/go.mod h1:ZRfOIKPFDYQoDFF4Olj7/QJbW60Ol/kL1pU3VfY/Cnk=
3624
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
@@ -43,13 +31,10 @@ github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
4331
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
4432
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no=
4533
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM=
46-
golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
34+
golang.org/x/exp v0.0.0-20220909182711-5c715a9e8561 h1:MDc5xs78ZrZr3HMQugiXOAkSZtfTpbJLDr/lwfgO53E=
35+
golang.org/x/exp v0.0.0-20220909182711-5c715a9e8561/go.mod h1:cyybsKvd6eL0RnXn6p/Grxp8F5bW7iYuBgsNCOHpMYE=
4736
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
48-
golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc=
49-
golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
5037
golang.org/x/sys v0.36.0 h1:KVRy2GtZBrk1cBYA7MKu5bEZFxQk4NIDV6RLVcC8o0k=
5138
golang.org/x/sys v0.36.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
52-
golang.org/x/text v0.3.8 h1:nAL+RVCQ9uMn3vJZbV+MRnydTJFPf8qqY42YiA6MrqY=
53-
golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ=
5439
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
5540
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

cli/internal/runner/runner.go

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import (
1010
"os"
1111
"os/exec"
1212
"strings"
13+
"sync"
1314
)
1415

1516
// Environment contains the platform environment variables for the engine.
@@ -19,6 +20,7 @@ type Environment struct {
1920
APIURL string
2021
JobNonce string
2122
InferenceToken string
23+
InferenceURL string
2224
GitToken string
2325
}
2426

@@ -76,8 +78,12 @@ func Run(ctx context.Context, command string, env Environment, opts Options, cal
7678
return Result{ExitCode: 1, Error: fmt.Errorf("failed to start command: %w", err)}
7779
}
7880

79-
// Read stdout in a goroutine
81+
// Read stdout and stderr in goroutines
82+
var wg sync.WaitGroup
83+
wg.Add(2)
84+
8085
go func() {
86+
defer wg.Done()
8187
scanner := bufio.NewScanner(stdout)
8288
for scanner.Scan() {
8389
if callbacks.OnStdout != nil {
@@ -86,8 +92,8 @@ func Run(ctx context.Context, command string, env Environment, opts Options, cal
8692
}
8793
}()
8894

89-
// Read stderr in a goroutine
9095
go func() {
96+
defer wg.Done()
9197
scanner := bufio.NewScanner(stderr)
9298
for scanner.Scan() {
9399
if callbacks.OnStderr != nil {
@@ -96,8 +102,9 @@ func Run(ctx context.Context, command string, env Environment, opts Options, cal
96102
}
97103
}()
98104

99-
// Wait for the command to finish
105+
// Wait for the command to finish, then drain remaining output
100106
err = cmd.Wait()
107+
wg.Wait()
101108
exitCode := 0
102109
if err != nil {
103110
if exitErr, ok := err.(*exec.ExitError); ok {
@@ -125,6 +132,10 @@ func buildEnv(env Environment, extra map[string]string) []string {
125132
"GITHUB_GIT_TOKEN": env.GitToken,
126133
}
127134

135+
if env.InferenceURL != "" {
136+
platformVars["GITHUB_INFERENCE_URL"] = env.InferenceURL
137+
}
138+
128139
for k, v := range platformVars {
129140
result = append(result, fmt.Sprintf("%s=%s", k, v))
130141
}

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
"types": "./dist/index.d.ts"
1212
},
1313
"./mcp-server": {
14-
"import": "./dist/mcp-server.js",
14+
"import": "./dist/mcp-server.bundled.js",
1515
"types": "./dist/mcp-server.d.ts"
1616
}
1717
},

src/client.ts

Lines changed: 57 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -223,7 +223,12 @@ export class PlatformClient {
223223

224224
// Try to parse response (may contain user_messages)
225225
const text = await response.text();
226-
const parsed = text.trim() ? (JSON.parse(text) as ProgressResponse) : undefined;
226+
let parsed: ProgressResponse | undefined;
227+
try {
228+
parsed = text.trim() ? (JSON.parse(text) as ProgressResponse) : undefined;
229+
} catch {
230+
// Invalid JSON in response — treat as success with no parsed body
231+
}
227232

228233
return {
229234
success: true,
@@ -429,6 +434,57 @@ export class PlatformClient {
429434
return null;
430435
}
431436
}
437+
438+
/**
439+
* Fetches job details from the platform API.
440+
*
441+
* @returns Job details including problem statement, repository info, and branch
442+
*/
443+
async fetchJobDetails(): Promise<JobDetails> {
444+
const url = new URL(`jobs/${this._jobId}`, this.baseUrl);
445+
446+
const response = await fetch(url.toString(), {
447+
method: "GET",
448+
headers: this.headers,
449+
});
450+
451+
if (!response.ok) {
452+
throw new Error(
453+
`Failed to fetch job details: HTTP ${response.status} ${response.statusText}`
454+
);
455+
}
456+
457+
const data = (await response.json()) as JobDetails;
458+
459+
if (!data.problem_statement?.content) {
460+
throw new Error("Job details missing required field: problem_statement.content");
461+
}
462+
463+
return data;
464+
}
465+
}
466+
467+
// =============================================================================
468+
// Job Details Types
469+
// =============================================================================
470+
471+
/** Problem statement from the job */
472+
export interface ProblemStatement {
473+
content: string;
474+
}
475+
476+
/** Job details returned from the platform API */
477+
export interface JobDetails {
478+
ID: string;
479+
problem_statement: ProblemStatement;
480+
organization_custom_instructions?: string;
481+
action: string;
482+
repository: string;
483+
server_url: string;
484+
branch_name?: string;
485+
commit_login: string;
486+
commit_email: string;
487+
mcp_proxy_url?: string;
432488
}
433489

434490
/**

src/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -93,7 +93,7 @@ export type {
9393

9494
export { PlatformClient } from "./client.js";
9595

96-
export type { PlatformClientConfig, ProgressPayload, ProgressRecord, ProgressResponse, SendResult } from "./client.js";
96+
export type { PlatformClientConfig, ProgressPayload, ProgressRecord, ProgressResponse, SendResult, JobDetails, ProblemStatement } from "./client.js";
9797

9898
// =============================================================================
9999
// MCP Server

0 commit comments

Comments
 (0)