Skip to content
Open
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
7 changes: 7 additions & 0 deletions examples/go_simple_test/go.mod
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
module example.com/simple_test

go 1.24.3

replace github.com/jules-labs/jules-go-sdk => ../../go_sdk

require github.com/jules-labs/jules-go-sdk v0.0.0-00010101000000-000000000000
46 changes: 46 additions & 0 deletions examples/go_simple_test/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
package main

import (
"fmt"
"log"
"os"

"github.com/jules-labs/jules-go-sdk"
)

func main() {
apiKey := os.Getenv("JULES_API_KEY")
if apiKey == "" {
log.Fatal("JULES_API_KEY environment variable not set")
}

client := jules.NewJulesClient(apiKey)

// List sources
sourcesResp, err := client.Sources.List()
if err != nil {
log.Fatalf("Error listing sources: %v", err)
}

if len(sourcesResp.Sources) == 0 {
log.Fatal("No sources found")
}

fmt.Printf("Found %d sources\n", len(sourcesResp.Sources))

// Create a session
createReq := &jules.CreateSessionRequest{
Prompt: "Add error handling to the authentication module",
SourceContext: &jules.SourceContext{
Source: sourcesResp.Sources[0].Name,
},
}

session, err := client.Sessions.Create(createReq)
if err != nil {
log.Fatalf("Error creating session: %v", err)
}

fmt.Printf("Session created: %s\n", session.ID)
fmt.Printf("View at: %s\n", session.URL)
}
34 changes: 34 additions & 0 deletions go_sdk/activities.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
package jules

import "fmt"

// ActivitiesService is the service for interacting with the Activities API.
type ActivitiesService struct {
client *Client
}

// NewActivitiesService creates a new ActivitiesService.
func NewActivitiesService(client *Client) *ActivitiesService {
return &ActivitiesService{client: client}
}

// Get retrieves an activity by session and activity ID.
func (s *ActivitiesService) Get(sessionID, activityID string) (*Activity, error) {
var activity Activity
path := fmt.Sprintf("sessions/%s/activities/%s", sessionID, activityID)
_, err := s.client.get(path, &activity)
return &activity, err
}

type ListActivitiesResponse struct {
Activities []Activity `json:"activities"`
NextPageToken string `json:"nextPageToken"`
}

// List lists all activities for a session.
func (s *ActivitiesService) List(sessionID string) (*ListActivitiesResponse, error) {
var resp ListActivitiesResponse
path := fmt.Sprintf("sessions/%s/activities", sessionID)
_, err := s.client.get(path, &resp)
return &resp, err
}
107 changes: 107 additions & 0 deletions go_sdk/client.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
package jules

import (
"bytes"
"encoding/json"
"fmt"
"io"
"net/http"
"time"
)

const (
defaultBaseURL = "https://jules.googleapis.com/v1alpha"
defaultMaxRetries = 3
DefaultTimeout = 30 * time.Second
)

// Client is the base client for the Jules API.
type Client struct {
apiKey string
baseURL string
httpClient *http.Client
maxRetries int
}

// NewClient creates a new Jules API client.
func NewClient(apiKey string) *Client {
return &Client{
apiKey: apiKey,
baseURL: defaultBaseURL,
httpClient: &http.Client{Timeout: DefaultTimeout},
maxRetries: defaultMaxRetries,
}
}

func (c *Client) newRequest(method, path string, body interface{}) (*http.Request, error) {
url := fmt.Sprintf("%s/%s", c.baseURL, path)

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Using fmt.Sprintf to join URL path segments can be fragile as it doesn't handle edge cases like trailing or leading slashes gracefully. A more robust approach is to use net/url.JoinPath (available in Go 1.19+), which is designed for this purpose. You will need to import net/url.


var buf io.ReadWriter
if body != nil {
buf = new(bytes.Buffer)
err := json.NewEncoder(buf).Encode(body)
if err != nil {
return nil, err
}
}

req, err := http.NewRequest(method, url, buf)
if err != nil {
return nil, err
}

req.Header.Set("X-Goog-Api-Key", c.apiKey)
req.Header.Set("Content-Type", "application/json")
req.Header.Set("User-Agent", "jules-go-sdk/0.1.0")

return req, nil
}

