Skip to content

Claude code Integration Feature and Integration Tests#2059

Draft
adamwrose wants to merge 8 commits intodanielmiessler:mainfrom
adamwrose:feat/claudecode-integration-tests
Draft

Claude code Integration Feature and Integration Tests#2059
adamwrose wants to merge 8 commits intodanielmiessler:mainfrom
adamwrose:feat/claudecode-integration-tests

Conversation

@adamwrose
Copy link
Copy Markdown

What this Pull Request (PR) does

Add an integration to claude code.

  • I was introduced to your project from NetworkChuck and became facinated with the work of fabric. I currently have an github copilot and claude (Anthropic pro) subscription. I noticed that the anthropic API was charged differently and claude code could use the Subscription vs paying $5 per x many tokens. I hope this will make it to where this is useful for more users with a tighter budget. If you like this, I can make one for opencode as well.

  • prerequisites

    • install claude code from either npm or brew.
    • have an anthropic subscription.

Related issues

Please reference any open issues this PR relates to in here.
If it closes an issue, type closes #[ISSUE_NUMBER].

Screenshots

I have written a lot of tests to validate my work and done some manual testing as well.

Great Work,
Adam

@ksylvan
Copy link
Copy Markdown
Collaborator

ksylvan commented Mar 15, 2026

@adamwrose Did Claude write this? What is the problem this is solving? How would I run it and test it?

