- 
                Notifications
    You must be signed in to change notification settings 
- Fork 3
feat: Add Go SDK for the Jules API #3
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| 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 | 
| 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) | ||
| } | 
| 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 | ||
| } | 
| 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) | ||
|  | ||
| 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) | ||
| There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The current retry logic reuses the same  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 | ||
| There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The comment states this is an "Exponential backoff", but the implementation  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
    
   There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 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
    
   There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 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  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) | ||
| } | ||
| 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 | 
| 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), | ||
| } | ||
| } | 
| 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"` | ||||||
| There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. In Go, it's conventional to capitalize common initialisms like 'ID', 'URL', and 'GitHub'. The field name  
        Suggested change
       
 | ||||||
| } | ||||||
|  | ||||||
| type GitHubRepoContext struct { | ||||||
| StartingBranch string `json:"startingBranch"` | ||||||
| } | ||||||
|  | ||||||
| type SourceContext struct { | ||||||
| Source string `json:"source"` | ||||||
| GithubRepoContext *GitHubRepoContext `json:"githubRepoContext,omitempty"` | ||||||
| There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. In Go, it's conventional to capitalize common initialisms like 'ID', 'URL', and 'GitHub'. The field name  
        Suggested change
       
 | ||||||
| } | ||||||
|  | ||||||
| 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"` | ||||||
| } | ||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Using
fmt.Sprintfto 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 usenet/url.JoinPath(available in Go 1.19+), which is designed for this purpose. You will need to importnet/url.