func (c *Client) do(req *http.Request, v interface{}) (*http.Response, error) {
var resp *http.Response
var err error

for i := 0; i < c.maxRetries; i++ {
resp, err = c.httpClient.Do(req)

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

critical

The current retry logic reuses the same http.Request object for each attempt. When a request has a body (e.g., a POST request), the body is an io.Reader that gets consumed on the first attempt. Subsequent retries will then send an empty body, which is a critical bug. To fix this, you should clone the request for each retry attempt. req.Clone(req.Context()) will create a new request with a fresh body stream, provided req.GetBody is set, which http.NewRequest does automatically for *bytes.Buffer.

clonedReq := req.Clone(req.Context())
		resp, err = c.httpClient.Do(clonedReq)

if err == nil && resp.StatusCode < 500 {
break // Success or non-retriable error
}
time.Sleep(time.Duration(i*i) * time.Second) // Exponential backoff

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

The comment states this is an "Exponential backoff", but the implementation time.Duration(i*i) * time.Second is quadratic. Furthermore, the first retry happens with a 0-second delay, which is not ideal. You should implement a proper exponential backoff, for example by using bit-shifting.

time.Sleep(time.Duration(1<<uint(i)) * time.Second) // Exponential backoff

}

if err != nil {
return nil, err
}

defer resp.Body.Close()

if resp.StatusCode >= 400 {
return resp, fmt.Errorf("API error: %s", resp.Status)
}
Comment on lines +78 to +80

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The error message for non-successful status codes is very generic. To make debugging easier for users of the SDK, you should read the response body and include it in the error message, as APIs often provide detailed error information in the response payload.

if resp.StatusCode >= 400 {
		body, _ := io.ReadAll(resp.Body)
		return resp, fmt.Errorf("API error: %s, response: %s", resp.Status, string(body))
	}


if v != nil {
if w, ok := v.(io.Writer); ok {
io.Copy(w, resp.Body)
} else {
err = json.NewDecoder(resp.Body).Decode(v)
}
}

return resp, err
}

func (c *Client) get(path string, v interface{}) (*http.Response, error) {
req, err := c.newRequest("GET", path, nil)
if err != nil {
return nil, err
}
return c.do(req, v)
}
Comment on lines +93 to +99

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The code formatting in this function and others within the file is inconsistent and doesn't follow standard Go style (e.g., indentation is incorrect). This makes the code harder to read. Please run gofmt -w . on the project to automatically fix these formatting issues.

func (c *Client) get(path string, v interface{}) (*http.Response, error) {
	req, err := c.newRequest("GET", path, nil)
	if err != nil {
		return nil, err
	}
	return c.do(req, v)
}


func (c *Client) post(path string, body, v interface{}) (*http.Response, error) {
req, err := c.newRequest("POST", path, body)
if err != nil {
return nil, err
}
return c.do(req, v)
}
3 changes: 3 additions & 0 deletions go_sdk/go.mod
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
module github.com/jules-labs/jules-go-sdk

go 1.24.3
20 changes: 20 additions & 0 deletions go_sdk/jules.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package jules

// JulesClient is the main client for the Jules API.
type JulesClient struct {
client *Client
Sessions *SessionsService
Activities *ActivitiesService
Sources *SourcesService
}

// NewJulesClient creates a new JulesClient.
func NewJulesClient(apiKey string) *JulesClient {
baseClient := NewClient(apiKey)
return &JulesClient{
client: baseClient,
Sessions: NewSessionsService(baseClient),
Activities: NewActivitiesService(baseClient),
Sources: NewSourcesService(baseClient),
}
}
125 changes: 125 additions & 0 deletions go_sdk/models.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
package jules

import "time"

type SessionState string

const (
StateUnspecified SessionState = "STATE_UNSPECIFIED"
Queued SessionState = "QUEUED"
Planning SessionState = "PLANNING"
AwaitingPlanApproval SessionState = "AWAITING_PLAN_APPROVAL"
AwaitingUserFeedback SessionState = "AWAITING_USER_FEEDBACK"
InProgress SessionState = "IN_PROGRESS"
Paused SessionState = "PAUSED"
Failed SessionState = "FAILED"
Completed SessionState = "COMPLETED"
)

type GitHubBranch struct {
DisplayName string `json:"displayName"`
}