@ksylvan ksylvan marked this pull request as draft March 15, 2026 01:29
@ksylvan ksylvan self-assigned this Mar 15, 2026
…ns` in tests

- Replace custom `contains()` with stdlib `slices.Contains` in tests
- Remove redundant `contains` helper function from test file
- Add `slices` package import to test file
@ksylvan ksylvan force-pushed the feat/claudecode-integration-tests branch from b6e8cdc to 6ab52ea Compare March 15, 2026 23:02
@ksylvan
Copy link
Copy Markdown
Collaborator

ksylvan commented Mar 15, 2026

@adamwrose Your PR description has a piece of a conversation (with some large language model?) in it....

I have written a lot of tests to validate my work and done some manual testing as well.

Great Work,
Adam

@adamwrose
Copy link
Copy Markdown
Author

I have not forgotten about this. I have been doing research on this for the last couple of weeks. There are opensource projects that expose claude code as a service that fabric and openclaw can talk to. What do you think. Want me to clean this up where AI helped me on the test cases and commit or do you want me to document a way to use claude code with an open source API?

@ksylvan
Copy link
Copy Markdown
Collaborator

ksylvan commented Mar 31, 2026

@adamwrose Thanks for your response. I'd love for us to be able to use claude in this way without running afoul of the Anthropic Terms of Service. As far as I understand it, those claude code proxies are a gray area.

Tell me what you're thinking, let's discuss!


binary := c.getBinary()
debuglog.Debug(debuglog.Detailed, "ClaudeCode SendStream launching: %s %s\n", binary, strings.Join(args, " "))
cmd := exec.Command(binary, args...)
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

[Major] SendStream uses exec.Command but Send uses exec.CommandContext. If the claude subprocess hangs or the caller disconnects, this process cannot be killed — it will orphan.

Suggestion: Use exec.CommandContext with an internal context.WithCancel or context.WithTimeout, consistent with Send() at line 183.

Comment on lines +175 to +177
if userPrompt == "" {
return "", nil
}
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

[Major] When userPrompt is empty after message extraction, Send returns ("", nil) — a successful result with no content. Callers cannot distinguish "model returned nothing" from "no input was provided."

Suggestion: Return an error instead:

return "", fmt.Errorf("claude: no prompt content after message extraction")

Comment on lines +78 to +96
func (c *Client) ListModels() ([]string, error) {
return []string{
"claude-opus-4-6",
"claude-sonnet-4-6",
"claude-opus-4-5-20251101",
"claude-opus-4-5",
"claude-haiku-4-5",
"claude-haiku-4-5-20251001",
"claude-sonnet-4-5",
"claude-sonnet-4-5-20250929",
"claude-opus-4-1-20250805",
"claude-sonnet-4-20250514",
"claude-sonnet-4-0",
"claude-4-sonnet-20250514",
"claude-opus-4-0",
"claude-opus-4-20250514",
"claude-4-opus-20250514",
}, nil
}
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

[Suggestion] This static model list will become outdated as Anthropic releases new models. Consider querying claude --list-models dynamically (if the CLI supports it), falling back to this static list on error.

Comment on lines +151 to +171
func (c *Client) buildArgs(opts *domain.ChatOptions, system string) []string {
args := []string{"--print", "--no-session-persistence"}
if opts.Model != "" {
args = append(args, "--model", opts.Model)
}
if system != "" {
args = append(args, "--system-prompt", system)
}
switch opts.Thinking {
case domain.ThinkingLow:
args = append(args, "--effort", "low")
case domain.ThinkingMedium:
args = append(args, "--effort", "medium")
case domain.ThinkingHigh:
args = append(args, "--effort", "high")
}
if opts.ImageFile != "" {
args = append(args, "--add-dir", filepath.Dir(opts.ImageFile))
}
return args
}
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

[Minor] ChatOptions fields like Temperature, TopP, MaxTokens, and Seed are silently dropped. Consider passing --max-tokens if the CLI supports it, and logging a debug warning for unsupported options so users aren't confused when their settings have no effect.

Comment on lines +244 to +248
channel <- domain.StreamUpdate{
Type: domain.StreamTypeContent,
Content: text,
}
}
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

[Minor] If the channel consumer stops reading, this send blocks the goroutine forever. A select with a ctx.Done() channel would enable graceful shutdown (this ties into the exec.CommandContext suggestion above).

args := c.buildArgs(opts, system)
args = append(args, userPrompt)
binary := c.getBinary()
debuglog.Debug(debuglog.Detailed, "ClaudeCode Send launching: %s %s\n", binary, strings.Join(args, " "))
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

[Minor] Full command args (including the user prompt) are logged at debug level. For long prompts this could be noisy or leak sensitive content into logs. Consider truncating the prompt portion.

@@ -0,0 +1,293 @@
package claudecode
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

[Suggestion] Missing package-level doc comment. A brief // Package claudecode ... would help with go doc and IDE hover.

Comment on lines +235 to +248
for scanner.Scan() {
line := strings.TrimSpace(scanner.Text())
if line == "" {
continue
}
text, ok := parseStreamDelta(line)
if !ok {
continue
}
channel <- domain.StreamUpdate{
Type: domain.StreamTypeContent,
Content: text,
}
}
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

[Suggestion] The message event from Claude's stream-json output contains token usage metadata (input_tokens, output_tokens). Currently this is discarded. Forwarding it as a StreamTypeUsage update would give callers visibility into costs.

Comment on lines +79 to +112
func buildGoBinary(t *testing.T, relativeDir string) string {
t.Helper()

entryAny, _ := binaryCache.LoadOrStore(relativeDir, &builtBinary{})
entry := entryAny.(*builtBinary)

entry.once.Do(func() {
outputDir, err := os.MkdirTemp("", "fabric-binary-*")
if err != nil {
entry.err = err
return
}

binaryName := filepath.Base(relativeDir)
if runtime.GOOS == "windows" {
binaryName += ".exe"
}
entry.path = filepath.Join(outputDir, binaryName)

cmd := exec.Command("go", "build", "-o", entry.path, "./"+filepath.ToSlash(relativeDir))
cmd.Dir = repoRoot(t)

var stderr bytes.Buffer
cmd.Stderr = &stderr
entry.err = cmd.Run()
entry.log = stderr.String()
})

if entry.err != nil {
t.Fatalf("build %s: %v\n%s", relativeDir, entry.err, entry.log)
}

return entry.path
}
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

[Suggestion] buildGoBinary compiles Go binaries at test time. While the sync.Once caching is smart, this still adds significant CI wall time. Consider a TestMain that pre-builds all needed binaries, or a Makefile target so CI can cache them between runs.

return []string{"CLAUDECODE_BINARY_PATH=" + wrapperPath}
}

func TestIntegrationClaudeCodePatternExecution(t *testing.T) {
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

[Minor] Integration tests don't set per-test timeouts. If a Claude API call hangs, the entire CI job blocks until the global timeout. Consider adding t.Deadline() checks or explicit context.WithTimeout for each test (e.g. 2-5 minutes).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants