diff --git a/.github/workflows/publish-maven.yml b/.github/workflows/publish-maven.yml index 6a72fe30e..7b41bc0a9 100644 --- a/.github/workflows/publish-maven.yml +++ b/.github/workflows/publish-maven.yml @@ -6,10 +6,14 @@ env: on: workflow_dispatch: inputs: - version: - description: "Version to publish (optional). If empty, uses version from pom.xml." + releaseVersion: + description: "Release version (e.g., 1.0.0). If empty, derives from pom.xml by removing -SNAPSHOT" + required: false type: string + developmentVersion: + description: "Next development version (e.g., 1.0.1-SNAPSHOT). If empty, increments patch version" required: false + type: string prerelease: description: "Is this a prerelease?" type: boolean @@ -29,7 +33,7 @@ jobs: name: Publish Java SDK to Maven Central runs-on: ubuntu-latest outputs: - version: ${{ steps.get-version.outputs.version }} + version: ${{ steps.versions.outputs.release_version }} steps: - uses: actions/checkout@v6 with: @@ -49,29 +53,86 @@ jobs: java-version: "17" distribution: "temurin" server-id: central - server-username: ${{ secrets.MAVEN_CENTRAL_USERNAME }} - server-password: ${{ secrets.MAVEN_CENTRAL_PASSWORD }} + server-username: MAVEN_USERNAME + server-password: MAVEN_PASSWORD gpg-private-key: ${{ secrets.MAVEN_GPG_PRIVATE_KEY }} - gpg-passphrase: ${{ secrets.MAVEN_GPG_PASSPHRASE }} + gpg-passphrase: MAVEN_GPG_PASSPHRASE - - name: Set version in pom.xml - if: ${{ github.event.inputs.version != '' }} - run: mvn versions:set -DnewVersion=${{ github.event.inputs.version }} -DgenerateBackupPoms=false + - name: Determine versions + id: versions + run: | + CURRENT_VERSION=$(mvn help:evaluate -Dexpression=project.version -q -DforceStdout) + echo "Current pom.xml version: $CURRENT_VERSION" + + # Determine release version + if [ -n "${{ inputs.releaseVersion }}" ]; then + RELEASE_VERSION="${{ inputs.releaseVersion }}" + else + # Remove -SNAPSHOT suffix if present + RELEASE_VERSION="${CURRENT_VERSION%-SNAPSHOT}" + fi + echo "Release version: $RELEASE_VERSION" + + # Determine next development version + if [ -n "${{ inputs.developmentVersion }}" ]; then + DEV_VERSION="${{ inputs.developmentVersion }}" + else + # Increment patch version and add -SNAPSHOT + IFS='.' read -r MAJOR MINOR PATCH <<< "$RELEASE_VERSION" + NEXT_PATCH=$((PATCH + 1)) + DEV_VERSION="${MAJOR}.${MINOR}.${NEXT_PATCH}-SNAPSHOT" + fi + echo "Next development version: $DEV_VERSION" + + echo "release_version=$RELEASE_VERSION" >> $GITHUB_OUTPUT + echo "dev_version=$DEV_VERSION" >> $GITHUB_OUTPUT + + echo "### Version Summary" >> $GITHUB_STEP_SUMMARY + echo "- **Release version:** $RELEASE_VERSION" >> $GITHUB_STEP_SUMMARY + echo "- **Next development version:** $DEV_VERSION" >> $GITHUB_STEP_SUMMARY - - name: Get version - id: get-version + - name: Prepare Release run: | - VERSION=$(mvn help:evaluate -Dexpression=project.version -q -DforceStdout) - echo "version=$VERSION" >> $GITHUB_OUTPUT - echo "Publishing version: $VERSION" >> $GITHUB_STEP_SUMMARY + mvn -B release:prepare \ + -DreleaseVersion=${{ steps.versions.outputs.release_version }} \ + -DdevelopmentVersion=${{ steps.versions.outputs.dev_version }} \ + -DtagNameFormat=v@{project.version} \ + -DpushChanges=true \ + -Darguments="-DskipTests" + env: + MAVEN_USERNAME: ${{ secrets.MAVEN_CENTRAL_USERNAME }} + MAVEN_PASSWORD: ${{ secrets.MAVEN_CENTRAL_PASSWORD }} + MAVEN_GPG_PASSPHRASE: ${{ secrets.MAVEN_GPG_PASSPHRASE }} - - name: Build and Deploy to Maven Central - run: mvn -X verify deploy -DskipTests -Prelease + - name: Perform Release and Deploy to Maven Central + run: | + mvn -B release:perform \ + -Dgoals="deploy" \ + -Darguments="-DskipTests -Prelease" env: MAVEN_USERNAME: ${{ secrets.MAVEN_CENTRAL_USERNAME }} MAVEN_PASSWORD: ${{ secrets.MAVEN_CENTRAL_PASSWORD }} MAVEN_GPG_PASSPHRASE: ${{ secrets.MAVEN_GPG_PASSPHRASE }} - MAVEN_GPG_PRIVATE_KEY: ${{ secrets.MAVEN_GPG_PRIVATE_KEY }} + + - name: Update documentation with the released version + run: | + VERSION="${{ steps.versions.outputs.release_version }}" + + # Update version in README.md + sed -i "s|[0-9]*\.[0-9]*\.[0-9]*|${VERSION}|g" README.md + sed -i "s|copilot-sdk:[0-9]*\.[0-9]*\.[0-9]*|copilot-sdk:${VERSION}|g" README.md + + # Update version in jbang-example.java + sed -i "s|copilot-sdk:[0-9]*\.[0-9]*\.[0-9]*|copilot-sdk:${VERSION}|g" jbang-example.java + + # Stage the documentation changes (will be committed by release:prepare) + git add README.md jbang-example.java + + # Commit + git commit -m "Update documentation for version ${VERSION}" + + # Push + git push origin main github-release: name: Create GitHub Release @@ -80,12 +141,46 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v6 + with: + fetch-depth: 0 - name: Create GitHub Release run: | - gh release create "java/v${{ needs.publish-maven.outputs.version }}" \ - ${{ github.event.inputs.prerelease == 'true' && '--prerelease' || '' }} \ - --title "Java SDK v${{ needs.publish-maven.outputs.version }}" \ + VERSION="${{ needs.publish-maven.outputs.version }}" + GROUP_ID="io.github.copilot-community-sdk" + ARTIFACT_ID="copilot-sdk" + + RELEASE_NOTES=$(cat < + ${GROUP_ID} + ${ARTIFACT_ID} + ${VERSION} + + \`\`\` + + ## Gradle (Kotlin DSL) + \`\`\`kotlin + implementation("${GROUP_ID}:${ARTIFACT_ID}:${VERSION}") + \`\`\` + + ## Gradle (Groovy DSL) + \`\`\`groovy + implementation '${GROUP_ID}:${ARTIFACT_ID}:${VERSION}' + \`\`\` + + EOF + ) + + gh release create "${VERSION}" \ + ${{ inputs.prerelease == true && '--prerelease' || '' }} \ + --title "Copilot Community Java SDK ${VERSION}" \ + --notes "${RELEASE_NOTES}" \ --generate-notes \ - --target ${{ github.sha }} + --target "${VERSION}" env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/sdk-build.yml b/.github/workflows/sdk-build.yml index f8f7aff24..e025682a9 100644 --- a/.github/workflows/sdk-build.yml +++ b/.github/workflows/sdk-build.yml @@ -1,8 +1,6 @@ name: "SDK E2E Tests" env: - HUSKY: 0 - PYTHONUTF8: 1 COPILOT_GITHUB_TOKEN: ${{ secrets.COPILOT_GITHUB_TOKEN }} on: diff --git a/LICENSE b/LICENSE new file mode 100644 index 000000000..4fa9fd93f --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2026 Bruno Borges and the Copilot Community SDK contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md index 72dfbf73a..186d064c7 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,29 @@ # Copilot SDK for Java +[![Build](https://github.com/copilot-community-sdk/copilot-sdk-java/actions/workflows/sdk-build.yml/badge.svg)](https://github.com/copilot-community-sdk/copilot-sdk-java/actions/workflows/sdk-build.yml) +[![Maven Central](https://img.shields.io/maven-central/v/io.github.copilot-community-sdk/copilot-sdk)](https://central.sonatype.com/artifact/io.github.copilot-community-sdk/copilot-sdk) +[![Java 17+](https://img.shields.io/badge/Java-17%2B-blue)](https://openjdk.org/) +[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) + Java SDK for programmatic control of GitHub Copilot CLI. -> **Note:** This SDK is in technical preview and may change in breaking ways. +> **Note:** This SDK may change in breaking ways. + +

