Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 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 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