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
10 changes: 9 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,20 +29,28 @@ import (

func main() {
ctx := context.Background()
ch, err := claudecode.Query(ctx, "What is 2 + 2?", nil)
ch, errCh, err := claudecode.Query(ctx, "What is 2 + 2?", nil)
if err != nil {
log.Fatal(err)
}
for msg := range ch {
log.Printf("%+v", msg)
}
if e := <-errCh; e != nil {
log.Fatalf("query error: %v", e)
}
}
```

## Examples

See `examples/quick_start.go` for a more complete example of using the package.

### Custom CLI Path

If the `claude` CLI binary is not on your `PATH`, set `Options.CLIPath` to the
location of the executable when calling `Query`.

## License

MIT
2 changes: 1 addition & 1 deletion claudecode/query.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import (
)

// Query sends a prompt to Claude Code and returns a stream of Messages.
func Query(ctx context.Context, prompt string, opts *model.Options) (<-chan model.Message, error) {
func Query(ctx context.Context, prompt string, opts *model.Options) (<-chan model.Message, <-chan error, error) {
os.Setenv("CLAUDE_CODE_ENTRYPOINT", "sdk-go")
transport := &internal.SubprocessCLITransport{}
client := &internal.Client{Transport: transport}
Expand Down
5 changes: 4 additions & 1 deletion examples/quick_start.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,15 @@ import (
func main() {
ctx := context.Background()

ch, err := claudecode.Query(ctx, "What is 2 + 2?", nil)
ch, errCh, err := claudecode.Query(ctx, "What is 2 + 2?", nil)
if err != nil {
log.Fatal(err)
}

for msg := range ch {
log.Printf("%+v", msg)
}
if e := <-errCh; e != nil {
log.Fatalf("query error: %v", e)
}
}
24 changes: 14 additions & 10 deletions internal/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,35 +12,39 @@
}

// Query connects to the transport and streams parsed messages.
func (c *Client) Query(ctx context.Context, prompt string, opts *model.Options) (<-chan model.Message, error) {
if sp, ok := c.Transport.(*SubprocessCLITransport); ok {
sp.Prompt = prompt
sp.Options = opts
if opts != nil {
sp.Cwd = opts.Cwd
}
func (c *Client) Query(ctx context.Context, prompt string, opts *model.Options) (<-chan model.Message, <-chan error, error) {
if err := c.Transport.SendRequest(ctx, prompt, opts); err != nil {
return nil, nil, err
}

if err := c.Transport.Connect(ctx); err != nil {
return nil, err
return nil, nil, err
}

rawCh, err := c.Transport.ReceiveMessages(ctx)
if err != nil {
return nil, err
return nil, nil, err
}

out := make(chan model.Message)
errCh := make(chan error, 1)
go func() {
defer close(out)
defer close(errCh)
defer c.Transport.Disconnect()

Check failure on line 34 in internal/client.go

View workflow job for this annotation

GitHub Actions / build (1.21)

Error return value of `c.Transport.Disconnect` is not checked (errcheck)

Check failure on line 34 in internal/client.go

View workflow job for this annotation

GitHub Actions / build (1.22)

Error return value of `c.Transport.Disconnect` is not checked (errcheck)
for data := range rawCh {
if errVal, ok := data["error"]; ok {
if e, ok2 := errVal.(error); ok2 {
errCh <- e
return
}
}
if m := parseMessage(data); m != nil {
out <- m
}
}
}()
return out, nil
return out, errCh, nil
}

func parseMessage(data map[string]any) model.Message {
Expand Down
30 changes: 28 additions & 2 deletions internal/client_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ type stubTransport struct {
msgs []map[string]any
connected bool
disconnected bool
sent bool
}

func (s *stubTransport) Connect(ctx context.Context) error {
Expand All @@ -23,6 +24,11 @@ func (s *stubTransport) Disconnect() error {
return nil
}

func (s *stubTransport) SendRequest(ctx context.Context, prompt string, opts *model.Options) error {
s.sent = true
return nil
}

func (s *stubTransport) ReceiveMessages(ctx context.Context) (<-chan map[string]any, error) {
ch := make(chan map[string]any)
go func() {
Expand Down Expand Up @@ -91,7 +97,7 @@ func TestClientQuery(t *testing.T) {
{"type": "result", "subtype": "done"},
}}
c := &Client{Transport: st}
ch, err := c.Query(context.Background(), "hi", nil)
ch, errCh, err := c.Query(context.Background(), "hi", nil)
if err != nil {
t.Fatal(err)
}
Expand All @@ -102,7 +108,27 @@ func TestClientQuery(t *testing.T) {
if len(msgs) != 2 {
t.Fatalf("expected 2 messages, got %d", len(msgs))
}
if !st.connected || !st.disconnected {
if !st.connected || !st.disconnected || !st.sent {
t.Fatalf("transport lifecycle not called")
}
if errVal := <-errCh; errVal != nil {
t.Fatalf("unexpected error: %v", errVal)
}
}

func TestClientQueryErrorPropagation(t *testing.T) {
perr := &model.ProcessError{Msg: "fail", ExitCode: 1}
st := &stubTransport{msgs: []map[string]any{
{"error": perr},
}}
c := &Client{Transport: st}
ch, errCh, err := c.Query(context.Background(), "hi", nil)
if err != nil {
t.Fatal(err)
}
for range ch {
}
if e := <-errCh; e != perr {
t.Fatalf("expected error propagation")
}
}
22 changes: 22 additions & 0 deletions internal/transport.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import (
type Transport interface {
Connect(ctx context.Context) error
Disconnect() error
SendRequest(ctx context.Context, prompt string, opts *model.Options) error
ReceiveMessages(ctx context.Context) (<-chan map[string]any, error)
}

Expand All @@ -34,6 +35,19 @@ type SubprocessCLITransport struct {
stderr io.ReadCloser
}

// SendRequest stores the prompt and options for the next connection.
func (t *SubprocessCLITransport) SendRequest(ctx context.Context, prompt string, opts *model.Options) error {
t.Prompt = prompt
t.Options = opts
if opts != nil {
t.Cwd = opts.Cwd
if opts.CLIPath != "" {
t.CLIPath = opts.CLIPath
}
}
return nil
}

// Connect starts the CLI process with the provided options.
func (t *SubprocessCLITransport) Connect(ctx context.Context) error {
if t.CLIPath == "" {
Expand Down Expand Up @@ -153,6 +167,14 @@ func buildCommand(cliPath, prompt string, opts *model.Options) []string {
if len(opts.AllowedTools) > 0 {
args = append(args, "--allowedTools", join(opts.AllowedTools))
}
if opts.MaxThinkingTokens > 0 {
args = append(args, "--max-thinking-tokens", fmt.Sprint(opts.MaxThinkingTokens))
} else {
args = append(args, "--max-thinking-tokens", "8000")
}
if len(opts.MCPTools) > 0 {
args = append(args, "--mcpTools", join(opts.MCPTools))
}
if opts.MaxTurns > 0 {
args = append(args, "--max-turns", fmt.Sprint(opts.MaxTurns))
}
Expand Down
11 changes: 10 additions & 1 deletion internal/transport_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,16 @@ import (
func TestBuildCommandBasic(t *testing.T) {
opts := &model.Options{SystemPrompt: "hi"}
cmd := buildCommand("/usr/bin/claude", "hello", opts)
expect := []string{"/usr/bin/claude", "--output-format", "stream-json", "--verbose", "--system-prompt", "hi", "--print", "hello"}
expect := []string{"/usr/bin/claude", "--output-format", "stream-json", "--verbose", "--system-prompt", "hi", "--max-thinking-tokens", "8000", "--print", "hello"}
if !reflect.DeepEqual(cmd, expect) {
t.Fatalf("unexpected cmd: %v", cmd)
}
}

func TestBuildCommandWithExtras(t *testing.T) {
opts := &model.Options{MaxThinkingTokens: 9000, MCPTools: []string{"foo", "bar"}}
cmd := buildCommand("/usr/bin/claude", "hello", opts)
expect := []string{"/usr/bin/claude", "--output-format", "stream-json", "--verbose", "--max-thinking-tokens", "9000", "--mcpTools", "foo,bar", "--print", "hello"}
if !reflect.DeepEqual(cmd, expect) {
t.Fatalf("unexpected cmd: %v", cmd)
}
Expand Down
1 change: 1 addition & 0 deletions model/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,7 @@ type Options struct {
AppendSystemPrompt string
MCPTools []string
MCPServers map[string]MCPServerConfig
CLIPath string
PermissionMode PermissionMode
ContinueConversation bool
Resume string
Expand Down
Loading