Skip to content
Open
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
75 changes: 75 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -275,6 +275,81 @@ interface RalphLoopAgentResult {

Runs non-streaming iterations until verification passes, then streams the final one. Returns `StreamTextResult`.

## Using Claude Max Subscription (No API Key)

Ralph Loop Agent supports running without an API key by using the official [Claude Agent SDK](https://github.com/anthropics/claude-agent-sdk-typescript). This spawns Claude Code as a subprocess, authenticating with your Claude account.

### Installation

```bash
npm install ralph-loop-agent @anthropic-ai/claude-agent-sdk
```

Make sure you're authenticated with Claude Code:
```bash
claude auth login
```

### Usage

```typescript
import { runWithClaudeCode } from 'ralph-loop-agent/providers/claude-code';

const result = await runWithClaudeCode({
prompt: 'Fix all TypeScript errors in the project',
maxTurns: 10,
permissionMode: 'acceptEdits', // auto-accept file edits

verifyCompletion: async ({ text }) => ({
complete: text.includes('All errors fixed'),
reason: 'Errors remain',
}),

onTurnStart: ({ turn }) => console.log(`Starting turn ${turn}`),
onTurnEnd: ({ turn }) => console.log(`Completed turn ${turn}`),
});

console.log(`Completed in ${result.turns} turns`);
console.log(`Reason: ${result.completionReason}`);
console.log(`Cost: $${result.cost?.toFixed(4)} (covered by subscription)`);
```

### API Reference

#### `runWithClaudeCode(options)`

| Option | Type | Default | Description |
|--------|------|---------|-------------|
| `prompt` | `string` | **required** | The task to complete |
| `maxTurns` | `number` | `10` | Maximum turns before stopping |
| `model` | `string` | CLI default | Claude model to use |
| `cwd` | `string` | `process.cwd()` | Working directory |
| `permissionMode` | `'default' \| 'acceptEdits' \| 'bypassPermissions'` | `'acceptEdits'` | How to handle permissions |
| `systemPromptAppend` | `string` | - | Text to append to system prompt |
| `verifyCompletion` | `function` | - | Verification callback |
| `onTurnStart` | `function` | - | Called at turn start |
| `onTurnEnd` | `function` | - | Called at turn end |

#### Return Value

```typescript
interface ClaudeCodeResult {
text: string; // Final output text
turns: number; // Turns executed
completionReason: 'verified' | 'max-turns' | 'error';
reason?: string; // Reason from verification
cost?: number; // Cost (covered by subscription)
allMessages: SDKMessage[]; // All SDK messages
}
```

### Benefits

- **No API key required** - Uses your Claude Max subscription
- **Full Claude Code tools** - Access to Bash, Read, Edit, Grep, etc.
- **Permission control** - Auto-accept edits or prompt for approval
- **Cost tracking** - See usage (covered by subscription, not billed separately)

## License

Apache-2.0
7 changes: 7 additions & 0 deletions packages/ralph-loop-agent/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,10 @@
".": {
"types": "./dist/index.d.ts",
"import": "./dist/index.js"
},
"./providers/claude-code": {
"types": "./dist/providers/index.d.ts",
"import": "./dist/providers/index.js"
}
},
"files": [
Expand All @@ -37,6 +41,9 @@
},
"peerDependencies": {
"zod": "^4.0.0"
},
"optionalDependencies": {
"@anthropic-ai/claude-agent-sdk": "^0.2.0"
}
}

280 changes: 280 additions & 0 deletions packages/ralph-loop-agent/src/providers/claude-code-provider.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,280 @@
/**
* Claude Code Provider for Ralph Loop Agent
*
* This provider allows using your Claude Max subscription instead of API keys
* by spawning Claude Code as a subprocess via the official Claude Agent SDK.
*
* Prerequisites:
* - Claude Code CLI installed and authenticated (`claude auth login`)
* - @anthropic-ai/claude-agent-sdk package installed
*
* @example
* ```typescript
* import { runWithClaudeCode } from 'ralph-loop-agent/providers/claude-code';
*
* const result = await runWithClaudeCode({
* prompt: 'Fix all TypeScript errors in the project',
* maxTurns: 10,
* tools: { readFile, writeFile },
* verifyCompletion: async ({ text }) => ({
* complete: text.includes('All errors fixed'),
* }),
* });
* ```
*/

import { query, type SDKMessage, type SDKResultMessage, type Options as SDKOptions } from '@anthropic-ai/claude-agent-sdk';

