Skip to content
Closed
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
40 changes: 24 additions & 16 deletions APPLYWITHLLM_QUICKSTART.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,19 +7,21 @@ The `applywithllm` command is now part of Shepherd. It has been fully integrated
### Prerequisites

1. **Node.js 18+** (for built-in fetch support)
2. **OpenAI API Key** - Get one from [platform.openai.com](https://platform.openai.com/api-keys)
2. **LLM API Key** - Get one from [platform.openai.com](https://platform.openai.com/api-keys) (OpenAI) or [console.groq.com](https://console.groq.com/keys) (Groq)
3. **Git** - Must be installed on your system

### Configuration

Set your OpenAI API key as an environment variable:
Set your LLM API key as an environment variable (choose OpenAI or Groq):

```bash
# Export the API key (add to .bashrc or .zshrc for persistence)
export GROQ_API_KEY="sk-your-openai-api-key-here"
# Option 1: OpenAI (recommended for best results)
export OPENAI_API_KEY="sk-your-openai-key-here"
export OPENAI_MODEL="gpt-4" # or gpt-4-turbo, gpt-3.5-turbo, etc. (defaults to gpt-3.5-turbo)

# Optionally set the model (defaults to gpt-4)
export GROQ_MODEL="gpt-4-turbo" # or gpt-4, gpt-3.5-turbo, etc.
# Option 2: Groq (faster, open-source models)
export GROQ_API_KEY="gsk_your-groq-key-here"
export GROQ_MODEL="llama-3.3-70b-versatile" # or mixtral-8x7b-32768, etc. (defaults to llama-3.3-70b-versatile)
```

## Command Syntax
Expand Down Expand Up @@ -51,7 +53,7 @@ shepherd applywithllm my-migration "@files src/utils.ts Convert callback functio
What happens:

1. Reads `src/utils.ts` from each checked-out repository
2. Sends it to OpenAI with your refactoring instructions
2. Sends it to the LLM (OpenAI or Groq) with your refactoring instructions
3. Receives unified diffs back
4. Validates diffs using `git apply --check`
5. Applies the changes to your repositories
Expand Down Expand Up @@ -103,7 +105,7 @@ INPUT (Natural Language Prompt + Files)
3. READ: Load file contents
4. CALL LLM: Send prompt + context to OpenAI
4. CALL LLM: Send prompt + context to LLM provider (OpenAI or Groq)
5. VALIDATE DIFFS: Check patches with git apply --check
Expand All @@ -130,7 +132,7 @@ Repositories are **automatically reset** on failure, ensuring no partial changes
Migrate from React class components to hooks:

```bash
export GROQ_API_KEY="sk-..."
export OPENAI_API_KEY="sk-..." # or GROQ_API_KEY="gsk_..." for Groq
shepherd applywithllm react-hooks-migration "@files src/components/UserProfile.tsx,src/components/Header.tsx \
Convert these React class components to functional components with hooks. \
Use useState for state management and useEffect for lifecycle methods."
Expand Down Expand Up @@ -219,11 +221,13 @@ shepherd pr my-migration

## Troubleshooting

### "GROQ_API_KEY environment variable is not set"
### "No LLM API key found" error

```bash
# Solution: Export your API key
export GROQ_API_KEY="sk-your-key"
# Solution: Export either OpenAI or Groq API key
export OPENAI_API_KEY="sk-your-openai-key" # OpenAI format starts with "sk-"
# OR
export GROQ_API_KEY="gsk_your-groq-key" # Groq format starts with "gsk_"
```

### "Diff validation failed"
Expand All @@ -245,10 +249,14 @@ Ensure:

### "LLM API error"

- Check your API key is valid
- Check your OpenAI account has credits
- Check your API key is valid and in the correct format:
- OpenAI keys start with `sk-`
- Groq keys start with `gsk_`
- Check your account has credits/quota
- Check network connectivity
- Verify GROQ_MODEL is valid (gpt-4, gpt-3.5-turbo, etc.)
- Verify model name is valid:
- OpenAI: gpt-4, gpt-4-turbo, gpt-3.5-turbo, etc.
- Groq: llama-3.3-70b-versatile, mixtral-8x7b-32768, etc.

## Best Practices

Expand Down Expand Up @@ -307,7 +315,7 @@ Plan accordingly for batch migrations on many repositories.

Current limitations:

- Single LLM provider (OpenAI only, for now)
- Two LLM providers supported (OpenAI and Groq)
- Sequential processing (one repo at a time)
- No caching between runs

Expand Down
47 changes: 18 additions & 29 deletions src/commands/applywithllm.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,9 @@ import { IMigrationContext } from '../migration-context';
import mockAdapter from '../adapters/adapter.mock';
import mockLogger from '../logger/logger.mock';
import * as llmService from '../services/llm';
import * as gitDiff from '../util/git-diff';
import fs from 'fs-extra';

jest.mock('../services/llm');
jest.mock('../util/git-diff');
jest.mock('fs-extra');

// Mock process.exit globally - don't throw, just return
Expand Down Expand Up @@ -51,30 +49,16 @@ describe('applywithllm command', () => {

// Default mock implementations
mockAdapter.getRepoDir.mockReturnValue('/tmp/repo1');
mockAdapter.resetChangedFiles.mockResolvedValue(undefined);
(fs.pathExists as jest.Mock).mockResolvedValue(true);
(fs.writeFile as jest.Mock).mockResolvedValue(undefined);
(llmService.readFilesForContext as jest.Mock).mockResolvedValue([
{ path: 'file1.ts', content: 'const x = 1;' },
]);
(gitDiff.validateDiff as jest.Mock).mockResolvedValue({
valid: true,
errors: [],
warnings: [],
});
(gitDiff.applyDiff as jest.Mock).mockResolvedValue(undefined);
(gitDiff.parseDiffStats as jest.Mock).mockReturnValue({
additions: 1,
deletions: 1,
});
(gitDiff.extractFilePaths as jest.Mock).mockReturnValue(['file1.ts']);

const mockProvider = {
callLLM: jest.fn().mockResolvedValue({
diffs: `--- a/file1.ts
+++ b/file1.ts
@@ -1 +1 @@
-const x = 1;
+const x = 2;
`,
diffs: 'const x = 2;',
}),
};
(llmService.getLLMProvider as jest.Mock).mockReturnValue(mockProvider);
Expand Down Expand Up @@ -121,22 +105,27 @@ describe('applywithllm command', () => {

await applywithllm(mockContext, options, prompt);

// validateDiff should be called but not applyDiff
expect(gitDiff.validateDiff).toHaveBeenCalled();
expect(mockLogger.info).toHaveBeenCalled();
});

it('should reset repo on validation failure', async () => {
it('should handle empty LLM response', async () => {
const prompt = '@files file1.ts\nRefactor this file';
(gitDiff.validateDiff as jest.Mock).mockResolvedValueOnce({
valid: false,
errors: ['Patch does not apply'],
warnings: [],
});
const mockProvider = {
callLLM: jest.fn().mockResolvedValue({
diffs: '',
}),
};
(llmService.getLLMProvider as jest.Mock).mockReturnValue(mockProvider);

await applywithllm(mockContext, options, prompt);

// Should reset the repo on failure
expect(mockAdapter.resetChangedFiles).toHaveBeenCalled();
// Should not write the file content when response is empty (only the response JSON is written)
// Check that writeFile was only called once for the response JSON, not for the actual file
const writeFileCalls = (fs.writeFile as jest.Mock).mock.calls;
const fileContentWriteCalls = writeFileCalls.filter(
(call) => call[0] === '/tmp/repo1/file1.ts'
);
expect(fileContentWriteCalls.length).toBe(0);
});

it('should handle LLM API errors gracefully', async () => {
Expand Down
2 changes: 1 addition & 1 deletion src/commands/applywithllm.ts
Original file line number Diff line number Diff line change
Expand Up @@ -211,7 +211,7 @@ export default async (
// Original repo-based mode
const repos = migration.repos || [];

console.log('Applying migration with LLM to repos:', repos);
logger.info('Applying migration with LLM to repos:', repos);

if (!process.env.GROQ_API_KEY && !process.env.OPENAI_API_KEY) {
logger.error('Either GROQ_API_KEY or OPENAI_API_KEY must be set');
Expand Down
4 changes: 3 additions & 1 deletion src/services/llm.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,9 @@ describe('LLM Service', () => {

describe('getLLMProvider', () => {
it('should throw error when API key is not provided', () => {
expect(() => getLLMProvider()).toThrow('Groq API key not provided');
expect(() => getLLMProvider()).toThrow(
'No LLM API key found. Set OPENAI_API_KEY or GROQ_API_KEY environment variable.'
);
});

it('should return provider with provided API key', () => {
Expand Down