Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/scripts/build/format-and-test.sh
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
# ──────────────────────────────────────────────────────────────
set -euo pipefail

ROOT_DIR="$(cd "$(dirname "$0")/../.." && pwd)"
ROOT_DIR="$(cd "$(dirname "$0")/../../.." && pwd)"
cd "$ROOT_DIR"

FORMAT=true
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ Java SDK for programmatic control of GitHub Copilot CLI, enabling you to build A
### Requirements

- Java 17 or later
- GitHub Copilot CLI 0.0.409 or later installed and in PATH (or provide custom `cliPath`)
- GitHub Copilot CLI 0.0.411-1 or later installed and in PATH (or provide custom `cliPath`)

### Maven

Expand Down
22 changes: 22 additions & 0 deletions src/main/java/com/github/copilot/sdk/CliServerManager.java
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ final class CliServerManager {
private static final Logger LOG = Logger.getLogger(CliServerManager.class.getName());

private final CopilotClientOptions options;
private final StringBuilder stderrBuffer = new StringBuilder();

CliServerManager(CopilotClientOptions options) {
this.options = options;
Expand All @@ -47,6 +48,8 @@ final class CliServerManager {
* if interrupted while waiting for port detection
*/
ProcessInfo startCliServer() throws IOException, InterruptedException {
clearStderrBuffer();

String cliPath = options.getCliPath() != null ? options.getCliPath() : "copilot";
var args = new ArrayList<String>();

Expand Down Expand Up @@ -152,6 +155,9 @@ private void startStderrReader(Process process) {
new InputStreamReader(process.getErrorStream(), StandardCharsets.UTF_8))) {
String line;
while ((line = reader.readLine()) != null) {
synchronized (stderrBuffer) {
stderrBuffer.append(line).append('\n');
}
LOG.fine("[CLI] " + line);
}
} catch (IOException e) {
Expand All @@ -171,6 +177,10 @@ private Integer waitForPortAnnouncement(Process process) throws IOException {
while (System.currentTimeMillis() < deadline) {
String line = reader.readLine();
if (line == null) {
String stderr = getStderrOutput();
if (!stderr.isEmpty()) {
throw new IOException("CLI process exited unexpectedly. stderr: " + stderr);
}
throw new IOException("CLI process exited unexpectedly");
}

Expand All @@ -185,6 +195,18 @@ private Integer waitForPortAnnouncement(Process process) throws IOException {
}
}

String getStderrOutput() {
synchronized (stderrBuffer) {
return stderrBuffer.toString().trim();
}
}

private void clearStderrBuffer() {
synchronized (stderrBuffer) {
stderrBuffer.setLength(0);
}
}

private List<String> resolveCliCommand(String cliPath, List<String> args) {
boolean isJsFile = cliPath.toLowerCase().endsWith(".js");

Expand Down
6 changes: 6 additions & 0 deletions src/main/java/com/github/copilot/sdk/CopilotClient.java
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

package com.github.copilot.sdk;

import java.io.IOException;
import java.net.URI;
import java.util.ArrayList;
import java.util.HashMap;
Expand Down Expand Up @@ -174,6 +175,11 @@ private CompletableFuture<Connection> startCore() {
LOG.info("Copilot client connected");
return connection;
} catch (Exception e) {
String stderr = serverManager.getStderrOutput();
if (!stderr.isEmpty()) {
throw new CompletionException(
new IOException("CLI process exited unexpectedly. stderr: " + stderr, e));
}
throw new CompletionException(e);
}
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ static CreateSessionRequest buildCreateRequest(SessionConfig config) {
request.setWorkingDirectory(config.getWorkingDirectory());
request.setStreaming(config.isStreaming() ? true : null);
request.setMcpServers(config.getMcpServers());
request.setEnvValueMode("direct");
request.setCustomAgents(config.getCustomAgents());
request.setInfiniteSessions(config.getInfiniteSessions());
request.setSkillDirectories(config.getSkillDirectories());
Expand Down Expand Up @@ -90,6 +91,7 @@ static ResumeSessionRequest buildResumeRequest(String sessionId, ResumeSessionCo
request.setDisableResume(config.isDisableResume() ? true : null);
request.setStreaming(config.isStreaming() ? true : null);
request.setMcpServers(config.getMcpServers());
request.setEnvValueMode("direct");
request.setCustomAgents(config.getCustomAgents());
request.setSkillDirectories(config.getSkillDirectories());
request.setDisabledSkills(config.getDisabledSkills());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -52,9 +52,9 @@
public abstract sealed class AbstractSessionEvent permits
// Session events
SessionStartEvent, SessionResumeEvent, SessionErrorEvent, SessionIdleEvent, SessionInfoEvent,
SessionModelChangeEvent, SessionHandoffEvent, SessionTruncationEvent, SessionSnapshotRewindEvent,
SessionUsageInfoEvent, SessionCompactionStartEvent, SessionCompactionCompleteEvent, SessionShutdownEvent,
SessionContextChangedEvent,
SessionModelChangeEvent, SessionModeChangedEvent, SessionPlanChangedEvent, SessionWorkspaceFileChangedEvent,
SessionHandoffEvent, SessionTruncationEvent, SessionSnapshotRewindEvent, SessionUsageInfoEvent,
SessionCompactionStartEvent, SessionCompactionCompleteEvent, SessionShutdownEvent, SessionContextChangedEvent,
// Assistant events
AssistantTurnStartEvent, AssistantIntentEvent, AssistantReasoningEvent, AssistantReasoningDeltaEvent,
AssistantMessageEvent, AssistantMessageDeltaEvent, AssistantTurnEndEvent, AssistantUsageEvent, AbortEvent,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,9 @@ public class SessionEventParser {
TYPE_MAP.put("session.idle", SessionIdleEvent.class);
TYPE_MAP.put("session.info", SessionInfoEvent.class);
TYPE_MAP.put("session.model_change", SessionModelChangeEvent.class);
TYPE_MAP.put("session.mode_changed", SessionModeChangedEvent.class);
TYPE_MAP.put("session.plan_changed", SessionPlanChangedEvent.class);
TYPE_MAP.put("session.workspace_file_changed", SessionWorkspaceFileChangedEvent.class);
TYPE_MAP.put("session.handoff", SessionHandoffEvent.class);
TYPE_MAP.put("session.truncation", SessionTruncationEvent.class);
TYPE_MAP.put("session.snapshot_rewind", SessionSnapshotRewindEvent.class);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
*--------------------------------------------------------------------------------------------*/

package com.github.copilot.sdk.events;

import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonProperty;

/**
* Event: session.mode_changed
*
* @since 1.0.10
*/
@JsonIgnoreProperties(ignoreUnknown = true)
public final class SessionModeChangedEvent extends AbstractSessionEvent {

@JsonProperty("data")
private SessionModeChangedData data;

@Override
public String getType() {
return "session.mode_changed";
}

public SessionModeChangedData getData() {
return data;
}

public void setData(SessionModeChangedData data) {
this.data = data;
}

@JsonIgnoreProperties(ignoreUnknown = true)
public record SessionModeChangedData(@JsonProperty("previousMode") String previousMode,
@JsonProperty("newMode") String newMode) {
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
*--------------------------------------------------------------------------------------------*/

package com.github.copilot.sdk.events;

import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonProperty;

/**
* Event: session.plan_changed
*
* @since 1.0.10
*/
@JsonIgnoreProperties(ignoreUnknown = true)
public final class SessionPlanChangedEvent extends AbstractSessionEvent {

@JsonProperty("data")
private SessionPlanChangedData data;

@Override
public String getType() {
return "session.plan_changed";
}

public SessionPlanChangedData getData() {
return data;
}

public void setData(SessionPlanChangedData data) {
this.data = data;
}

@JsonIgnoreProperties(ignoreUnknown = true)
public record SessionPlanChangedData(@JsonProperty("operation") String operation) {
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
*--------------------------------------------------------------------------------------------*/

package com.github.copilot.sdk.events;

import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonProperty;

/**
* Event: session.workspace_file_changed
*
* @since 1.0.10
*/
@JsonIgnoreProperties(ignoreUnknown = true)
public final class SessionWorkspaceFileChangedEvent extends AbstractSessionEvent {

@JsonProperty("data")
private SessionWorkspaceFileChangedData data;

@Override
public String getType() {
return "session.workspace_file_changed";
}

public SessionWorkspaceFileChangedData getData() {
return data;
}

public void setData(SessionWorkspaceFileChangedData data) {
this.data = data;
}

@JsonIgnoreProperties(ignoreUnknown = true)
public record SessionWorkspaceFileChangedData(@JsonProperty("path") String path,
@JsonProperty("operation") String operation) {
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,9 @@ public final class CreateSessionRequest {
@JsonProperty("mcpServers")
private Map<String, Object> mcpServers;

@JsonProperty("envValueMode")
private String envValueMode;

@JsonProperty("customAgents")
private List<CustomAgentConfig> customAgents;

Expand Down Expand Up @@ -224,6 +227,16 @@ public void setMcpServers(Map<String, Object> mcpServers) {
this.mcpServers = mcpServers;
}

/** Gets MCP environment variable value mode. @return the mode */
public String getEnvValueMode() {
return envValueMode;
}

/** Sets MCP environment variable value mode. @param envValueMode the mode */
public void setEnvValueMode(String envValueMode) {
this.envValueMode = envValueMode;
}

/** Gets custom agents. @return the agents */
public List<CustomAgentConfig> getCustomAgents() {
return customAgents == null ? null : Collections.unmodifiableList(customAgents);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,9 @@ public final class ResumeSessionRequest {
@JsonProperty("mcpServers")
private Map<String, Object> mcpServers;

@JsonProperty("envValueMode")
private String envValueMode;

@JsonProperty("customAgents")
private List<CustomAgentConfig> customAgents;

Expand Down Expand Up @@ -251,6 +254,16 @@ public void setMcpServers(Map<String, Object> mcpServers) {
this.mcpServers = mcpServers;
}

/** Gets MCP environment variable value mode. @return the mode */
public String getEnvValueMode() {
return envValueMode;
}

/** Sets MCP environment variable value mode. @param envValueMode the mode */
public void setEnvValueMode(String envValueMode) {
this.envValueMode = envValueMode;
}

/** Gets custom agents. @return the agents */
public List<CustomAgentConfig> getCustomAgents() {
return customAgents == null ? null : Collections.unmodifiableList(customAgents);
Expand Down
2 changes: 1 addition & 1 deletion src/site/markdown/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ Welcome to the documentation for the **Copilot SDK for Java** — a Java SDK for
### Requirements

- Java 17 or later
- GitHub Copilot CLI 0.0.409 or later installed and in PATH (or provide custom `cliPath`)
- GitHub Copilot CLI 0.0.411-1 or later installed and in PATH (or provide custom `cliPath`)

### Installation

Expand Down
20 changes: 20 additions & 0 deletions src/test/java/com/github/copilot/sdk/CopilotClientTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -149,6 +149,26 @@ void testStartAndConnectUsingStdio() throws Exception {
}
}

@Test
void testShouldReportErrorWithStderrWhenCliFailsToStart() throws Exception {
if (cliPath == null) {
System.out.println("Skipping test: CLI not found");
return;
}

var options = new CopilotClientOptions().setCliPath(cliPath)
.setCliArgs(new String[]{"--nonexistent-flag-for-testing"}).setUseStdio(true);

try (var client = new CopilotClient(options)) {
Exception ex = assertThrows(Exception.class, () -> client.start().get());
Throwable root = ex instanceof ExecutionException && ex.getCause() != null ? ex.getCause() : ex;
String message = root.getMessage();
assertNotNull(message);
assertTrue(message.toLowerCase().contains("stderr") || message.toLowerCase().contains("unexpectedly"),
"Error should include stderr or unexpected exit details: " + message);
}
}

@Test
void testStartAndConnectUsingTcp() throws Exception {
if (cliPath == null) {
Expand Down
Loading
Loading