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 +21 -0
- package/README.md +123 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +33 -0
- package/dist/commands/config.d.ts +7 -0
- package/dist/commands/config.js +159 -0
- package/dist/commands/start.d.ts +5 -0
- package/dist/commands/start.js +151 -0
- package/dist/core/AgentEngine.d.ts +19 -0
- package/dist/core/AgentEngine.js +157 -0
- package/dist/core/ThreeJsWizard.d.ts +12 -0
- package/dist/core/ThreeJsWizard.js +124 -0
- package/dist/core/types.d.ts +45 -0
- package/dist/core/types.js +5 -0
- package/dist/project/ProjectManager.d.ts +22 -0
- package/dist/project/ProjectManager.js +135 -0
- package/dist/prompts/system.d.ts +1 -0
- package/dist/prompts/system.js +237 -0
- package/dist/tools/ToolExecutor.d.ts +40 -0
- package/dist/tools/ToolExecutor.js +390 -0
- package/dist/tools/definitions.d.ts +2 -0
- package/dist/tools/definitions.js +92 -0
- package/dist/ui/TerminalUI.d.ts +36 -0
- package/dist/ui/TerminalUI.js +184 -0
- package/dist/ui/onboarding.d.ts +4 -0
- package/dist/ui/onboarding.js +38 -0
- package/dist/ui/spinner.d.ts +10 -0
- package/dist/ui/spinner.js +41 -0
- package/dist/wizard.d.ts +2 -0
- package/dist/wizard.js +26 -0
- package/package.json +53 -0
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
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,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,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
|
+
}
|