threewzrd 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 dogmandcl
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,123 @@
1
+ # threewzrd
2
+
3
+ AI-powered CLI for generating Three.js projects from natural language.
4
+
5
+ Describe what you want to build and let the AI create complete, runnable Three.js scenes for you.
6
+
7
+ ## Installation
8
+
9
+ ```bash
10
+ npm install -g threewzrd
11
+ ```
12
+
13
+ Requires Node.js 18 or higher.
14
+
15
+ ## Quick Start
16
+
17
+ 1. Get an API key from [Anthropic Console](https://console.anthropic.com/)
18
+
19
+ 2. Start the wizard:
20
+ ```bash
21
+ threewzrd start
22
+ ```
23
+
24
+ 3. Enter your API key when prompted (it will be saved securely for future sessions)
25
+
26
+ 4. Describe what you want to build:
27
+ ```
28
+ > Create a rotating cube with a gradient shader
29
+ ```
30
+
31
+ The wizard will generate all necessary files (HTML, JavaScript, shaders) and guide you through running them.
32
+
33
+ ## Commands
34
+
35
+ ### `threewzrd start [directory]`
36
+
37
+ Start an interactive session in the specified directory (defaults to current directory).
38
+
39
+ ```bash
40
+ threewzrd start
41
+ threewzrd start ./my-project
42
+ ```
43
+
44
+ ### `threewzrd config`
45
+
46
+ View current configuration.
47
+
48
+ ```bash
49
+ threewzrd config
50
+ ```
51
+
52
+ ### `threewzrd config --set`
53
+
54
+ Set or update your API key.
55
+
56
+ ```bash
57
+ threewzrd config --set
58
+ ```
59
+
60
+ ### `threewzrd config --delete`
61
+
62
+ Delete your saved API key.
63
+
64
+ ```bash
65
+ threewzrd config --delete
66
+ ```
67
+
68
+ ### `threewzrd config --path`
69
+
70
+ Show the config file location.
71
+
72
+ ```bash
73
+ threewzrd config --path
74
+ ```
75
+
76
+ ## API Key Setup
77
+
78
+ Your API key is stored securely at `~/.threewzrd/.env` with restricted permissions (owner read/write only).
79
+
80
+ You can also set it via environment variable:
81
+
82
+ ```bash
83
+ export ANTHROPIC_API_KEY=sk-ant-...
84
+ threewzrd start
85
+ ```
86
+
87
+ Or create a `.env` file in your project directory:
88
+
89
+ ```
90
+ ANTHROPIC_API_KEY=sk-ant-...
91
+ ```
92
+
93
+ ## Features
94
+
95
+ - **Natural Language Input**: Describe 3D scenes in plain English
96
+ - **Complete Project Generation**: Creates HTML, JavaScript, and shader files
97
+ - **Interactive REPL**: Iterate on your designs with follow-up requests
98
+ - **File Management**: Automatically organizes generated files
99
+ - **Secure**: API keys are masked during input and stored with restricted permissions
100
+ - **Command Safety**: Only whitelisted commands can be executed
101
+
102
+ ## Examples
103
+
104
+ ```
105
+ > Create a particle system that looks like falling snow
106
+
107
+ > Make a 3D solar system with orbiting planets
108
+
109
+ > Build a terrain with procedural noise and water
110
+
111
+ > Create a first-person camera controller
112
+ ```
113
+
114
+ ## Security
115
+
116
+ - API keys are stored with `0600` permissions (owner read/write only)
117
+ - Input is masked when entering API keys
118
+ - Commands are restricted to a safe whitelist (npm, npx, node, git, etc.)
119
+ - File operations are sandboxed to the working directory
120
+
121
+ ## License
122
+
123
+ MIT
package/dist/cli.d.ts ADDED
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
package/dist/cli.js ADDED
@@ -0,0 +1,33 @@
1
+ #!/usr/bin/env node
2
+ import { Command } from 'commander';
3
+ import { homedir } from 'os';
4
+ import { startCommand } from './commands/start.js';
5
+ import { configCommand } from './commands/config.js';
6
+ // Safely get current working directory, fallback to home
7
+ function safeGetCwd() {
8
+ try {
9
+ return process.cwd();
10
+ }
11
+ catch {
12
+ // If cwd is inaccessible (deleted directory, etc.), use home
13
+ return homedir();
14
+ }
15
+ }
16
+ const program = new Command();
17
+ program
18
+ .name('threewzrd')
19
+ .description('AI-powered CLI for generating Three.js projects from natural language')
20
+ .version('1.0.0');
21
+ program
22
+ .command('start')
23
+ .description('Start the wizard REPL')
24
+ .option('-d, --directory <path>', 'Working directory for the wizard', safeGetCwd())
25
+ .action(startCommand);
26
+ program
27
+ .command('config')
28
+ .description('Manage API key and configuration')
29
+ .option('-s, --set', 'Set or update API key')
30
+ .option('-d, --delete', 'Delete saved API key')
31
+ .option('-p, --path', 'Show config file path')
32
+ .action(configCommand);
33
+ program.parse();
@@ -0,0 +1,7 @@
1
+ interface ConfigOptions {
2
+ set?: boolean;
3
+ delete?: boolean;
4
+ path?: boolean;
5
+ }
6
+ export declare function configCommand(options: ConfigOptions): Promise<void>;
7
+ export {};
@@ -0,0 +1,159 @@
1
+ import { homedir } from 'os';
2
+ import { join } from 'path';
3
+ import { mkdir, writeFile, readFile, unlink } from 'fs/promises';
4
+ import * as readline from 'readline';
5
+ import chalk from 'chalk';
6
+ const CONFIG_DIR = join(homedir(), '.threewzrd');
7
+ const CONFIG_PATH = join(CONFIG_DIR, '.env');
8
+ async function promptInput(question, masked = false) {
9
+ if (!masked) {
10
+ const rl = readline.createInterface({
11
+ input: process.stdin,
12
+ output: process.stdout,
13
+ });
14
+ return new Promise((resolve) => {
15
+ rl.question(question, (answer) => {
16
+ rl.close();
17
+ resolve(answer.trim());
18
+ });
19
+ });
20
+ }
21
+ // Masked input for sensitive data
22
+ return new Promise((resolve) => {
23
+ const stdin = process.stdin;
24
+ const stdout = process.stdout;
25
+ stdout.write(question);
26
+ const wasRaw = stdin.isRaw;
27
+ stdin.setRawMode(true);
28
+ stdin.resume();
29
+ stdin.setEncoding('utf8');
30
+ let input = '';
31
+ const onData = (char) => {
32
+ const code = char.charCodeAt(0);
33
+ // Enter key
34
+ if (code === 13 || code === 10) {
35
+ stdin.setRawMode(wasRaw ?? false);
36
+ stdin.removeListener('data', onData);
37
+ stdin.pause();
38
+ stdout.write('\n');
39
+ resolve(input.trim());
40
+ return;
41
+ }
42
+ // Ctrl+C
43
+ if (code === 3) {
44
+ stdin.setRawMode(wasRaw ?? false);
45
+ stdin.removeListener('data', onData);
46
+ stdout.write('\n');
47
+ process.exit(1);
48
+ }
49
+ // Backspace
50
+ if (code === 127 || code === 8) {
51
+ if (input.length > 0) {
52
+ input = input.slice(0, -1);
53
+ stdout.write('\b \b');
54
+ }
55
+ return;
56
+ }
57
+ // Regular character
58
+ if (code >= 32 && code <= 126) {
59
+ input += char;
60
+ stdout.write('*');
61
+ }
62
+ };
63
+ stdin.on('data', onData);
64
+ });
65
+ }
66
+ async function showConfig() {
67
+ console.log();
68
+ console.log(chalk.cyan(' Configuration'));
69
+ console.log(chalk.gray(' ─────────────────────────────────────────'));
70
+ console.log();
71
+ console.log(chalk.gray(' Config location: ') + CONFIG_PATH);
72
+ console.log();
73
+ try {
74
+ const content = await readFile(CONFIG_PATH, 'utf-8');
75
+ const hasKey = content.includes('ANTHROPIC_API_KEY=');
76
+ if (hasKey) {
77
+ // Extract and mask the key (show first 7 and last 4 chars only)
78
+ const match = content.match(/ANTHROPIC_API_KEY=(.+)/);
79
+ if (match && match[1]) {
80
+ const key = match[1].trim();
81
+ const masked = key.length > 11
82
+ ? key.substring(0, 7) + '...' + key.substring(key.length - 4)
83
+ : '***configured***';
84
+ console.log(chalk.gray(' API Key: ') + chalk.green(masked));
85
+ }
86
+ }
87
+ else {
88
+ console.log(chalk.yellow(' No API key configured'));
89
+ }
90
+ }
91
+ catch {
92
+ console.log(chalk.yellow(' No config file found'));
93
+ }
94
+ console.log();
95
+ }
96
+ async function setApiKey() {
97
+ console.log();
98
+ const apiKey = await promptInput(chalk.magenta(' Enter new API key: '), true);
99
+ if (!apiKey) {
100
+ console.log(chalk.red(' No key provided. Cancelled.'));
101
+ return;
102
+ }
103
+ if (!apiKey.startsWith('sk-ant-')) {
104
+ console.log(chalk.yellow(' Warning: Key doesn\'t look like an Anthropic key (expected sk-ant-...)'));
105
+ const proceed = await promptInput(chalk.gray(' Save anyway? [y/N] '));
106
+ if (proceed.toLowerCase() !== 'y') {
107
+ console.log(chalk.gray(' Cancelled.'));
108
+ return;
109
+ }
110
+ }
111
+ try {
112
+ await mkdir(CONFIG_DIR, { recursive: true, mode: 0o700 });
113
+ await writeFile(CONFIG_PATH, `ANTHROPIC_API_KEY=${apiKey}\n`, { mode: 0o600 });
114
+ console.log();
115
+ console.log(chalk.green(' API key updated successfully'));
116
+ }
117
+ catch (error) {
118
+ console.log(chalk.red(' Failed to save API key'));
119
+ }
120
+ console.log();
121
+ }
122
+ async function deleteConfig() {
123
+ console.log();
124
+ const confirm = await promptInput(chalk.yellow(' Delete API key? This cannot be undone. [y/N] '));
125
+ if (confirm.toLowerCase() !== 'y') {
126
+ console.log(chalk.gray(' Cancelled.'));
127
+ return;
128
+ }
129
+ try {
130
+ await unlink(CONFIG_PATH);
131
+ console.log(chalk.green(' API key deleted'));
132
+ }
133
+ catch {
134
+ console.log(chalk.yellow(' No config file to delete'));
135
+ }
136
+ console.log();
137
+ }
138
+ async function showPath() {
139
+ console.log();
140
+ console.log(chalk.gray(' Config file: ') + CONFIG_PATH);
141
+ console.log();
142
+ console.log(chalk.gray(' To edit manually:'));
143
+ console.log(chalk.cyan(` nano ${CONFIG_PATH}`));
144
+ console.log();
145
+ }
146
+ export async function configCommand(options) {
147
+ if (options.set) {
148
+ await setApiKey();
149
+ }
150
+ else if (options.delete) {
151
+ await deleteConfig();
152
+ }
153
+ else if (options.path) {
154
+ await showPath();
155
+ }
156
+ else {
157
+ await showConfig();
158
+ }
159
+ }
@@ -0,0 +1,5 @@
1
+ interface StartOptions {
2
+ directory: string;
3
+ }
4
+ export declare function startCommand(options: StartOptions): Promise<void>;
5
+ export {};
@@ -0,0 +1,151 @@
1
+ import dotenv from 'dotenv';
2
+ import { homedir } from 'os';
3
+ import { join } from 'path';
4
+ import { mkdir, writeFile } from 'fs/promises';
5
+ import * as readline from 'readline';
6
+ import chalk from 'chalk';
7
+ import { ThreeJsWizard } from '../core/ThreeJsWizard.js';
8
+ function loadEnvFiles(workingDir) {
9
+ // Load from multiple locations (later ones don't override earlier)
10
+ // 1. Current working directory
11
+ dotenv.config({ path: join(workingDir, '.env') });
12
+ // 2. User's home config directory
13
+ dotenv.config({ path: join(homedir(), '.threewzrd', '.env') });
14
+ }
15
+ async function promptForApiKey() {
16
+ console.log();
17
+ console.log(chalk.yellow(' No API key found!'));
18
+ console.log();
19
+ console.log(chalk.gray(' To use Three.js Wizard, you need an Anthropic API key.'));
20
+ console.log(chalk.gray(' Get one at: ') + chalk.cyan('https://console.anthropic.com/'));
21
+ console.log();
22
+ // Use masked input for security
23
+ return new Promise((resolve) => {
24
+ const stdin = process.stdin;
25
+ const stdout = process.stdout;
26
+ stdout.write(chalk.magenta(' Enter your API key: '));
27
+ // Save original terminal state
28
+ const wasRaw = stdin.isRaw;
29
+ stdin.setRawMode(true);
30
+ stdin.resume();
31
+ stdin.setEncoding('utf8');
32
+ let input = '';
33
+ const onData = (char) => {
34
+ const code = char.charCodeAt(0);
35
+ // Enter key
36
+ if (code === 13 || code === 10) {
37
+ stdin.setRawMode(wasRaw ?? false);
38
+ stdin.removeListener('data', onData);
39
+ stdin.pause();
40
+ stdout.write('\n');
41
+ resolve(input.trim());
42
+ return;
43
+ }
44
+ // Ctrl+C
45
+ if (code === 3) {
46
+ stdin.setRawMode(wasRaw ?? false);
47
+ stdin.removeListener('data', onData);
48
+ stdout.write('\n');
49
+ process.exit(1);
50
+ }
51
+ // Backspace
52
+ if (code === 127 || code === 8) {
53
+ if (input.length > 0) {
54
+ input = input.slice(0, -1);
55
+ stdout.write('\b \b');
56
+ }
57
+ return;
58
+ }
59
+ // Regular character - show asterisk
60
+ if (code >= 32 && code <= 126) {
61
+ input += char;
62
+ stdout.write('*');
63
+ }
64
+ };
65
+ stdin.on('data', onData);
66
+ });
67
+ }
68
+ async function saveApiKey(apiKey) {
69
+ const configDir = join(homedir(), '.threewzrd');
70
+ const configPath = join(configDir, '.env');
71
+ try {
72
+ // Create directory with restricted permissions (owner only)
73
+ await mkdir(configDir, { recursive: true, mode: 0o700 });
74
+ // Write file with restricted permissions (owner read/write only)
75
+ await writeFile(configPath, `ANTHROPIC_API_KEY=${apiKey}\n`, { mode: 0o600 });
76
+ console.log();
77
+ console.log(chalk.green(' API key saved securely to ') + chalk.gray(configPath));
78
+ console.log(chalk.gray(' (permissions: owner read/write only)'));
79
+ console.log();
80
+ }
81
+ catch (error) {
82
+ console.log(chalk.yellow(' Could not save API key to config file.'));
83
+ console.log(chalk.gray(' You can manually create: ') + configPath);
84
+ }
85
+ }
86
+ export async function startCommand(options) {
87
+ const workingDir = options.directory;
88
+ // Always try to change to the working directory
89
+ try {
90
+ process.chdir(workingDir);
91
+ }
92
+ catch (error) {
93
+ console.error(chalk.red(`Error: Could not access directory "${workingDir}"`));
94
+ console.error(chalk.gray('Make sure the directory exists and you have permission to access it.'));
95
+ process.exit(1);
96
+ }
97
+ // Load .env files from working directory and home config
98
+ loadEnvFiles(workingDir);
99
+ // Check for API key, prompt if missing
100
+ if (!process.env.ANTHROPIC_API_KEY) {
101
+ const apiKey = await promptForApiKey();
102
+ if (!apiKey) {
103
+ console.log();
104
+ console.log(chalk.red(' No API key provided. Exiting.'));
105
+ process.exit(1);
106
+ }
107
+ // Validate key format (basic check)
108
+ if (!apiKey.startsWith('sk-ant-')) {
109
+ console.log();
110
+ console.log(chalk.yellow(' Warning: API key doesn\'t look like an Anthropic key.'));
111
+ console.log(chalk.gray(' Expected format: sk-ant-...'));
112
+ }
113
+ // Set the key for this session
114
+ process.env.ANTHROPIC_API_KEY = apiKey;
115
+ // Offer to save it
116
+ const rl = readline.createInterface({
117
+ input: process.stdin,
118
+ output: process.stdout,
119
+ });
120
+ const shouldSave = await new Promise((resolve) => {
121
+ rl.question(chalk.gray(' Save this key for future sessions? ') + chalk.gray('[Y/n] '), (answer) => {
122
+ rl.close();
123
+ const normalized = answer.trim().toLowerCase();
124
+ resolve(normalized !== 'n' && normalized !== 'no');
125
+ });
126
+ });
127
+ if (shouldSave) {
128
+ await saveApiKey(apiKey);
129
+ }
130
+ }
131
+ // Create and start the wizard
132
+ const wizard = new ThreeJsWizard();
133
+ // Handle graceful shutdown
134
+ process.on('SIGINT', () => {
135
+ console.log('\nShutting down...');
136
+ wizard.stop();
137
+ process.exit(0);
138
+ });
139
+ process.on('SIGTERM', () => {
140
+ wizard.stop();
141
+ process.exit(0);
142
+ });
143
+ // Start the REPL
144
+ try {
145
+ await wizard.start();
146
+ }
147
+ catch (error) {
148
+ console.error('Fatal error:', error);
149
+ process.exit(1);
150
+ }
151
+ }
@@ -0,0 +1,19 @@
1
+ import { ModelId } from './types.js';
2
+ import { TerminalUI } from '../ui/TerminalUI.js';
3
+ export declare class AgentEngine {
4
+ private client;
5
+ private model;
6
+ private conversationHistory;
7
+ private toolExecutor;
8
+ private ui;
9
+ constructor(ui: TerminalUI, workingDirectory: string);
10
+ private trimHistory;
11
+ private sleep;
12
+ setModel(model: ModelId): void;
13
+ getModel(): ModelId;
14
+ clearHistory(): void;
15
+ getCreatedFiles(): string[];
16
+ processMessage(userMessage: string): Promise<void>;
17
+ private runAgentLoop;
18
+ private runSingleTurn;
19
+ }
@@ -0,0 +1,157 @@
1
+ import Anthropic from '@anthropic-ai/sdk';
2
+ import { MODEL_MAP } from './types.js';
3
+ import { toolDefinitions } from '../tools/definitions.js';
4
+ import { ToolExecutor } from '../tools/ToolExecutor.js';
5
+ import { THREEJS_SYSTEM_PROMPT } from '../prompts/system.js';
6
+ // Limits to prevent hitting rate limits
7
+ const MAX_HISTORY_MESSAGES = 20; // Keep last N messages
8
+ const MAX_TOKENS = 4096; // Reduced from 8192
9
+ const MAX_RETRIES = 3;
10
+ const RETRY_DELAY_MS = 5000; // 5 seconds base delay
11
+ export class AgentEngine {
12
+ client;
13
+ model = 'sonnet';
14
+ conversationHistory = [];
15
+ toolExecutor;
16
+ ui;
17
+ constructor(ui, workingDirectory) {
18
+ this.client = new Anthropic();
19
+ this.ui = ui;
20
+ this.toolExecutor = new ToolExecutor(workingDirectory, ui);
21
+ }
22
+ // Trim conversation history to prevent token overflow
23
+ trimHistory() {
24
+ if (this.conversationHistory.length > MAX_HISTORY_MESSAGES) {
25
+ // Keep the first message (initial context) and the last N-1 messages
26
+ const first = this.conversationHistory[0];
27
+ const recent = this.conversationHistory.slice(-(MAX_HISTORY_MESSAGES - 1));
28
+ this.conversationHistory = [first, ...recent];
29
+ }
30
+ }
31
+ // Sleep helper for retry delays
32
+ sleep(ms) {
33
+ return new Promise(resolve => setTimeout(resolve, ms));
34
+ }
35
+ setModel(model) {
36
+ this.model = model;
37
+ }
38
+ getModel() {
39
+ return this.model;
40
+ }
41
+ clearHistory() {
42
+ this.conversationHistory = [];
43
+ this.toolExecutor.clearCreatedFiles();
44
+ }
45
+ getCreatedFiles() {
46
+ return this.toolExecutor.getCreatedFiles();
47
+ }
48
+ async processMessage(userMessage) {
49
+ // Add user message to history
50
+ this.conversationHistory.push({
51
+ role: 'user',
52
+ content: userMessage,
53
+ });
54
+ // Trim history to prevent token overflow
55
+ this.trimHistory();
56
+ // Run the agentic loop
57
+ await this.runAgentLoop();
58
+ }
59
+ async runAgentLoop() {
60
+ let continueLoop = true;
61
+ while (continueLoop) {
62
+ continueLoop = await this.runSingleTurn();
63
+ }
64
+ }
65
+ async runSingleTurn(retryCount = 0) {
66
+ try {
67
+ // Show thinking indicator
68
+ this.ui.startThinking('Thinking');
69
+ // Create the API request with streaming
70
+ const stream = this.client.messages.stream({
71
+ model: MODEL_MAP[this.model],
72
+ max_tokens: MAX_TOKENS,
73
+ system: THREEJS_SYSTEM_PROMPT,
74
+ tools: toolDefinitions,
75
+ messages: this.conversationHistory,
76
+ });
77
+ // Collect response content
78
+ const contentBlocks = [];
79
+ let isFirstText = true;
80
+ // Handle streaming events
81
+ stream.on('text', (text) => {
82
+ if (isFirstText) {
83
+ this.ui.stopThinking();
84
+ this.ui.startStreaming();
85
+ isFirstText = false;
86
+ }
87
+ this.ui.streamText(text);
88
+ });
89
+ // Wait for the complete response
90
+ const response = await stream.finalMessage();
91
+ // Make sure to stop thinking if no text was streamed
92
+ this.ui.stopThinking();
93
+ if (!isFirstText) {
94
+ this.ui.endStreaming();
95
+ }
96
+ // Process all content blocks
97
+ for (const block of response.content) {
98
+ contentBlocks.push(block);
99
+ }
100
+ // Add assistant response to history
101
+ this.conversationHistory.push({
102
+ role: 'assistant',
103
+ content: contentBlocks,
104
+ });
105
+ // Check if we need to process tool calls
106
+ const toolUseBlocks = contentBlocks.filter((block) => block.type === 'tool_use');
107
+ if (toolUseBlocks.length === 0) {
108
+ // No tool calls, we're done
109
+ return false;
110
+ }
111
+ // Execute all tool calls
112
+ const toolResults = [];
113
+ for (const toolUse of toolUseBlocks) {
114
+ const result = await this.toolExecutor.execute(toolUse.name, toolUse.input);
115
+ // Truncate large outputs to save tokens
116
+ let output = result.success ? result.output : `Error: ${result.error}`;
117
+ if (output.length > 2000) {
118
+ output = output.substring(0, 2000) + '\n... (truncated)';
119
+ }
120
+ toolResults.push({
121
+ type: 'tool_result',
122
+ tool_use_id: toolUse.id,
123
+ content: output,
124
+ is_error: !result.success,
125
+ });
126
+ }
127
+ // Add tool results to history
128
+ this.conversationHistory.push({
129
+ role: 'user',
130
+ content: toolResults,
131
+ });
132
+ // Continue the loop if we have tool results to process
133
+ return response.stop_reason === 'tool_use';
134
+ }
135
+ catch (error) {
136
+ this.ui.stopThinking();
137
+ // Handle rate limit errors with retry
138
+ if (error instanceof Anthropic.RateLimitError) {
139
+ if (retryCount < MAX_RETRIES) {
140
+ const delay = RETRY_DELAY_MS * Math.pow(2, retryCount);
141
+ this.ui.printWarning(`Rate limited. Retrying in ${delay / 1000}s...`);
142
+ await this.sleep(delay);
143
+ return this.runSingleTurn(retryCount + 1);
144
+ }
145
+ this.ui.printError('Rate limit exceeded. Please wait a moment and try again.');
146
+ return false;
147
+ }
148
+ if (error instanceof Anthropic.APIError) {
149
+ this.ui.printError(`API Error: ${error.message}`);
150
+ }
151
+ else {
152
+ this.ui.printError(`Error: ${error instanceof Error ? error.message : String(error)}`);
153
+ }
154
+ return false;
155
+ }
156
+ }
157
+ }