diff --git a/claudetool/bash.go b/claudetool/bash.go index dd21769566e1afbf16e45f40b51b575787312ce9..f50d4cbdadda0e165c2700adf0fb664ea8b3a66d 100644 --- a/claudetool/bash.go +++ b/claudetool/bash.go @@ -36,6 +36,9 @@ type BashTool struct { WorkingDir *MutableWorkingDir // LLMProvider provides access to LLM services for tool validation LLMProvider LLMServiceProvider + // ConversationID is the ID of the conversation this tool belongs to. + // It is exposed to invoked commands via SHELLEY_CONVERSATION_ID. + ConversationID string } const ( @@ -256,6 +259,9 @@ func (b *BashTool) makeBashCommand(ctx context.Context, command string, out io.W }) env = append(env, "SKETCH=1") // signal that this has been run by Sketch, sometimes useful for scripts env = append(env, "EDITOR=/bin/false") // interactive editors won't work + if b.ConversationID != "" { + env = append(env, "SHELLEY_CONVERSATION_ID="+b.ConversationID) + } cmd.Env = env return cmd } diff --git a/claudetool/bash_test.go b/claudetool/bash_test.go index b3c377827b4d51d3aece7fb2d0022fd37c6a0b02..deb5d36c5850879ec9a89d333d6c7c519dbfe490 100644 --- a/claudetool/bash_test.go +++ b/claudetool/bash_test.go @@ -223,6 +223,45 @@ func TestExecuteBash(t *testing.T) { } }) + // Test SHELLEY_CONVERSATION_ID environment variable is set when configured + t.Run("SHELLEY_CONVERSATION_ID Environment Variable", func(t *testing.T) { + bashWithConvID := &BashTool{ + WorkingDir: NewMutableWorkingDir("/"), + ConversationID: "test-conv-123", + } + req := bashInput{ + Command: "echo $SHELLEY_CONVERSATION_ID", + } + + output, err := bashWithConvID.executeBash(ctx, req, 5*time.Second) + if err != nil { + t.Fatalf("Unexpected error: %v", err) + } + + want := "test-conv-123\n" + if output != want { + t.Errorf("Expected SHELLEY_CONVERSATION_ID=test-conv-123, got %q", output) + } + }) + + // Test SHELLEY_CONVERSATION_ID is not set when not configured + t.Run("SHELLEY_CONVERSATION_ID Not Set When Empty", func(t *testing.T) { + req := bashInput{ + Command: "echo \"conv_id:$SHELLEY_CONVERSATION_ID:\"", + } + + output, err := bashTool.executeBash(ctx, req, 5*time.Second) + if err != nil { + t.Fatalf("Unexpected error: %v", err) + } + + // Should be empty since ConversationID is not set on bashTool + want := "conv_id::\n" + if output != want { + t.Errorf("Expected empty SHELLEY_CONVERSATION_ID, got %q", output) + } + }) + // Test command with output to stderr t.Run("Command with stderr", func(t *testing.T) { req := bashInput{ diff --git a/claudetool/toolset.go b/claudetool/toolset.go index 061bc4303d496fa5f82ffb64bebb8c7b3f9b017a..a51eb8251aab970c6c347ecffdce0c7c5bdc7837 100644 --- a/claudetool/toolset.go +++ b/claudetool/toolset.go @@ -57,6 +57,9 @@ type ToolSetConfig struct { SubagentDB SubagentDB // ParentConversationID is the ID of the parent conversation (for subagent tool). ParentConversationID string + // ConversationID is the ID of the conversation these tools belong to. + // This is exposed to bash commands via the SHELLEY_CONVERSATION_ID environment variable. + ConversationID string } // ToolSet holds a set of tools for a single conversation. @@ -102,6 +105,7 @@ func NewToolSet(ctx context.Context, cfg ToolSetConfig) *ToolSet { WorkingDir: wd, LLMProvider: cfg.LLMProvider, EnableJITInstall: cfg.EnableJITInstall, + ConversationID: cfg.ConversationID, } // Use simplified patch schema for weaker models, full schema for sonnet/opus diff --git a/server/convo.go b/server/convo.go index 68cfebe20b387c039673a9ad4dc30581e70a97d7..534c741c3f5225436da4a4c0bfb8fc31ca2f914d 100644 --- a/server/convo.go +++ b/server/convo.go @@ -363,6 +363,7 @@ func (cm *ConversationManager) ensureLoop(service llm.Service, modelID string) e // Create tools for this conversation with the conversation's working directory toolSetConfig.WorkingDir = cwd toolSetConfig.ModelID = modelID + toolSetConfig.ConversationID = conversationID toolSetConfig.ParentConversationID = conversationID // For subagent tool toolSetConfig.OnWorkingDirChange = func(newDir string) { // Persist working directory change to database