type GitHubRepo struct {
Owner string `json:"owner"`
Repo string `json:"repo"`
IsPrivate bool `json:"isPrivate"`
DefaultBranch *GitHubBranch `json:"defaultBranch,omitempty"`
Branches []GitHubBranch `json:"branches,omitempty"`
}

type Source struct {
Name string `json:"name"`
ID string `json:"id"`
GithubRepo *GitHubRepo `json:"githubRepo,omitempty"`

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

In Go, it's conventional to capitalize common initialisms like 'ID', 'URL', and 'GitHub'. The field name GithubRepo should be GitHubRepo to align with this convention and for consistency with the type name GitHubRepo.

Suggested change
GithubRepo *GitHubRepo `json:"githubRepo,omitempty"`
GitHubRepo *GitHubRepo `json:"githubRepo,omitempty"`

}

type GitHubRepoContext struct {
StartingBranch string `json:"startingBranch"`
}

type SourceContext struct {
Source string `json:"source"`
GithubRepoContext *GitHubRepoContext `json:"githubRepoContext,omitempty"`

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

In Go, it's conventional to capitalize common initialisms like 'ID', 'URL', and 'GitHub'. The field name GithubRepoContext should be GitHubRepoContext to align with this convention and for consistency with the type name GitHubRepoContext.

Suggested change
GithubRepoContext *GitHubRepoContext `json:"githubRepoContext,omitempty"`
GitHubRepoContext *GitHubRepoContext `json:"githubRepoContext,omitempty"`

}

type PullRequest struct {
URL string `json:"url"`
Title string `json:"title"`
Description string `json:"description"`
}

type SessionOutput struct {
PullRequest *PullRequest `json:"pullRequest,omitempty"`
}

type Session struct {
Prompt string `json:"prompt"`
SourceContext SourceContext `json:"sourceContext"`
Name string `json:"name,omitempty"`
ID string `json:"id,omitempty"`
Title string `json:"title,omitempty"`
RequirePlanApproval bool `json:"requirePlanApproval,omitempty"`
CreateTime time.Time `json:"createTime,omitempty"`
UpdateTime time.Time `json:"updateTime,omitempty"`
State SessionState `json:"state,omitempty"`
URL string `json:"url,omitempty"`
Outputs []SessionOutput `json:"outputs,omitempty"`
}

type PlanStep struct {
ID string `json:"id"`
Title string `json:"title"`
Description string `json:"description"`
Index int `json:"index"`
}

type Plan struct {
ID string `json:"id"`
Steps []PlanStep `json:"steps"`
CreateTime time.Time `json:"createTime,omitempty"`
}

type GitPatch struct {
UnidiffPatch string `json:"unidiffPatch"`
BaseCommitID string `json:"baseCommitId"`
SuggestedCommitMessage string `json:"suggestedCommitMessage"`
}

type ChangeSet struct {
Source string `json:"source"`
GitPatch *GitPatch `json:"gitPatch,omitempty"`
}

type Media struct {
Data string `json:"data"`
MimeType string `json:"mimeType"`
}

type BashOutput struct {
Command string `json:"command"`
Output string `json:"output"`
ExitCode int `json:"exitCode"`
}

type Artifact struct {
ChangeSet *ChangeSet `json:"changeSet,omitempty"`
Media *Media `json:"media,omitempty"`
BashOutput *BashOutput `json:"bashOutput,omitempty"`
}

type Activity struct {
Name string `json:"name"`
ID string `json:"id,omitempty"`
Description string `json:"description,omitempty"`
CreateTime time.Time `json:"createTime,omitempty"`
Originator string `json:"originator,omitempty"`
Artifacts []Artifact `json:"artifacts,omitempty"`
AgentMessaged map[string]string `json:"agentMessaged,omitempty"`
UserMessaged map[string]string `json:"userMessaged,omitempty"`
PlanGenerated map[string]interface{} `json:"planGenerated,omitempty"`
PlanApproved map[string]string `json:"planApproved,omitempty"`
ProgressUpdated map[string]string `json:"progressUpdated,omitempty"`
SessionCompleted map[string]interface{} `json:"sessionCompleted,omitempty"`
SessionFailed map[string]string `json:"sessionFailed,omitempty"`
}
Loading