+ Copilot SDK for Java +

+ +## Table of Contents + +- [Requirements](#requirements) +- [Installation](#installation) +- [Quick Start](#quick-start) +- [Try it with JBang](#try-it-with-jbang) +- [Documentation](#documentation) +- [Building and Testing](#building-and-testing) +- [Projects Using This SDK](#projects-using-this-sdk) +- [Contributing](#contributing) +- [License](#license) ## Requirements @@ -20,14 +41,21 @@ Run `mvn install` locally, then configure the dependency in your project. io.github.copilot-community-sdk copilot-sdk - 0.1.0 + 1.0.0 ``` ### Gradle +Groovy: + ```groovy -implementation 'io.github.copilot-community-sdk:copilot-sdk:0.1.0' +implementation 'io.github.copilot-community-sdk:copilot-sdk:1.0.0' +``` + +Kotlin +```kotlin +implementation("io.github.copilot-community-sdk:copilot-sdk:1.0.0") ``` ## Quick Start @@ -47,7 +75,7 @@ public class Example { // Create a session var session = client.createSession( - new SessionConfig().setModel(CopilotModel.CLAUDE_SONNET_4_5.toString()) + new SessionConfig().setModel("claude-sonnet-4.5") ).get(); // Wait for response using session.idle event @@ -89,7 +117,7 @@ jbang jbang-example.java The `jbang-example.java` file includes the dependency declaration and can be run directly: ```java -//DEPS com.github.copilot:copilot-sdk:0.1.0 +//DEPS io.github.copilot-community-sdk:copilot-sdk:1.0.0 ``` ## Documentation @@ -98,18 +126,11 @@ For detailed API reference and advanced usage examples, see [DOCS.md](DOCS.md). ## Building and Testing -### Building the Project +The tests require test resources (snapshots, harness) from the official [copilot-sdk](https://github.com/github/copilot-sdk) repository. +The build automatically clones this repository during the `generate-test-resources` phase: ```bash -mvn clean compile -``` - -### Running Tests - -The tests require test resources (snapshots, harness) from the official [copilot-sdk](https://github.com/github/copilot-sdk) repository. The build automatically clones this repository during the `generate-test-resources` phase: - -```bash -mvn clean test +mvn clean verify ``` The official SDK repository is cloned to `target/copilot-sdk/` and the `copilot.tests.dir` property is set to point to its `test/` folder. @@ -136,6 +157,14 @@ The pre-commit hook runs `mvn spotless:check` and will fail the commit if there mvn spotless:apply ``` +## Projects Using This SDK + +| Project | Description | +|---------|-------------| +| [JMeter Copilot Plugin](https://github.com/brunoborges/jmeter-copilot-plugin) | A JMeter plugin that integrates GitHub Copilot for AI-assisted load testing | + +> Want to add your project? Open a PR! + ## License MIT diff --git a/image.png b/image.png new file mode 100644 index 000000000..bb83912a3 Binary files /dev/null and b/image.png differ diff --git a/jbang-example.java b/jbang-example.java index 01597d1ec..d410e8264 100644 --- a/jbang-example.java +++ b/jbang-example.java @@ -1,5 +1,5 @@ -//DEPS io.github.copilot-community-sdk:copilot-sdk:0.1.0 +//DEPS io.github.copilot-community-sdk:copilot-sdk:1.0.0 import com.github.copilot.sdk.*; import com.github.copilot.sdk.events.*; import com.github.copilot.sdk.json.*; @@ -13,7 +13,7 @@ public static void main(String[] args) throws Exception { // Create a session var session = client.createSession( - new SessionConfig().setModel(CopilotModel.CLAUDE_SONNET_4_5.toString())).get(); + new SessionConfig().setModel("claude-sonnet-4.5")).get(); // Wait for response using session.idle event var done = new CompletableFuture(); diff --git a/pom.xml b/pom.xml index 661f4c9a5..1ff7fe973 100644 --- a/pom.xml +++ b/pom.xml @@ -2,14 +2,12 @@ - + 4.0.0 io.github.copilot-community-sdk copilot-sdk - 1.0.0 + 1.0.1 jar GitHub Copilot Community SDK :: Java @@ -32,10 +30,11 @@ - scm:git:git://github.com/copilot-community-sdk/copilot-sdk-java.git - scm:git:ssh://github.com:copilot-community-sdk/copilot-sdk-java.git + scm:git:https://github.com/copilot-community-sdk/copilot-sdk-java.git + scm:git:https://github.com/copilot-community-sdk/copilot-sdk-java.git https://github.com/copilot-community-sdk/copilot-sdk-java - + v1.0.1 + 17 diff --git a/src/main/java/com/github/copilot/sdk/CopilotClient.java b/src/main/java/com/github/copilot/sdk/CopilotClient.java index 7d775ea77..4ac2172ff 100644 --- a/src/main/java/com/github/copilot/sdk/CopilotClient.java +++ b/src/main/java/com/github/copilot/sdk/CopilotClient.java @@ -33,8 +33,12 @@ import com.github.copilot.sdk.json.CreateSessionRequest; import com.github.copilot.sdk.json.CreateSessionResponse; 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.GetModelsResponse; +import com.github.copilot.sdk.json.GetStatusResponse; import com.github.copilot.sdk.json.ListSessionsResponse; +import com.github.copilot.sdk.json.ModelInfo; import com.github.copilot.sdk.json.PermissionRequestResult; import com.github.copilot.sdk.json.PingResponse; import com.github.copilot.sdk.json.ResumeSessionConfig; @@ -537,6 +541,40 @@ public CompletableFuture ping(String message) { Map.of("message", message != null ? message : ""), PingResponse.class)); } + /** + * Gets CLI status including version and protocol information. + * + * @return a future that resolves with the status response containing version + * and protocol version + * @see GetStatusResponse + */ + public CompletableFuture getStatus() { + return ensureConnected() + .thenCompose(connection -> connection.rpc.invoke("status.get", Map.of(), GetStatusResponse.class)); + } + + /** + * Gets current authentication status. + * + * @return a future that resolves with the authentication status + * @see GetAuthStatusResponse + */ + public CompletableFuture getAuthStatus() { + return ensureConnected().thenCompose( + connection -> connection.rpc.invoke("auth.getStatus", Map.of(), GetAuthStatusResponse.class)); + } + + /** + * Lists available models with their metadata. + * + * @return a future that resolves with a list of available models + * @see ModelInfo + */ + public CompletableFuture> listModels() { + return ensureConnected().thenCompose(connection -> connection.rpc + .invoke("models.list", Map.of(), GetModelsResponse.class).thenApply(GetModelsResponse::getModels)); + } + /** * Gets the ID of the most recently used session. *

diff --git a/src/main/java/com/github/copilot/sdk/CopilotModel.java b/src/main/java/com/github/copilot/sdk/CopilotModel.java deleted file mode 100644 index 7f6b5bd40..000000000 --- a/src/main/java/com/github/copilot/sdk/CopilotModel.java +++ /dev/null @@ -1,64 +0,0 @@ -package com.github.copilot.sdk; - -/** - * Available Copilot models. - * - *

- * The actual availability of models depends on your GitHub Copilot - * subscription. - */ -public enum CopilotModel { - /** Claude Sonnet 4.5 */ - CLAUDE_SONNET_4_5("claude-sonnet-4.5"), - /** Claude Haiku 4.5 */ - CLAUDE_HAIKU_4_5("claude-haiku-4.5"), - /** Claude Opus 4.5 */ - CLAUDE_OPUS_4_5("claude-opus-4.5"), - /** Claude Sonnet 4 */ - CLAUDE_SONNET_4("claude-sonnet-4"), - /** GPT-5.2 Codex */ - GPT_5_2_CODEX("gpt-5.2-codex"), - /** GPT-5.1 Codex Max */ - GPT_5_1_CODEX_MAX("gpt-5.1-codex-max"), - /** GPT-5.1 Codex */ - GPT_5_1_CODEX("gpt-5.1-codex"), - /** GPT-5.2 */ - GPT_5_2("gpt-5.2"), - /** GPT-5.1 */ - GPT_5_1("gpt-5.1"), - /** GPT-5 */ - GPT_5("gpt-5"), - /** GPT-5.1 Codex Mini */ - GPT_5_1_CODEX_MINI("gpt-5.1-codex-mini"), - /** GPT-5 Mini */ - GPT_5_MINI("gpt-5-mini"), - /** GPT-4.1 */ - GPT_4_1("gpt-4.1"), - /** Gemini 3 Pro Preview */ - GEMINI_3_PRO_PREVIEW("gemini-3-pro-preview"); - - private final String value; - - CopilotModel(String value) { - this.value = value; - } - - /** - * Returns the model identifier string to use with the API. - * - * @return the model identifier - */ - public String getValue() { - return value; - } - - /** - * Returns the string representation of the model. - * - * @return the model identifier string - */ - @Override - public String toString() { - return value; - } -} diff --git a/src/main/java/com/github/copilot/sdk/SdkProtocolVersion.java b/src/main/java/com/github/copilot/sdk/SdkProtocolVersion.java index 3539a595f..7cbf5908d 100644 --- a/src/main/java/com/github/copilot/sdk/SdkProtocolVersion.java +++ b/src/main/java/com/github/copilot/sdk/SdkProtocolVersion.java @@ -12,7 +12,7 @@ */ public enum SdkProtocolVersion { - LATEST(1); + LATEST(2); private int versionNumber; diff --git a/src/main/java/com/github/copilot/sdk/events/AbstractSessionEvent.java b/src/main/java/com/github/copilot/sdk/events/AbstractSessionEvent.java index 39cd8e4f7..d648e2de5 100644 --- a/src/main/java/com/github/copilot/sdk/events/AbstractSessionEvent.java +++ b/src/main/java/com/github/copilot/sdk/events/AbstractSessionEvent.java @@ -57,7 +57,8 @@ public abstract sealed class AbstractSessionEvent permits AssistantTurnStartEvent, AssistantIntentEvent, AssistantReasoningEvent, AssistantReasoningDeltaEvent, AssistantMessageEvent, AssistantMessageDeltaEvent, AssistantTurnEndEvent, AssistantUsageEvent, AbortEvent, // Tool events - ToolUserRequestedEvent, ToolExecutionStartEvent, ToolExecutionPartialResultEvent, ToolExecutionCompleteEvent, + ToolUserRequestedEvent, ToolExecutionStartEvent, ToolExecutionPartialResultEvent, ToolExecutionProgressEvent, + ToolExecutionCompleteEvent, // User events UserMessageEvent, PendingMessagesModifiedEvent, // Other events diff --git a/src/main/java/com/github/copilot/sdk/events/SessionEventParser.java b/src/main/java/com/github/copilot/sdk/events/SessionEventParser.java index e54beb043..4388fae13 100644 --- a/src/main/java/com/github/copilot/sdk/events/SessionEventParser.java +++ b/src/main/java/com/github/copilot/sdk/events/SessionEventParser.java @@ -73,6 +73,7 @@ public class SessionEventParser { TYPE_MAP.put("tool.user_requested", ToolUserRequestedEvent.class); TYPE_MAP.put("tool.execution_start", ToolExecutionStartEvent.class); TYPE_MAP.put("tool.execution_partial_result", ToolExecutionPartialResultEvent.class); + TYPE_MAP.put("tool.execution_progress", ToolExecutionProgressEvent.class); TYPE_MAP.put("tool.execution_complete", ToolExecutionCompleteEvent.class); TYPE_MAP.put("subagent.started", SubagentStartedEvent.class); TYPE_MAP.put("subagent.completed", SubagentCompletedEvent.class); diff --git a/src/main/java/com/github/copilot/sdk/events/ToolExecutionProgressEvent.java b/src/main/java/com/github/copilot/sdk/events/ToolExecutionProgressEvent.java new file mode 100644 index 000000000..e7d6217ed --- /dev/null +++ b/src/main/java/com/github/copilot/sdk/events/ToolExecutionProgressEvent.java @@ -0,0 +1,64 @@ +/*--------------------------------------------------------------------------------------------- + * 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 fired when a tool execution reports progress. + *

+ * This event provides progress updates during tool execution. + */ +@JsonIgnoreProperties(ignoreUnknown = true) +public final class ToolExecutionProgressEvent extends AbstractSessionEvent { + + public static final String TYPE = "tool.execution_progress"; + + @JsonProperty("data") + private ToolExecutionProgressData data; + + @Override + public String getType() { + return TYPE; + } + + public ToolExecutionProgressData getData() { + return data; + } + + public ToolExecutionProgressEvent setData(ToolExecutionProgressData data) { + this.data = data; + return this; + } + + @JsonIgnoreProperties(ignoreUnknown = true) + public static class ToolExecutionProgressData { + + @JsonProperty("toolCallId") + private String toolCallId; + + @JsonProperty("progressMessage") + private String progressMessage; + + public String getToolCallId() { + return toolCallId; + } + + public ToolExecutionProgressData setToolCallId(String toolCallId) { + this.toolCallId = toolCallId; + return this; + } + + public String getProgressMessage() { + return progressMessage; + } + + public ToolExecutionProgressData setProgressMessage(String progressMessage) { + this.progressMessage = progressMessage; + return this; + } + } +} diff --git a/src/main/java/com/github/copilot/sdk/json/GetAuthStatusResponse.java b/src/main/java/com/github/copilot/sdk/json/GetAuthStatusResponse.java new file mode 100644 index 000000000..6d962d423 --- /dev/null +++ b/src/main/java/com/github/copilot/sdk/json/GetAuthStatusResponse.java @@ -0,0 +1,92 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +package com.github.copilot.sdk.json; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonProperty; + +/** + * Response from the auth.getStatus RPC call. + *

+ * Contains information about the current authentication status. + */ +@JsonIgnoreProperties(ignoreUnknown = true) +public class GetAuthStatusResponse { + + /** + * Whether the user is authenticated. + */ + @JsonProperty("isAuthenticated") + private boolean isAuthenticated; + + /** + * Authentication type (user, env, gh-cli, hmac, api-key, token). + */ + @JsonProperty("authType") + private String authType; + + /** + * GitHub host URL. + */ + @JsonProperty("host") + private String host; + + /** + * User login name. + */ + @JsonProperty("login") + private String login; + + /** + * Human-readable status message. + */ + @JsonProperty("statusMessage") + private String statusMessage; + + public boolean isAuthenticated() { + return isAuthenticated; + } + + public GetAuthStatusResponse setAuthenticated(boolean authenticated) { + isAuthenticated = authenticated; + return this; + } + + public String getAuthType() { + return authType; + } + + public GetAuthStatusResponse setAuthType(String authType) { + this.authType = authType; + return this; + } + + public String getHost() { + return host; + } + + public GetAuthStatusResponse setHost(String host) { + this.host = host; + return this; + } + + public String getLogin() { + return login; + } + + public GetAuthStatusResponse setLogin(String login) { + this.login = login; + return this; + } + + public String getStatusMessage() { + return statusMessage; + } + + public GetAuthStatusResponse setStatusMessage(String statusMessage) { + this.statusMessage = statusMessage; + return this; + } +} diff --git a/src/main/java/com/github/copilot/sdk/json/GetModelsResponse.java b/src/main/java/com/github/copilot/sdk/json/GetModelsResponse.java new file mode 100644 index 000000000..7408adc2e --- /dev/null +++ b/src/main/java/com/github/copilot/sdk/json/GetModelsResponse.java @@ -0,0 +1,31 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +package com.github.copilot.sdk.json; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonProperty; + +import java.util.List; + +/** + * Response from the models.list RPC call. + *

+ * Contains a list of available models with their metadata. + */ +@JsonIgnoreProperties(ignoreUnknown = true) +public class GetModelsResponse { + + @JsonProperty("models") + private List models; + + public List getModels() { + return models; + } + + public GetModelsResponse setModels(List models) { + this.models = models; + return this; + } +} diff --git a/src/main/java/com/github/copilot/sdk/json/GetStatusResponse.java b/src/main/java/com/github/copilot/sdk/json/GetStatusResponse.java new file mode 100644 index 000000000..b15ee8766 --- /dev/null +++ b/src/main/java/com/github/copilot/sdk/json/GetStatusResponse.java @@ -0,0 +1,47 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +package com.github.copilot.sdk.json; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonProperty; + +/** + * Response from the status.get RPC call. + *

+ * Contains information about the CLI version and protocol version. + */ +@JsonIgnoreProperties(ignoreUnknown = true) +public class GetStatusResponse { + + /** + * Package version (e.g., "1.0.0"). + */ + @JsonProperty("version") + private String version; + + /** + * Protocol version for SDK compatibility. + */ + @JsonProperty("protocolVersion") + private int protocolVersion; + + public String getVersion() { + return version; + } + + public GetStatusResponse setVersion(String version) { + this.version = version; + return this; + } + + public int getProtocolVersion() { + return protocolVersion; + } + + public GetStatusResponse setProtocolVersion(int protocolVersion) { + this.protocolVersion = protocolVersion; + return this; + } +} diff --git a/src/main/java/com/github/copilot/sdk/json/ModelBilling.java b/src/main/java/com/github/copilot/sdk/json/ModelBilling.java new file mode 100644 index 000000000..d519ba40a --- /dev/null +++ b/src/main/java/com/github/copilot/sdk/json/ModelBilling.java @@ -0,0 +1,27 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +package com.github.copilot.sdk.json; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonProperty; + +/** + * Model billing information. + */ +@JsonIgnoreProperties(ignoreUnknown = true) +public class ModelBilling { + + @JsonProperty("multiplier") + private double multiplier; + + public double getMultiplier() { + return multiplier; + } + + public ModelBilling setMultiplier(double multiplier) { + this.multiplier = multiplier; + return this; + } +} diff --git a/src/main/java/com/github/copilot/sdk/json/ModelCapabilities.java b/src/main/java/com/github/copilot/sdk/json/ModelCapabilities.java new file mode 100644 index 000000000..f61b83228 --- /dev/null +++ b/src/main/java/com/github/copilot/sdk/json/ModelCapabilities.java @@ -0,0 +1,39 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +package com.github.copilot.sdk.json; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonProperty; + +/** + * Model capabilities and limits. + */ +@JsonIgnoreProperties(ignoreUnknown = true) +public class ModelCapabilities { + + @JsonProperty("supports") + private ModelSupports supports; + + @JsonProperty("limits") + private ModelLimits limits; + + public ModelSupports getSupports() { + return supports; + } + + public ModelCapabilities setSupports(ModelSupports supports) { + this.supports = supports; + return this; + } + + public ModelLimits getLimits() { + return limits; + } + + public ModelCapabilities setLimits(ModelLimits limits) { + this.limits = limits; + return this; + } +} diff --git a/src/main/java/com/github/copilot/sdk/json/ModelInfo.java b/src/main/java/com/github/copilot/sdk/json/ModelInfo.java new file mode 100644 index 000000000..f064cce20 --- /dev/null +++ b/src/main/java/com/github/copilot/sdk/json/ModelInfo.java @@ -0,0 +1,90 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +package com.github.copilot.sdk.json; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonProperty; + +/** + * Information about an available model. + */ +@JsonIgnoreProperties(ignoreUnknown = true) +public class ModelInfo { + + /** + * Model identifier (e.g., "claude-sonnet-4.5"). + */ + @JsonProperty("id") + private String id; + + /** + * Display name. + */ + @JsonProperty("name") + private String name; + + /** + * Model capabilities and limits. + */ + @JsonProperty("capabilities") + private ModelCapabilities capabilities; + + /** + * Policy state. + */ + @JsonProperty("policy") + private ModelPolicy policy; + + /** + * Billing information. + */ + @JsonProperty("billing") + private ModelBilling billing; + + public String getId() { + return id; + } + + public ModelInfo setId(String id) { + this.id = id; + return this; + } + + public String getName() { + return name; + } + + public ModelInfo setName(String name) { + this.name = name; + return this; + } + + public ModelCapabilities getCapabilities() { + return capabilities; + } + + public ModelInfo setCapabilities(ModelCapabilities capabilities) { + this.capabilities = capabilities; + return this; + } + + public ModelPolicy getPolicy() { + return policy; + } + + public ModelInfo setPolicy(ModelPolicy policy) { + this.policy = policy; + return this; + } + + public ModelBilling getBilling() { + return billing; + } + + public ModelInfo setBilling(ModelBilling billing) { + this.billing = billing; + return this; + } +} diff --git a/src/main/java/com/github/copilot/sdk/json/ModelLimits.java b/src/main/java/com/github/copilot/sdk/json/ModelLimits.java new file mode 100644 index 000000000..18bb8adbc --- /dev/null +++ b/src/main/java/com/github/copilot/sdk/json/ModelLimits.java @@ -0,0 +1,51 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +package com.github.copilot.sdk.json; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonProperty; + +/** + * Model limits. + */ +@JsonIgnoreProperties(ignoreUnknown = true) +public class ModelLimits { + + @JsonProperty("max_prompt_tokens") + private Integer maxPromptTokens; + + @JsonProperty("max_context_window_tokens") + private int maxContextWindowTokens; + + @JsonProperty("vision") + private ModelVisionLimits vision; + + public Integer getMaxPromptTokens() { + return maxPromptTokens; + } + + public ModelLimits setMaxPromptTokens(Integer maxPromptTokens) { + this.maxPromptTokens = maxPromptTokens; + return this; + } + + public int getMaxContextWindowTokens() { + return maxContextWindowTokens; + } + + public ModelLimits setMaxContextWindowTokens(int maxContextWindowTokens) { + this.maxContextWindowTokens = maxContextWindowTokens; + return this; + } + + public ModelVisionLimits getVision() { + return vision; + } + + public ModelLimits setVision(ModelVisionLimits vision) { + this.vision = vision; + return this; + } +} diff --git a/src/main/java/com/github/copilot/sdk/json/ModelPolicy.java b/src/main/java/com/github/copilot/sdk/json/ModelPolicy.java new file mode 100644 index 000000000..ca5b542c1 --- /dev/null +++ b/src/main/java/com/github/copilot/sdk/json/ModelPolicy.java @@ -0,0 +1,39 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +package com.github.copilot.sdk.json; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonProperty; + +/** + * Model policy state. + */ +@JsonIgnoreProperties(ignoreUnknown = true) +public class ModelPolicy { + + @JsonProperty("state") + private String state; + + @JsonProperty("terms") + private String terms; + + public String getState() { + return state; + } + + public ModelPolicy setState(String state) { + this.state = state; + return this; + } + + public String getTerms() { + return terms; + } + + public ModelPolicy setTerms(String terms) { + this.terms = terms; + return this; + } +} diff --git a/src/main/java/com/github/copilot/sdk/json/ModelSupports.java b/src/main/java/com/github/copilot/sdk/json/ModelSupports.java new file mode 100644 index 000000000..805d6a040 --- /dev/null +++ b/src/main/java/com/github/copilot/sdk/json/ModelSupports.java @@ -0,0 +1,27 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +package com.github.copilot.sdk.json; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonProperty; + +/** + * Model support flags. + */ +@JsonIgnoreProperties(ignoreUnknown = true) +public class ModelSupports { + + @JsonProperty("vision") + private boolean vision; + + public boolean isVision() { + return vision; + } + + public ModelSupports setVision(boolean vision) { + this.vision = vision; + return this; + } +} diff --git a/src/main/java/com/github/copilot/sdk/json/ModelVisionLimits.java b/src/main/java/com/github/copilot/sdk/json/ModelVisionLimits.java new file mode 100644 index 000000000..b9d2124e3 --- /dev/null +++ b/src/main/java/com/github/copilot/sdk/json/ModelVisionLimits.java @@ -0,0 +1,53 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +package com.github.copilot.sdk.json; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonProperty; + +import java.util.List; + +/** + * Model vision-specific limits. + */ +@JsonIgnoreProperties(ignoreUnknown = true) +public class ModelVisionLimits { + + @JsonProperty("supported_media_types") + private List supportedMediaTypes; + + @JsonProperty("max_prompt_images") + private int maxPromptImages; + + @JsonProperty("max_prompt_image_size") + private int maxPromptImageSize; + + public List getSupportedMediaTypes() { + return supportedMediaTypes; + } + + public ModelVisionLimits setSupportedMediaTypes(List supportedMediaTypes) { + this.supportedMediaTypes = supportedMediaTypes; + return this; + } + + public int getMaxPromptImages() { + return maxPromptImages; + } + + public ModelVisionLimits setMaxPromptImages(int maxPromptImages) { + this.maxPromptImages = maxPromptImages; + return this; + } + + public int getMaxPromptImageSize() { + return maxPromptImageSize; + } + + public ModelVisionLimits setMaxPromptImageSize(int maxPromptImageSize) { + this.maxPromptImageSize = maxPromptImageSize; + return this; + } +} diff --git a/src/test/java/com/github/copilot/sdk/MetadataApiTest.java b/src/test/java/com/github/copilot/sdk/MetadataApiTest.java new file mode 100644 index 000000000..2d83b4ec5 --- /dev/null +++ b/src/test/java/com/github/copilot/sdk/MetadataApiTest.java @@ -0,0 +1,333 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +package com.github.copilot.sdk; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.github.copilot.sdk.events.SessionEventParser; +import com.github.copilot.sdk.events.ToolExecutionProgressEvent; +import com.github.copilot.sdk.json.*; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +import java.io.BufferedReader; +import java.io.InputStreamReader; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.List; + +import static org.junit.jupiter.api.Assertions.*; + +/** + * Tests for the new metadata APIs (getStatus, getAuthStatus, listModels) and + * the ToolExecutionProgressEvent. + */ +public class MetadataApiTest { + + private static String cliPath; + private static final ObjectMapper MAPPER = new ObjectMapper(); + + @BeforeAll + static void setup() { + cliPath = getCliPath(); + } + + private static String getCliPath() { + // First, try to find 'copilot' in PATH + String copilotInPath = findCopilotInPath(); + if (copilotInPath != null) { + return copilotInPath; + } + + // Fall back to COPILOT_CLI_PATH environment variable + String envPath = System.getenv("COPILOT_CLI_PATH"); + if (envPath != null && !envPath.isEmpty()) { + return envPath; + } + + // Search for the CLI in the parent directories (nodejs module) + Path current = Paths.get(System.getProperty("user.dir")); + while (current != null) { + Path cliPath = current.resolve("nodejs/node_modules/@github/copilot/index.js"); + if (cliPath.toFile().exists()) { + return cliPath.toString(); + } + current = current.getParent(); + } + + return null; + } + + private static String findCopilotInPath() { + try { + String command = System.getProperty("os.name").toLowerCase().contains("win") ? "where" : "which"; + ProcessBuilder pb = new ProcessBuilder(command, "copilot"); + pb.redirectErrorStream(true); + Process process = pb.start(); + try (BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream()))) { + String line = reader.readLine(); + int exitCode = process.waitFor(); + if (exitCode == 0 && line != null && !line.isEmpty()) { + return line.trim(); + } + } + } catch (Exception e) { + // Ignore - copilot not found in PATH + } + return null; + } + + // ===== ToolExecutionProgressEvent Tests ===== + + @Test + void testToolExecutionProgressEventParsing() { + String json = """ + { + "type": "tool.execution_progress", + "id": "550e8400-e29b-41d4-a716-446655440000", + "timestamp": "2026-01-22T10:00:00Z", + "data": { + "toolCallId": "call-123", + "progressMessage": "Processing file 1 of 10..." + } + } + """; + + var event = SessionEventParser.parse(json); + + assertNotNull(event); + assertInstanceOf(ToolExecutionProgressEvent.class, event); + + ToolExecutionProgressEvent progressEvent = (ToolExecutionProgressEvent) event; + assertEquals("tool.execution_progress", progressEvent.getType()); + assertNotNull(progressEvent.getData()); + assertEquals("call-123", progressEvent.getData().getToolCallId()); + assertEquals("Processing file 1 of 10...", progressEvent.getData().getProgressMessage()); + } + + @Test + void testToolExecutionProgressEventType() { + assertEquals("tool.execution_progress", ToolExecutionProgressEvent.TYPE); + } + + // ===== Response Type Deserialization Tests ===== + + @Test + void testGetStatusResponseDeserialization() throws Exception { + String json = """ + { + "version": "1.2.3", + "protocolVersion": 2 + } + """; + + GetStatusResponse response = MAPPER.readValue(json, GetStatusResponse.class); + + assertEquals("1.2.3", response.getVersion()); + assertEquals(2, response.getProtocolVersion()); + } + + @Test + void testGetAuthStatusResponseDeserialization() throws Exception { + String json = """ + { + "isAuthenticated": true, + "authType": "user", + "host": "github.com", + "login": "testuser", + "statusMessage": "Authenticated successfully" + } + """; + + GetAuthStatusResponse response = MAPPER.readValue(json, GetAuthStatusResponse.class); + + assertTrue(response.isAuthenticated()); + assertEquals("user", response.getAuthType()); + assertEquals("github.com", response.getHost()); + assertEquals("testuser", response.getLogin()); + assertEquals("Authenticated successfully", response.getStatusMessage()); + } + + @Test + void testGetAuthStatusResponseNotAuthenticated() throws Exception { + String json = """ + { + "isAuthenticated": false, + "statusMessage": "Not authenticated" + } + """; + + GetAuthStatusResponse response = MAPPER.readValue(json, GetAuthStatusResponse.class); + + assertFalse(response.isAuthenticated()); + assertNull(response.getAuthType()); + assertNull(response.getHost()); + assertNull(response.getLogin()); + assertEquals("Not authenticated", response.getStatusMessage()); + } + + @Test + void testModelInfoDeserialization() throws Exception { + String json = """ + { + "id": "gpt-4", + "name": "GPT-4", + "capabilities": { + "supports": { + "vision": true + }, + "limits": { + "max_prompt_tokens": 8192, + "max_context_window_tokens": 128000, + "vision": { + "supported_media_types": ["image/png", "image/jpeg"], + "max_prompt_images": 10, + "max_prompt_image_size": 20971520 + } + } + }, + "policy": { + "state": "active", + "terms": "https://example.com/terms" + }, + "billing": { + "multiplier": 1.5 + } + } + """; + + ModelInfo model = MAPPER.readValue(json, ModelInfo.class); + + assertEquals("gpt-4", model.getId()); + assertEquals("GPT-4", model.getName()); + + // Capabilities + assertNotNull(model.getCapabilities()); + assertTrue(model.getCapabilities().getSupports().isVision()); + assertEquals(8192, model.getCapabilities().getLimits().getMaxPromptTokens()); + assertEquals(128000, model.getCapabilities().getLimits().getMaxContextWindowTokens()); + + // Vision limits + ModelVisionLimits visionLimits = model.getCapabilities().getLimits().getVision(); + assertNotNull(visionLimits); + assertEquals(List.of("image/png", "image/jpeg"), visionLimits.getSupportedMediaTypes()); + assertEquals(10, visionLimits.getMaxPromptImages()); + assertEquals(20971520, visionLimits.getMaxPromptImageSize()); + + // Policy + assertNotNull(model.getPolicy()); + assertEquals("active", model.getPolicy().getState()); + assertEquals("https://example.com/terms", model.getPolicy().getTerms()); + + // Billing + assertNotNull(model.getBilling()); + assertEquals(1.5, model.getBilling().getMultiplier()); + } + + @Test + void testGetModelsResponseDeserialization() throws Exception { + String json = """ + { + "models": [ + { + "id": "gpt-4", + "name": "GPT-4", + "capabilities": { + "supports": { "vision": false }, + "limits": { "max_context_window_tokens": 8192 } + } + }, + { + "id": "claude-3", + "name": "Claude 3", + "capabilities": { + "supports": { "vision": true }, + "limits": { "max_context_window_tokens": 200000 } + } + } + ] + } + """; + + GetModelsResponse response = MAPPER.readValue(json, GetModelsResponse.class); + + assertNotNull(response.getModels()); + assertEquals(2, response.getModels().size()); + assertEquals("gpt-4", response.getModels().get(0).getId()); + assertEquals("claude-3", response.getModels().get(1).getId()); + } + + // ===== Integration Tests (require CLI) ===== + + @Test + void testGetStatus() throws Exception { + if (cliPath == null) { + System.out.println("Skipping test: CLI not found"); + return; + } + + try (var client = new CopilotClient(new CopilotClientOptions().setCliPath(cliPath).setUseStdio(true))) { + client.start().get(); + + GetStatusResponse status = client.getStatus().get(); + + assertNotNull(status); + assertNotNull(status.getVersion()); + assertFalse(status.getVersion().isEmpty()); + assertEquals(SdkProtocolVersion.get(), status.getProtocolVersion()); + } + } + + @Test + void testGetAuthStatus() throws Exception { + if (cliPath == null) { + System.out.println("Skipping test: CLI not found"); + return; + } + + try (var client = new CopilotClient(new CopilotClientOptions().setCliPath(cliPath).setUseStdio(true))) { + client.start().get(); + + GetAuthStatusResponse authStatus = client.getAuthStatus().get(); + + assertNotNull(authStatus); + // The response should have a status message regardless of auth state + // We can't guarantee the user is authenticated in tests + } + } + + @Test + void testListModels() throws Exception { + if (cliPath == null) { + System.out.println("Skipping test: CLI not found"); + return; + } + + try (var client = new CopilotClient(new CopilotClientOptions().setCliPath(cliPath).setUseStdio(true))) { + client.start().get(); + + // Note: listModels may require authentication + // This test verifies the method exists and can be called + try { + List models = client.listModels().get(); + assertNotNull(models); + // If we got models, verify they have expected fields + for (ModelInfo model : models) { + assertNotNull(model.getId()); + assertNotNull(model.getName()); + } + } catch (Exception e) { + // May fail if not authenticated, which is acceptable in tests + System.out.println("listModels failed (may require auth): " + e.getMessage()); + } + } + } + + // ===== Protocol Version Test ===== + + @Test + void testProtocolVersionIsTwo() { + assertEquals(2, SdkProtocolVersion.get()); + } +}