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 .lastmerge
Original file line number Diff line number Diff line change
@@ -1 +1 @@
40887393a9e687dacc141a645799441b0313ff15
f7fd7577109d64e261456b16c49baa56258eae4e
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. **JDK 25 recommended**. Selecting JDK 25 enables the use of virtual threads, as shown in the [Quick Start](#quick-start).
- GitHub Copilot 1.0.15-0 or later installed and in `PATH` (or provide custom `cliPath`)
- GitHub Copilot CLI 1.0.17 or later installed and in `PATH` (or provide custom `cliPath`)

### Maven

Expand Down
31 changes: 31 additions & 0 deletions src/main/java/com/github/copilot/sdk/CopilotClient.java
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
import com.github.copilot.sdk.json.DeleteSessionResponse;
import com.github.copilot.sdk.json.GetAuthStatusResponse;
import com.github.copilot.sdk.json.GetLastSessionIdResponse;
import com.github.copilot.sdk.json.GetSessionMetadataResponse;
import com.github.copilot.sdk.json.GetModelsResponse;
import com.github.copilot.sdk.json.GetStatusResponse;
import com.github.copilot.sdk.json.ListSessionsResponse;
Expand Down Expand Up @@ -374,6 +375,7 @@ public CompletableFuture<CopilotSession> createSession(SessionConfig config) {

return connection.rpc.invoke("session.create", request, CreateSessionResponse.class).thenApply(response -> {
session.setWorkspacePath(response.workspacePath());
session.setCapabilities(response.capabilities());
// If the server returned a different sessionId (e.g. a v2 CLI that ignores
// the client-supplied ID), re-key the sessions map.
String returnedId = response.sessionId();
Expand Down Expand Up @@ -444,6 +446,7 @@ public CompletableFuture<CopilotSession> resumeSession(String sessionId, ResumeS

return connection.rpc.invoke("session.resume", request, ResumeSessionResponse.class).thenApply(response -> {
session.setWorkspacePath(response.workspacePath());
session.setCapabilities(response.capabilities());
// If the server returned a different sessionId than what was requested, re-key.
String returnedId = response.sessionId();
if (returnedId != null && !returnedId.equals(sessionId)) {
Expand Down Expand Up @@ -657,6 +660,34 @@ public CompletableFuture<List<SessionMetadata>> listSessions(SessionListFilter f
});
}

/**
* Gets metadata for a specific session by ID.
* <p>
* This provides an efficient O(1) lookup of a single session's metadata instead
* of listing all sessions.
*
* <h2>Example Usage</h2>
*
* <pre>{@code
* var metadata = client.getSessionMetadata("session-123").get();
* if (metadata != null) {
* System.out.println("Session started at: " + metadata.getStartTime());
* }
* }</pre>
*
* @param sessionId
* the ID of the session to look up
* @return a future that resolves with the {@link SessionMetadata}, or
* {@code null} if the session was not found
* @see SessionMetadata
* @since 1.0.0
*/
public CompletableFuture<SessionMetadata> getSessionMetadata(String sessionId) {
return ensureConnected().thenCompose(connection -> connection.rpc
.invoke("session.getMetadata", Map.of("sessionId", sessionId), GetSessionMetadataResponse.class)
.thenApply(GetSessionMetadataResponse::session));
}

/**
* Gets the ID of the session currently displayed in the TUI.
* <p>
Expand Down
373 changes: 373 additions & 0 deletions src/main/java/com/github/copilot/sdk/CopilotSession.java

Large diffs are not rendered by default.

33 changes: 33 additions & 0 deletions src/main/java/com/github/copilot/sdk/SessionRequestBuilder.java
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
import java.util.function.Function;

import com.github.copilot.sdk.json.CreateSessionRequest;
import com.github.copilot.sdk.json.CommandWireDefinition;
import com.github.copilot.sdk.json.ResumeSessionConfig;
import com.github.copilot.sdk.json.ResumeSessionRequest;
import com.github.copilot.sdk.json.SectionOverride;
Expand Down Expand Up @@ -122,6 +123,16 @@ static CreateSessionRequest buildCreateRequest(SessionConfig config, String sess
request.setDisabledSkills(config.getDisabledSkills());
request.setConfigDir(config.getConfigDir());

if (config.getCommands() != null && !config.getCommands().isEmpty()) {
var wireCommands = config.getCommands().stream()
.map(c -> new CommandWireDefinition(c.getName(), c.getDescription()))
.collect(java.util.stream.Collectors.toList());
request.setCommands(wireCommands);
}
if (config.getOnElicitationRequest() != null) {
request.setRequestElicitation(true);
}

return request;
}

Expand Down Expand Up @@ -183,6 +194,16 @@ static ResumeSessionRequest buildResumeRequest(String sessionId, ResumeSessionCo
request.setDisabledSkills(config.getDisabledSkills());
request.setInfiniteSessions(config.getInfiniteSessions());

if (config.getCommands() != null && !config.getCommands().isEmpty()) {
var wireCommands = config.getCommands().stream()
.map(c -> new CommandWireDefinition(c.getName(), c.getDescription()))
.collect(java.util.stream.Collectors.toList());
request.setCommands(wireCommands);
}
if (config.getOnElicitationRequest() != null) {
request.setRequestElicitation(true);
}

return request;
}

Expand Down Expand Up @@ -211,6 +232,12 @@ static void configureSession(CopilotSession session, SessionConfig config) {
if (config.getHooks() != null) {
session.registerHooks(config.getHooks());
}
if (config.getCommands() != null) {
session.registerCommands(config.getCommands());
}
if (config.getOnElicitationRequest() != null) {
session.registerElicitationHandler(config.getOnElicitationRequest());
}
if (config.getOnEvent() != null) {
session.on(config.getOnEvent());
}
Expand Down Expand Up @@ -241,6 +268,12 @@ static void configureSession(CopilotSession session, ResumeSessionConfig config)
if (config.getHooks() != null) {
session.registerHooks(config.getHooks());
}
if (config.getCommands() != null) {
session.registerCommands(config.getCommands());
}
if (config.getOnElicitationRequest() != null) {
session.registerElicitationHandler(config.getOnElicitationRequest());
}
if (config.getOnEvent() != null) {
session.on(config.getOnEvent());
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -65,8 +65,8 @@ public abstract sealed class AbstractSessionEvent permits
ToolExecutionCompleteEvent,
// Broadcast request/completion events (protocol v3)
ExternalToolRequestedEvent, ExternalToolCompletedEvent, PermissionRequestedEvent, PermissionCompletedEvent,
CommandQueuedEvent, CommandCompletedEvent, ExitPlanModeRequestedEvent, ExitPlanModeCompletedEvent,
SystemNotificationEvent,
CommandQueuedEvent, CommandCompletedEvent, CommandExecuteEvent, ElicitationRequestedEvent,
CapabilitiesChangedEvent, ExitPlanModeRequestedEvent, ExitPlanModeCompletedEvent, SystemNotificationEvent,
// User events
UserMessageEvent, PendingMessagesModifiedEvent,
// Skill events
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
/*---------------------------------------------------------------------------------------------
* 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: capabilities.changed
* <p>
* Broadcast when the host's session capabilities change. The SDK updates
* {@link com.github.copilot.sdk.CopilotSession#getCapabilities()} accordingly.
*
* @since 1.0.0
*/
@JsonIgnoreProperties(ignoreUnknown = true)
public final class CapabilitiesChangedEvent extends AbstractSessionEvent {

@JsonProperty("data")
private CapabilitiesChangedData data;

@Override
public String getType() {
return "capabilities.changed";
}

public CapabilitiesChangedData getData() {
return data;
}

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

@JsonIgnoreProperties(ignoreUnknown = true)
public record CapabilitiesChangedData(@JsonProperty("ui") CapabilitiesChangedUi ui) {
}

@JsonIgnoreProperties(ignoreUnknown = true)
public record CapabilitiesChangedUi(@JsonProperty("elicitation") Boolean elicitation) {
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
/*---------------------------------------------------------------------------------------------
* 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: command.execute
* <p>
* Broadcast when the user executes a slash command registered by this client.
* Clients that have a matching command handler should respond via
* {@code session.commands.handlePendingCommand}.
*
* @since 1.0.0
*/
@JsonIgnoreProperties(ignoreUnknown = true)
public final class CommandExecuteEvent extends AbstractSessionEvent {

@JsonProperty("data")
private CommandExecuteData data;

@Override
public String getType() {
return "command.execute";
}

public CommandExecuteData getData() {
return data;
}

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

@JsonIgnoreProperties(ignoreUnknown = true)
public record CommandExecuteData(@JsonProperty("requestId") String requestId,
@JsonProperty("command") String command, @JsonProperty("commandName") String commandName,
@JsonProperty("args") String args) {
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
*--------------------------------------------------------------------------------------------*/

package com.github.copilot.sdk.events;

import java.util.List;
import java.util.Map;

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

/**
* Event: elicitation.requested
* <p>
* Broadcast when the server or an MCP tool requests structured input from the
* user. Clients that have an elicitation handler should respond via
* {@code session.ui.handlePendingElicitation}.
*
* @since 1.0.0
*/
@JsonIgnoreProperties(ignoreUnknown = true)
public final class ElicitationRequestedEvent extends AbstractSessionEvent {

@JsonProperty("data")
private ElicitationRequestedData data;

@Override
public String getType() {
return "elicitation.requested";
}

public ElicitationRequestedData getData() {
return data;
}

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

@JsonIgnoreProperties(ignoreUnknown = true)
public record ElicitationRequestedData(@JsonProperty("requestId") String requestId,
@JsonProperty("toolCallId") String toolCallId, @JsonProperty("elicitationSource") String elicitationSource,
@JsonProperty("message") String message, @JsonProperty("mode") String mode,
@JsonProperty("requestedSchema") ElicitationRequestedSchema requestedSchema,
@JsonProperty("url") String url) {
}

@JsonIgnoreProperties(ignoreUnknown = true)
public record ElicitationRequestedSchema(@JsonProperty("type") String type,
@JsonProperty("properties") Map<String, Object> properties,
@JsonProperty("required") List<String> required) {
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ public void setData(PermissionRequestedData data) {

@JsonIgnoreProperties(ignoreUnknown = true)
public record PermissionRequestedData(@JsonProperty("requestId") String requestId,
@JsonProperty("permissionRequest") PermissionRequest permissionRequest) {
@JsonProperty("permissionRequest") PermissionRequest permissionRequest,
@JsonProperty("resolvedByHook") Boolean resolvedByHook) {
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,9 @@ public class SessionEventParser {
TYPE_MAP.put("permission.completed", PermissionCompletedEvent.class);
TYPE_MAP.put("command.queued", CommandQueuedEvent.class);
TYPE_MAP.put("command.completed", CommandCompletedEvent.class);
TYPE_MAP.put("command.execute", CommandExecuteEvent.class);
TYPE_MAP.put("elicitation.requested", ElicitationRequestedEvent.class);
TYPE_MAP.put("capabilities.changed", CapabilitiesChangedEvent.class);
TYPE_MAP.put("exit_plan_mode.requested", ExitPlanModeRequestedEvent.class);
TYPE_MAP.put("exit_plan_mode.completed", ExitPlanModeCompletedEvent.class);
TYPE_MAP.put("system.notification", SystemNotificationEvent.class);
Expand Down
74 changes: 74 additions & 0 deletions src/main/java/com/github/copilot/sdk/json/CommandContext.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
*--------------------------------------------------------------------------------------------*/

package com.github.copilot.sdk.json;

/**
* Context passed to a {@link CommandHandler} when a slash command is executed.
*
* @since 1.0.0
*/
public class CommandContext {

private String sessionId;
private String command;
private String commandName;
private String args;

/** Gets the session ID where the command was invoked. @return the session ID */
public String getSessionId() {
return sessionId;
}

/** Sets the session ID. @param sessionId the session ID @return this */
public CommandContext setSessionId(String sessionId) {
this.sessionId = sessionId;
return this;
}

/**
* Gets the full command text (e.g., {@code /deploy production}).
*
* @return the full command text
*/
public String getCommand() {
return command;
}

/** Sets the full command text. @param command the command text @return this */
public CommandContext setCommand(String command) {
this.command = command;
return this;
}

/**
* Gets the command name without the leading {@code /}.
*
* @return the command name
*/
public String getCommandName() {
return commandName;
}

/** Sets the command name. @param commandName the command name @return this */
public CommandContext setCommandName(String commandName) {
this.commandName = commandName;
return this;
}

/**
* Gets the raw argument string after the command name.
*
* @return the argument string
*/
public String getArgs() {
return args;
}

/** Sets the argument string. @param args the argument string @return this */
public CommandContext setArgs(String args) {
this.args = args;
return this;
}
}
Loading
Loading