/**
* Options for running with Claude Code subscription
*/
export interface ClaudeCodeOptions {
/**
* The prompt/task to complete
*/
prompt: string;

/**
* Maximum number of turns (iterations) before stopping
* @default 10
*/
maxTurns?: number;

/**
* Model to use. If omitted, uses Claude Code's default.
*/
model?: string;

/**
* Working directory for the agent
* @default process.cwd()
*/
cwd?: string;

/**
* Permission mode for tool usage
* - 'default': Prompts for dangerous operations
* - 'acceptEdits': Auto-accept file edits
* - 'bypassPermissions': Skip all permission checks (dangerous)
* @default 'acceptEdits'
*/
permissionMode?: 'default' | 'acceptEdits' | 'bypassPermissions';

/**
* System prompt to append to Claude Code's default
*/
systemPromptAppend?: string;

/**
* Verification function called after each turn
*/
verifyCompletion?: (params: {
text: string;
turn: number;
allMessages: SDKMessage[];
}) => Promise<VerifyResult> | VerifyResult;

/**
* Callback at start of each turn
*/
onTurnStart?: (params: { turn: number }) => void | Promise<void>;

/**
* Callback at end of each turn
*/
onTurnEnd?: (params: { turn: number; message: SDKMessage }) => void | Promise<void>;

/**
* Additional SDK options passed to the Claude Agent SDK
*/
sdkOptions?: Partial<SDKOptions>;
}

export interface VerifyResult {
complete: boolean;
reason?: string;
}

export interface ClaudeCodeResult {
/**
* The final text output
*/
text: string;

/**
* Number of turns executed
*/
turns: number;

/**
* Why the loop stopped
*/
completionReason: 'verified' | 'max-turns' | 'error';

/**
* Reason message from verification
*/
reason?: string;

/**
* Total cost in USD (from Claude subscription, not billed separately)
*/
cost?: number;

/**
* All SDK messages from the execution
*/
allMessages: SDKMessage[];
}

/**
* Run a task using Claude Code subscription (no API key required)
*
* This function spawns Claude Code as a subprocess, using your authenticated
* Claude account. All usage is covered by your Claude Max subscription.
*
* @example
* ```typescript
* const result = await runWithClaudeCode({
* prompt: 'Create a new React component',
* maxTurns: 5,
* verifyCompletion: ({ text }) => ({
* complete: text.includes('Component created'),
* }),
* });
* ```
*/
export async function runWithClaudeCode(options: ClaudeCodeOptions): Promise<ClaudeCodeResult> {
const {
prompt,
maxTurns = 10,
model,
cwd = process.cwd(),
permissionMode = 'acceptEdits',
systemPromptAppend,
verifyCompletion,
onTurnStart,
onTurnEnd,
sdkOptions = {},
} = options;

const allMessages: SDKMessage[] = [];
let turn = 0;
let lastText = '';
let completionReason: ClaudeCodeResult['completionReason'] = 'max-turns';
let reason: string | undefined;
let cost: number | undefined;

// Build system prompt
const systemPrompt = systemPromptAppend
? {
type: 'preset' as const,
preset: 'claude_code' as const,
append: systemPromptAppend,
}
: undefined;

try {
const queryGenerator = query({
prompt,
options: {
maxTurns,
model,
cwd,
permissionMode,
systemPrompt,
...sdkOptions,
},
});

for await (const message of queryGenerator) {
allMessages.push(message);

// Track turns via assistant messages
if (message.type === 'assistant') {
turn++;
await onTurnStart?.({ turn });

// Extract text from assistant message
const content = (message as any).message?.content;
if (Array.isArray(content)) {
for (const block of content) {
if (block.type === 'text' && block.text) {
lastText = block.text;
}
}
}

await onTurnEnd?.({ turn, message });

// Check verification
if (verifyCompletion) {
const verification = await verifyCompletion({
text: lastText,
turn,
allMessages,
});

if (verification.complete) {
completionReason = 'verified';
reason = verification.reason;
await queryGenerator.interrupt();
break;
}

// Feedback is handled by continuing the loop
}
}

// Handle final result
if (message.type === 'result') {
const resultMsg = message as SDKResultMessage;

if (resultMsg.subtype === 'success') {
lastText = resultMsg.result;
cost = resultMsg.total_cost_usd;
if (completionReason !== 'verified') {
completionReason = 'verified';
}
} else if (resultMsg.subtype === 'error_max_turns') {
completionReason = 'max-turns';
cost = resultMsg.total_cost_usd;
} else {
completionReason = 'error';
reason = resultMsg.errors?.join(', ');
cost = resultMsg.total_cost_usd;
}
}
}
} catch (error: any) {
return {
text: lastText,
turns: turn,
completionReason: 'error',
reason: error.message,
allMessages,
};
}

return {
text: lastText,
turns: turn,
completionReason,
reason,
cost,
allMessages,
};
}

/**
* Check if Claude Code is available (authenticated)
*/
export async function isClaudeCodeAvailable(): Promise<boolean> {
try {
// Try to import the SDK - if it fails, not installed
await import('@anthropic-ai/claude-agent-sdk');
return true;
} catch {
return false;
}
}
7 changes: 7 additions & 0 deletions packages/ralph-loop-agent/src/providers/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
export {
runWithClaudeCode,
isClaudeCodeAvailable,
type ClaudeCodeOptions,
type ClaudeCodeResult,
type VerifyResult,
} from './claude-code-provider';