threewzrd 1.0.0 → 1.0.3
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/dist/cli.js +6 -0
- package/dist/commands/model.d.ts +1 -0
- package/dist/commands/model.js +62 -0
- package/dist/commands/start.d.ts +1 -0
- package/dist/commands/start.js +32 -2
- package/dist/core/AgentEngine.js +2 -2
- package/dist/core/ThreeJsWizard.d.ts +5 -1
- package/dist/core/ThreeJsWizard.js +7 -4
- package/dist/core/types.d.ts +3 -1
- package/dist/core/types.js +2 -0
- package/dist/prompts/system.d.ts +1 -1
- package/dist/prompts/system.js +12 -0
- package/dist/tools/CodeValidator.d.ts +44 -0
- package/dist/tools/CodeValidator.js +286 -0
- package/dist/tools/ToolExecutor.d.ts +5 -0
- package/dist/tools/ToolExecutor.js +95 -46
- package/dist/ui/TerminalUI.js +1 -0
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -3,6 +3,7 @@ import { Command } from 'commander';
|
|
|
3
3
|
import { homedir } from 'os';
|
|
4
4
|
import { startCommand } from './commands/start.js';
|
|
5
5
|
import { configCommand } from './commands/config.js';
|
|
6
|
+
import { modelCommand } from './commands/model.js';
|
|
6
7
|
// Safely get current working directory, fallback to home
|
|
7
8
|
function safeGetCwd() {
|
|
8
9
|
try {
|
|
@@ -22,6 +23,7 @@ program
|
|
|
22
23
|
.command('start')
|
|
23
24
|
.description('Start the wizard REPL')
|
|
24
25
|
.option('-d, --directory <path>', 'Working directory for the wizard', safeGetCwd())
|
|
26
|
+
.option('-m, --model <model>', 'Model to use (sonnet, opus, haiku, opus-4.6)')
|
|
25
27
|
.action(startCommand);
|
|
26
28
|
program
|
|
27
29
|
.command('config')
|
|
@@ -30,4 +32,8 @@ program
|
|
|
30
32
|
.option('-d, --delete', 'Delete saved API key')
|
|
31
33
|
.option('-p, --path', 'Show config file path')
|
|
32
34
|
.action(configCommand);
|
|
35
|
+
program
|
|
36
|
+
.command('model [name]')
|
|
37
|
+
.description('View or set the default model')
|
|
38
|
+
.action(modelCommand);
|
|
33
39
|
program.parse();
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function modelCommand(name?: string): Promise<void>;
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { homedir } from 'os';
|
|
2
|
+
import { join } from 'path';
|
|
3
|
+
import { mkdir, writeFile, readFile } from 'fs/promises';
|
|
4
|
+
import chalk from 'chalk';
|
|
5
|
+
import { MODEL_MAP, DEFAULT_MODEL } from '../core/types.js';
|
|
6
|
+
const CONFIG_DIR = join(homedir(), '.threewzrd');
|
|
7
|
+
const CONFIG_PATH = join(CONFIG_DIR, 'config.json');
|
|
8
|
+
const VALID_MODELS = ['sonnet', 'opus', 'haiku', 'opus-4.6'];
|
|
9
|
+
async function loadConfig() {
|
|
10
|
+
try {
|
|
11
|
+
const content = await readFile(CONFIG_PATH, 'utf-8');
|
|
12
|
+
return JSON.parse(content);
|
|
13
|
+
}
|
|
14
|
+
catch {
|
|
15
|
+
return {};
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
async function saveConfig(config) {
|
|
19
|
+
await mkdir(CONFIG_DIR, { recursive: true, mode: 0o700 });
|
|
20
|
+
await writeFile(CONFIG_PATH, JSON.stringify(config, null, 2) + '\n', { mode: 0o600 });
|
|
21
|
+
}
|
|
22
|
+
export async function modelCommand(name) {
|
|
23
|
+
console.log();
|
|
24
|
+
if (!name) {
|
|
25
|
+
// Show current model
|
|
26
|
+
const config = await loadConfig();
|
|
27
|
+
const currentModel = config.model || DEFAULT_MODEL;
|
|
28
|
+
console.log(chalk.cyan(' Model Configuration'));
|
|
29
|
+
console.log(chalk.gray(' ─────────────────────────────────────────'));
|
|
30
|
+
console.log();
|
|
31
|
+
console.log(chalk.gray(' Current default: ') + chalk.green(currentModel));
|
|
32
|
+
console.log(chalk.gray(' Model ID: ') + chalk.gray(MODEL_MAP[currentModel]));
|
|
33
|
+
console.log();
|
|
34
|
+
console.log(chalk.gray(' Available models:'));
|
|
35
|
+
for (const model of VALID_MODELS) {
|
|
36
|
+
const marker = model === currentModel ? chalk.green(' (default)') : '';
|
|
37
|
+
console.log(chalk.gray(' - ') + chalk.white(model) + marker);
|
|
38
|
+
}
|
|
39
|
+
console.log();
|
|
40
|
+
console.log(chalk.gray(' To change: ') + chalk.cyan('threewzrd model <name>'));
|
|
41
|
+
console.log();
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
// Set model
|
|
45
|
+
const modelName = name.toLowerCase();
|
|
46
|
+
if (!VALID_MODELS.includes(modelName)) {
|
|
47
|
+
console.log(chalk.red(` Unknown model: ${name}`));
|
|
48
|
+
console.log();
|
|
49
|
+
console.log(chalk.gray(' Valid models:'));
|
|
50
|
+
for (const model of VALID_MODELS) {
|
|
51
|
+
console.log(chalk.gray(' - ') + chalk.white(model));
|
|
52
|
+
}
|
|
53
|
+
console.log();
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
const config = await loadConfig();
|
|
57
|
+
config.model = modelName;
|
|
58
|
+
await saveConfig(config);
|
|
59
|
+
console.log(chalk.green(` Default model set to: ${modelName}`));
|
|
60
|
+
console.log(chalk.gray(` Model ID: ${MODEL_MAP[modelName]}`));
|
|
61
|
+
console.log();
|
|
62
|
+
}
|
package/dist/commands/start.d.ts
CHANGED
package/dist/commands/start.js
CHANGED
|
@@ -1,10 +1,25 @@
|
|
|
1
1
|
import dotenv from 'dotenv';
|
|
2
2
|
import { homedir } from 'os';
|
|
3
3
|
import { join } from 'path';
|
|
4
|
-
import { mkdir, writeFile } from 'fs/promises';
|
|
4
|
+
import { mkdir, writeFile, readFile } from 'fs/promises';
|
|
5
5
|
import * as readline from 'readline';
|
|
6
6
|
import chalk from 'chalk';
|
|
7
7
|
import { ThreeJsWizard } from '../core/ThreeJsWizard.js';
|
|
8
|
+
const VALID_MODELS = ['sonnet', 'opus', 'haiku', 'opus-4.6'];
|
|
9
|
+
async function getConfiguredModel() {
|
|
10
|
+
try {
|
|
11
|
+
const configPath = join(homedir(), '.threewzrd', 'config.json');
|
|
12
|
+
const content = await readFile(configPath, 'utf-8');
|
|
13
|
+
const config = JSON.parse(content);
|
|
14
|
+
if (config.model && VALID_MODELS.includes(config.model)) {
|
|
15
|
+
return config.model;
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
catch {
|
|
19
|
+
// No config file or invalid config
|
|
20
|
+
}
|
|
21
|
+
return undefined;
|
|
22
|
+
}
|
|
8
23
|
function loadEnvFiles(workingDir) {
|
|
9
24
|
// Load from multiple locations (later ones don't override earlier)
|
|
10
25
|
// 1. Current working directory
|
|
@@ -128,8 +143,23 @@ export async function startCommand(options) {
|
|
|
128
143
|
await saveApiKey(apiKey);
|
|
129
144
|
}
|
|
130
145
|
}
|
|
146
|
+
// Determine which model to use (CLI flag > config > default)
|
|
147
|
+
let model;
|
|
148
|
+
if (options.model) {
|
|
149
|
+
if (VALID_MODELS.includes(options.model)) {
|
|
150
|
+
model = options.model;
|
|
151
|
+
}
|
|
152
|
+
else {
|
|
153
|
+
console.error(chalk.red(`Invalid model: ${options.model}`));
|
|
154
|
+
console.error(chalk.gray(`Valid models: ${VALID_MODELS.join(', ')}`));
|
|
155
|
+
process.exit(1);
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
else {
|
|
159
|
+
model = await getConfiguredModel();
|
|
160
|
+
}
|
|
131
161
|
// Create and start the wizard
|
|
132
|
-
const wizard = new ThreeJsWizard();
|
|
162
|
+
const wizard = new ThreeJsWizard({ model });
|
|
133
163
|
// Handle graceful shutdown
|
|
134
164
|
process.on('SIGINT', () => {
|
|
135
165
|
console.log('\nShutting down...');
|
package/dist/core/AgentEngine.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import Anthropic from '@anthropic-ai/sdk';
|
|
2
|
-
import { MODEL_MAP } from './types.js';
|
|
2
|
+
import { MODEL_MAP, DEFAULT_MODEL } from './types.js';
|
|
3
3
|
import { toolDefinitions } from '../tools/definitions.js';
|
|
4
4
|
import { ToolExecutor } from '../tools/ToolExecutor.js';
|
|
5
5
|
import { THREEJS_SYSTEM_PROMPT } from '../prompts/system.js';
|
|
@@ -10,7 +10,7 @@ const MAX_RETRIES = 3;
|
|
|
10
10
|
const RETRY_DELAY_MS = 5000; // 5 seconds base delay
|
|
11
11
|
export class AgentEngine {
|
|
12
12
|
client;
|
|
13
|
-
model =
|
|
13
|
+
model = DEFAULT_MODEL;
|
|
14
14
|
conversationHistory = [];
|
|
15
15
|
toolExecutor;
|
|
16
16
|
ui;
|
|
@@ -1,3 +1,7 @@
|
|
|
1
|
+
import { ModelId } from './types.js';
|
|
2
|
+
export interface WizardOptions {
|
|
3
|
+
model?: ModelId;
|
|
4
|
+
}
|
|
1
5
|
export declare class ThreeJsWizard {
|
|
2
6
|
private ui;
|
|
3
7
|
private engine;
|
|
@@ -5,7 +9,7 @@ export declare class ThreeJsWizard {
|
|
|
5
9
|
private workingDirectory;
|
|
6
10
|
private isRunning;
|
|
7
11
|
private hasOnboarded;
|
|
8
|
-
constructor();
|
|
12
|
+
constructor(options?: WizardOptions);
|
|
9
13
|
start(): Promise<void>;
|
|
10
14
|
private handleCommand;
|
|
11
15
|
stop(): void;
|
|
@@ -9,11 +9,14 @@ export class ThreeJsWizard {
|
|
|
9
9
|
workingDirectory;
|
|
10
10
|
isRunning = false;
|
|
11
11
|
hasOnboarded = false;
|
|
12
|
-
constructor() {
|
|
12
|
+
constructor(options) {
|
|
13
13
|
this.workingDirectory = process.cwd();
|
|
14
14
|
this.ui = new TerminalUI();
|
|
15
15
|
this.engine = new AgentEngine(this.ui, this.workingDirectory);
|
|
16
16
|
this.projectManager = new ProjectManager(this.workingDirectory);
|
|
17
|
+
if (options?.model) {
|
|
18
|
+
this.engine.setModel(options.model);
|
|
19
|
+
}
|
|
17
20
|
}
|
|
18
21
|
async start() {
|
|
19
22
|
this.isRunning = true;
|
|
@@ -100,16 +103,16 @@ export class ThreeJsWizard {
|
|
|
100
103
|
case 'model':
|
|
101
104
|
if (args.length === 0) {
|
|
102
105
|
this.ui.printInfo(`Current model: ${this.engine.getModel()}`);
|
|
103
|
-
this.ui.printInfo('Available models: sonnet, opus, haiku');
|
|
106
|
+
this.ui.printInfo('Available models: sonnet, opus, haiku, opus-4.6');
|
|
104
107
|
}
|
|
105
108
|
else {
|
|
106
109
|
const modelName = args[0].toLowerCase();
|
|
107
|
-
if (['sonnet', 'opus', 'haiku'].includes(modelName)) {
|
|
110
|
+
if (['sonnet', 'opus', 'haiku', 'opus-4.6'].includes(modelName)) {
|
|
108
111
|
this.engine.setModel(modelName);
|
|
109
112
|
this.ui.printModelSwitch(modelName);
|
|
110
113
|
}
|
|
111
114
|
else {
|
|
112
|
-
this.ui.printError(`Unknown model: ${modelName}. Use: sonnet, opus, or
|
|
115
|
+
this.ui.printError(`Unknown model: ${modelName}. Use: sonnet, opus, haiku, or opus-4.6`);
|
|
113
116
|
}
|
|
114
117
|
}
|
|
115
118
|
break;
|
package/dist/core/types.d.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import Anthropic from '@anthropic-ai/sdk';
|
|
2
|
-
export type ModelId = 'sonnet' | 'opus' | 'haiku';
|
|
2
|
+
export type ModelId = 'sonnet' | 'opus' | 'haiku' | 'opus-4.6';
|
|
3
3
|
export declare const MODEL_MAP: Record<ModelId, string>;
|
|
4
|
+
export declare const DEFAULT_MODEL: ModelId;
|
|
4
5
|
export type ProjectLanguage = 'javascript' | 'typescript';
|
|
5
6
|
export type ProjectTarget = 'browser' | 'mobile' | 'desktop';
|
|
6
7
|
export interface ProjectPreferences {
|
|
@@ -30,6 +31,7 @@ export type ToolName = 'write_file' | 'read_file' | 'run_command' | 'list_files'
|
|
|
30
31
|
export interface WriteFileInput {
|
|
31
32
|
path: string;
|
|
32
33
|
content: string;
|
|
34
|
+
skipValidation?: boolean;
|
|
33
35
|
}
|
|
34
36
|
export interface ReadFileInput {
|
|
35
37
|
path: string;
|
package/dist/core/types.js
CHANGED
package/dist/prompts/system.d.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export declare const THREEJS_SYSTEM_PROMPT = "\nYou are a senior-level Three.js engineer and 3D world architect.\n\nYour responsibility is to design and implement complete, working Three.js projects using modern best practices. You operate inside a CLI-based development agent with tool access.\n\nYou think carefully before acting. You plan when necessary. You modify incrementally. You do not guess APIs.\n\n\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n## Core Identity\n\nYou are:\n- A production-grade Three.js developer\n- A 3D world designer (lighting, scale, composition matter)\n- Careful and methodical\n- Tool-disciplined\n- Architecture-aware\n\nYou do NOT:\n- Rewrite working code unnecessarily\n- Overwrite unrelated files\n- Guess unfamiliar APIs\n- Create partial or broken implementations\n\n\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n## Available Tools\n\nYou have access to:\n\n- write_file(path, content)\n- read_file(path)\n- list_files(path?, recursive?)\n- run_command(command, cwd?)\n\nUse them strategically and in the correct order.\n\n\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n## Autonomous Execution Protocol\n\nYou operate in a structured execution loop.\n\nFor every user request:\n\n1. Analyze the task fully.\n2. Determine whether to use:\n - Single-Shot Mode\n - Planning Mode\n\n\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n## Execution Modes\n\n### 1. Single-Shot Mode\n\nUse when:\n- The request is small and self-contained\n- No major architecture decisions are required\n- Creating a simple new project\n\nIn this mode:\n- Plan internally\n- Create all required files\n- Install dependencies\n- Provide run instructions\n\nDo not output a formal plan in this mode.\n\n---\n\n### 2. Planning Mode (Multi-Step)\n\nUse when:\n- The project is complex\n- Multiple systems are involved (loaders, shaders, physics, post-processing, controls, etc.)\n- Modifying an existing project\n- Architectural decisions are required\n\nWhen using Planning Mode, you MUST output:\n\n## Implementation Plan\n\n### Architecture Overview\n(High-level design)\n\n### Dependencies\n(List npm packages required)\n\n### File Structure\n(project layout)\n\n### Execution Steps\n(Numbered steps)\n\nAfter producing the plan, begin executing step-by-step using tools.\n\nAfter each tool call:\n- Reassess the project state\n- Continue execution until complete\n\n\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n## Session Initialization Rule\n\nAt the beginning of a session or before modifying code:\n\n- Use list_files to inspect the current directory.\n- Determine whether this is:\n - A new project\n - An existing Three.js project\n - A non-Three.js project\n\nNever assume project structure without inspecting it first.\n\n\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n## Tool Usage Discipline\n\nBefore calling any tool:\n\n1. Understand the full scope of the change.\n2. Identify all files that will be affected.\n3. Determine dependencies.\n4. Confirm directory structure.\n\nRules:\n\n- Always use read_file before modifying a file.\n- Never overwrite unrelated files.\n- Only call run_command after files are written.\n- If run_command fails:\n - Analyze the error\n - Fix the issue\n - Retry\n- Do not reinstall dependencies unnecessarily.\n- Do not recreate projects that already exist.\n\n\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n## Modern Three.js Standards\n\nAlways follow current best practices:\n\n- Use ES modules.\n- Import from 'three'.\n- Use addons correctly (three/addons/...).\n- Avoid deprecated APIs (e.g., Geometry).\n- Use BufferGeometry.\n- Set renderer pixel ratio.\n- Handle window resize properly.\n- Use renderer.outputColorSpace = THREE.SRGBColorSpace.\n- Use physically correct lighting when appropriate.\n- Enable antialiasing.\n- Never create objects inside the render loop.\n\n\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n## Required Scene Structure\n\nA standard scene should include:\n\n- Scene\n- PerspectiveCamera (unless otherwise required)\n- WebGLRenderer with antialias\n- AmbientLight + DirectionalLight (unless specified otherwise)\n- Animation loop using requestAnimationFrame\n- Resize handling\n- Clean object organization\n\nGroup related objects logically.\n\n\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n## Performance & Memory Safety\n\n- Avoid allocations inside animation loop.\n- Use InstancedMesh for repeated geometry.\n- Dispose of geometries/materials when replacing them.\n- Avoid blocking the main thread.\n- Merge static geometries when appropriate.\n- Be mindful of texture sizes.\n\n\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n## Shader Implementation Rules\n\nWhen creating custom shaders:\n\n- Use ShaderMaterial.\n- Separate complex shaders into /src/shaders/.\n- Use uniforms for time-based animation.\n- Pass varyings correctly.\n- Comment GLSL clearly.\n- Do not guess GLSL syntax.\n\n\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n## TypeScript Rules (When Requested)\n\nIf the user wants TypeScript:\n\n- Use .ts files.\n- Create tsconfig.json.\n- Add type annotations.\n- Import types from 'three'.\n- Ensure Vite supports TS.\n\n\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n## World Design Intelligence\n\nYou think like a 3D world architect.\n\nBefore implementing a scene, consider:\n\n- Scale realism\n- Camera ergonomics\n- Lighting mood\n- Visual composition\n- Performance trade-offs\n- Interactivity patterns\n\nDo not just place objects \u2014 design environments.\n\n\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n## Completion Criteria\n\nA task is complete when:\n\n- All required files are created or updated\n- Dependencies are installed\n- The project runs successfully\n- Clear run instructions are provided\n\nEnd by telling the user exactly how to run the project:\n npm install\n npm run dev\n\n\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n## Your Goal\n\nTransform natural language ideas into clean, scalable, production-ready Three.js projects.\n\nBe precise.\nBe structured.\nBe architectural.\nBe reliable.\n";
|
|
1
|
+
export declare const THREEJS_SYSTEM_PROMPT = "\nYou are a senior-level Three.js engineer and 3D world architect.\n\nYour responsibility is to design and implement complete, working Three.js projects using modern best practices. You operate inside a CLI-based development agent with tool access.\n\nYou think carefully before acting. You plan when necessary. You modify incrementally. You do not guess APIs.\n\n\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n## Core Identity\n\nYou are:\n- A production-grade Three.js developer\n- A 3D world designer (lighting, scale, composition matter)\n- Careful and methodical\n- Tool-disciplined\n- Architecture-aware\n\nYou do NOT:\n- Rewrite working code unnecessarily\n- Overwrite unrelated files\n- Guess unfamiliar APIs\n- Create partial or broken implementations\n\n\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n## Available Tools\n\nYou have access to:\n\n- write_file(path, content)\n- read_file(path)\n- list_files(path?, recursive?)\n- run_command(command, cwd?)\n\nUse them strategically and in the correct order.\n\n\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n## Autonomous Execution Protocol\n\nYou operate in a structured execution loop.\n\nFor every user request:\n\n1. Analyze the task fully.\n2. Determine whether to use:\n - Single-Shot Mode\n - Planning Mode\n\n\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n## Execution Modes\n\n### 1. Single-Shot Mode\n\nUse when:\n- The request is small and self-contained\n- No major architecture decisions are required\n- Creating a simple new project\n\nIn this mode:\n- Plan internally\n- Create all required files\n- Install dependencies\n- Provide run instructions\n\nDo not output a formal plan in this mode.\n\n---\n\n### 2. Planning Mode (Multi-Step)\n\nUse when:\n- The project is complex\n- Multiple systems are involved (loaders, shaders, physics, post-processing, controls, etc.)\n- Modifying an existing project\n- Architectural decisions are required\n\nWhen using Planning Mode, you MUST output:\n\n## Implementation Plan\n\n### Architecture Overview\n(High-level design)\n\n### Dependencies\n(List npm packages required)\n\n### File Structure\n(project layout)\n\n### Execution Steps\n(Numbered steps)\n\nAfter producing the plan, begin executing step-by-step using tools.\n\nAfter each tool call:\n- Reassess the project state\n- Continue execution until complete\n\n\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n## Session Initialization Rule\n\nAt the beginning of a session or before modifying code:\n\n- Use list_files to inspect the current directory.\n- Determine whether this is:\n - A new project\n - An existing Three.js project\n - A non-Three.js project\n\nNever assume project structure without inspecting it first.\n\n\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n## Tool Usage Discipline\n\nBefore calling any tool:\n\n1. Understand the full scope of the change.\n2. Identify all files that will be affected.\n3. Determine dependencies.\n4. Confirm directory structure.\n\nRules:\n\n- Always use read_file before modifying a file.\n- Never overwrite unrelated files.\n- Only call run_command after files are written.\n- If run_command fails:\n - Analyze the error\n - Fix the issue\n - Retry\n- Do not reinstall dependencies unnecessarily.\n- Do not recreate projects that already exist.\n\n\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n## Code Quality Requirements\n\nBefore writing any code file:\n1. Ensure all braces, brackets, and parentheses are balanced\n2. Ensure all strings and template literals are properly closed\n3. Verify import statements are correct and complete\n4. Double-check syntax before calling write_file\n\nThe system validates JavaScript, TypeScript, and JSON syntax automatically.\nIf validation fails, fix the errors and retry.\n\n\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n## Modern Three.js Standards\n\nAlways follow current best practices:\n\n- Use ES modules.\n- Import from 'three'.\n- Use addons correctly (three/addons/...).\n- Avoid deprecated APIs (e.g., Geometry).\n- Use BufferGeometry.\n- Set renderer pixel ratio.\n- Handle window resize properly.\n- Use renderer.outputColorSpace = THREE.SRGBColorSpace.\n- Use physically correct lighting when appropriate.\n- Enable antialiasing.\n- Never create objects inside the render loop.\n\n\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n## Required Scene Structure\n\nA standard scene should include:\n\n- Scene\n- PerspectiveCamera (unless otherwise required)\n- WebGLRenderer with antialias\n- AmbientLight + DirectionalLight (unless specified otherwise)\n- Animation loop using requestAnimationFrame\n- Resize handling\n- Clean object organization\n\nGroup related objects logically.\n\n\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n## Performance & Memory Safety\n\n- Avoid allocations inside animation loop.\n- Use InstancedMesh for repeated geometry.\n- Dispose of geometries/materials when replacing them.\n- Avoid blocking the main thread.\n- Merge static geometries when appropriate.\n- Be mindful of texture sizes.\n\n\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n## Shader Implementation Rules\n\nWhen creating custom shaders:\n\n- Use ShaderMaterial.\n- Separate complex shaders into /src/shaders/.\n- Use uniforms for time-based animation.\n- Pass varyings correctly.\n- Comment GLSL clearly.\n- Do not guess GLSL syntax.\n\n\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n## TypeScript Rules (When Requested)\n\nIf the user wants TypeScript:\n\n- Use .ts files.\n- Create tsconfig.json.\n- Add type annotations.\n- Import types from 'three'.\n- Ensure Vite supports TS.\n\n\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n## World Design Intelligence\n\nYou think like a 3D world architect.\n\nBefore implementing a scene, consider:\n\n- Scale realism\n- Camera ergonomics\n- Lighting mood\n- Visual composition\n- Performance trade-offs\n- Interactivity patterns\n\nDo not just place objects \u2014 design environments.\n\n\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n## Completion Criteria\n\nA task is complete when:\n\n- All required files are created or updated\n- Dependencies are installed\n- The project runs successfully\n- Clear run instructions are provided\n\nEnd by telling the user exactly how to run the project:\n npm install\n npm run dev\n\n\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n## Your Goal\n\nTransform natural language ideas into clean, scalable, production-ready Three.js projects.\n\nBe precise.\nBe structured.\nBe architectural.\nBe reliable.\n";
|
package/dist/prompts/system.js
CHANGED
|
@@ -130,6 +130,18 @@ Rules:
|
|
|
130
130
|
- Do not reinstall dependencies unnecessarily.
|
|
131
131
|
- Do not recreate projects that already exist.
|
|
132
132
|
|
|
133
|
+
────────────────────────────────
|
|
134
|
+
## Code Quality Requirements
|
|
135
|
+
|
|
136
|
+
Before writing any code file:
|
|
137
|
+
1. Ensure all braces, brackets, and parentheses are balanced
|
|
138
|
+
2. Ensure all strings and template literals are properly closed
|
|
139
|
+
3. Verify import statements are correct and complete
|
|
140
|
+
4. Double-check syntax before calling write_file
|
|
141
|
+
|
|
142
|
+
The system validates JavaScript, TypeScript, and JSON syntax automatically.
|
|
143
|
+
If validation fails, fix the errors and retry.
|
|
144
|
+
|
|
133
145
|
────────────────────────────────
|
|
134
146
|
## Modern Three.js Standards
|
|
135
147
|
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Code Validator - Syntax validation for JavaScript, TypeScript, and JSON files
|
|
3
|
+
* Provides basic syntax checks before writing code files
|
|
4
|
+
*/
|
|
5
|
+
export interface ValidationResult {
|
|
6
|
+
valid: boolean;
|
|
7
|
+
errors: string[];
|
|
8
|
+
warnings: string[];
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* Checks if a file should be validated based on its extension
|
|
12
|
+
*/
|
|
13
|
+
export declare function shouldValidate(filePath: string): boolean;
|
|
14
|
+
/**
|
|
15
|
+
* Validates file content based on file type
|
|
16
|
+
*/
|
|
17
|
+
export declare function validate(filePath: string, content: string): ValidationResult;
|
|
18
|
+
/**
|
|
19
|
+
* Validates JSON syntax
|
|
20
|
+
*/
|
|
21
|
+
export declare function validateJSON(content: string): ValidationResult;
|
|
22
|
+
/**
|
|
23
|
+
* Validates JavaScript/TypeScript syntax (basic checks)
|
|
24
|
+
*/
|
|
25
|
+
export declare function validateJavaScript(content: string): ValidationResult;
|
|
26
|
+
/**
|
|
27
|
+
* Checks for balanced delimiters: {}, [], ()
|
|
28
|
+
*/
|
|
29
|
+
export declare function checkBalancedDelimiters(content: string): {
|
|
30
|
+
errors: string[];
|
|
31
|
+
};
|
|
32
|
+
/**
|
|
33
|
+
* Checks for unclosed string literals
|
|
34
|
+
*/
|
|
35
|
+
export declare function checkStrings(content: string): {
|
|
36
|
+
errors: string[];
|
|
37
|
+
warnings: string[];
|
|
38
|
+
};
|
|
39
|
+
/**
|
|
40
|
+
* Checks for unclosed template literals
|
|
41
|
+
*/
|
|
42
|
+
export declare function checkTemplateLiterals(content: string): {
|
|
43
|
+
errors: string[];
|
|
44
|
+
};
|
|
@@ -0,0 +1,286 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Code Validator - Syntax validation for JavaScript, TypeScript, and JSON files
|
|
3
|
+
* Provides basic syntax checks before writing code files
|
|
4
|
+
*/
|
|
5
|
+
// File extensions that should be validated
|
|
6
|
+
const VALIDATABLE_EXTENSIONS = new Set(['.js', '.ts', '.jsx', '.tsx', '.mjs', '.cjs', '.json']);
|
|
7
|
+
/**
|
|
8
|
+
* Checks if a file should be validated based on its extension
|
|
9
|
+
*/
|
|
10
|
+
export function shouldValidate(filePath) {
|
|
11
|
+
const ext = filePath.slice(filePath.lastIndexOf('.')).toLowerCase();
|
|
12
|
+
return VALIDATABLE_EXTENSIONS.has(ext);
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Validates file content based on file type
|
|
16
|
+
*/
|
|
17
|
+
export function validate(filePath, content) {
|
|
18
|
+
const ext = filePath.slice(filePath.lastIndexOf('.')).toLowerCase();
|
|
19
|
+
if (ext === '.json') {
|
|
20
|
+
return validateJSON(content);
|
|
21
|
+
}
|
|
22
|
+
if (['.js', '.ts', '.jsx', '.tsx', '.mjs', '.cjs'].includes(ext)) {
|
|
23
|
+
return validateJavaScript(content);
|
|
24
|
+
}
|
|
25
|
+
return { valid: true, errors: [], warnings: [] };
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Validates JSON syntax
|
|
29
|
+
*/
|
|
30
|
+
export function validateJSON(content) {
|
|
31
|
+
const errors = [];
|
|
32
|
+
const warnings = [];
|
|
33
|
+
try {
|
|
34
|
+
JSON.parse(content);
|
|
35
|
+
}
|
|
36
|
+
catch (error) {
|
|
37
|
+
if (error instanceof SyntaxError) {
|
|
38
|
+
errors.push(`JSON syntax error: ${error.message}`);
|
|
39
|
+
}
|
|
40
|
+
else {
|
|
41
|
+
errors.push(`JSON parsing failed: ${String(error)}`);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
return { valid: errors.length === 0, errors, warnings };
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Validates JavaScript/TypeScript syntax (basic checks)
|
|
48
|
+
*/
|
|
49
|
+
export function validateJavaScript(content) {
|
|
50
|
+
const errors = [];
|
|
51
|
+
const warnings = [];
|
|
52
|
+
// Check balanced delimiters
|
|
53
|
+
const delimiterResult = checkBalancedDelimiters(content);
|
|
54
|
+
errors.push(...delimiterResult.errors);
|
|
55
|
+
// Check unclosed strings
|
|
56
|
+
const stringResult = checkStrings(content);
|
|
57
|
+
errors.push(...stringResult.errors);
|
|
58
|
+
warnings.push(...stringResult.warnings);
|
|
59
|
+
// Check template literals
|
|
60
|
+
const templateResult = checkTemplateLiterals(content);
|
|
61
|
+
errors.push(...templateResult.errors);
|
|
62
|
+
return { valid: errors.length === 0, errors, warnings };
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Checks for balanced delimiters: {}, [], ()
|
|
66
|
+
*/
|
|
67
|
+
export function checkBalancedDelimiters(content) {
|
|
68
|
+
const errors = [];
|
|
69
|
+
const stack = [];
|
|
70
|
+
const pairs = { '{': '}', '[': ']', '(': ')' };
|
|
71
|
+
const closers = { '}': '{', ']': '[', ')': '(' };
|
|
72
|
+
let inString = null;
|
|
73
|
+
let inTemplateString = false;
|
|
74
|
+
let inLineComment = false;
|
|
75
|
+
let inBlockComment = false;
|
|
76
|
+
let lineNumber = 1;
|
|
77
|
+
let i = 0;
|
|
78
|
+
while (i < content.length) {
|
|
79
|
+
const char = content[i];
|
|
80
|
+
const nextChar = content[i + 1];
|
|
81
|
+
// Track line numbers
|
|
82
|
+
if (char === '\n') {
|
|
83
|
+
lineNumber++;
|
|
84
|
+
inLineComment = false;
|
|
85
|
+
i++;
|
|
86
|
+
continue;
|
|
87
|
+
}
|
|
88
|
+
// Handle comments
|
|
89
|
+
if (!inString && !inTemplateString && !inBlockComment && char === '/' && nextChar === '/') {
|
|
90
|
+
inLineComment = true;
|
|
91
|
+
i += 2;
|
|
92
|
+
continue;
|
|
93
|
+
}
|
|
94
|
+
if (!inString && !inTemplateString && !inLineComment && char === '/' && nextChar === '*') {
|
|
95
|
+
inBlockComment = true;
|
|
96
|
+
i += 2;
|
|
97
|
+
continue;
|
|
98
|
+
}
|
|
99
|
+
if (inBlockComment && char === '*' && nextChar === '/') {
|
|
100
|
+
inBlockComment = false;
|
|
101
|
+
i += 2;
|
|
102
|
+
continue;
|
|
103
|
+
}
|
|
104
|
+
// Skip if in comment
|
|
105
|
+
if (inLineComment || inBlockComment) {
|
|
106
|
+
i++;
|
|
107
|
+
continue;
|
|
108
|
+
}
|
|
109
|
+
// Handle escape sequences
|
|
110
|
+
if ((inString || inTemplateString) && char === '\\') {
|
|
111
|
+
i += 2;
|
|
112
|
+
continue;
|
|
113
|
+
}
|
|
114
|
+
// Handle string delimiters
|
|
115
|
+
if (char === '`') {
|
|
116
|
+
inTemplateString = !inTemplateString;
|
|
117
|
+
i++;
|
|
118
|
+
continue;
|
|
119
|
+
}
|
|
120
|
+
if ((char === '"' || char === "'") && !inTemplateString) {
|
|
121
|
+
if (inString === char) {
|
|
122
|
+
inString = null;
|
|
123
|
+
}
|
|
124
|
+
else if (!inString) {
|
|
125
|
+
inString = char;
|
|
126
|
+
}
|
|
127
|
+
i++;
|
|
128
|
+
continue;
|
|
129
|
+
}
|
|
130
|
+
// Skip if inside string
|
|
131
|
+
if (inString || inTemplateString) {
|
|
132
|
+
i++;
|
|
133
|
+
continue;
|
|
134
|
+
}
|
|
135
|
+
// Track delimiters
|
|
136
|
+
if (pairs[char]) {
|
|
137
|
+
stack.push({ char, line: lineNumber });
|
|
138
|
+
}
|
|
139
|
+
else if (closers[char]) {
|
|
140
|
+
const expected = closers[char];
|
|
141
|
+
if (stack.length === 0) {
|
|
142
|
+
errors.push(`Unexpected '${char}' at line ${lineNumber} - no matching '${expected}'`);
|
|
143
|
+
}
|
|
144
|
+
else {
|
|
145
|
+
const top = stack.pop();
|
|
146
|
+
if (top.char !== expected) {
|
|
147
|
+
errors.push(`Mismatched delimiter: expected '${pairs[top.char]}' to close '${top.char}' from line ${top.line}, but found '${char}' at line ${lineNumber}`);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
i++;
|
|
152
|
+
}
|
|
153
|
+
// Report unclosed delimiters
|
|
154
|
+
for (const unclosed of stack) {
|
|
155
|
+
errors.push(`Unclosed '${unclosed.char}' from line ${unclosed.line} - missing '${pairs[unclosed.char]}'`);
|
|
156
|
+
}
|
|
157
|
+
return { errors };
|
|
158
|
+
}
|
|
159
|
+
/**
|
|
160
|
+
* Checks for unclosed string literals
|
|
161
|
+
*/
|
|
162
|
+
export function checkStrings(content) {
|
|
163
|
+
const errors = [];
|
|
164
|
+
const warnings = [];
|
|
165
|
+
const lines = content.split('\n');
|
|
166
|
+
for (let lineIdx = 0; lineIdx < lines.length; lineIdx++) {
|
|
167
|
+
const line = lines[lineIdx];
|
|
168
|
+
const lineNumber = lineIdx + 1;
|
|
169
|
+
// Skip lines that are likely comments
|
|
170
|
+
const trimmed = line.trim();
|
|
171
|
+
if (trimmed.startsWith('//') || trimmed.startsWith('*') || trimmed.startsWith('/*')) {
|
|
172
|
+
continue;
|
|
173
|
+
}
|
|
174
|
+
let inString = null;
|
|
175
|
+
let stringStart = -1;
|
|
176
|
+
let i = 0;
|
|
177
|
+
while (i < line.length) {
|
|
178
|
+
const char = line[i];
|
|
179
|
+
// Handle escape sequences
|
|
180
|
+
if (inString && char === '\\') {
|
|
181
|
+
i += 2;
|
|
182
|
+
continue;
|
|
183
|
+
}
|
|
184
|
+
// Check for template literals - they can span multiple lines
|
|
185
|
+
if (char === '`') {
|
|
186
|
+
// Template literals are handled separately
|
|
187
|
+
i++;
|
|
188
|
+
continue;
|
|
189
|
+
}
|
|
190
|
+
if ((char === '"' || char === "'")) {
|
|
191
|
+
if (inString === char) {
|
|
192
|
+
inString = null;
|
|
193
|
+
}
|
|
194
|
+
else if (!inString) {
|
|
195
|
+
inString = char;
|
|
196
|
+
stringStart = i;
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
i++;
|
|
200
|
+
}
|
|
201
|
+
// Check for unclosed string on this line (not template literals)
|
|
202
|
+
if (inString) {
|
|
203
|
+
// Check if this might be intentional (like a long string literal)
|
|
204
|
+
// or if it's clearly an error
|
|
205
|
+
const remainingContent = line.slice(stringStart);
|
|
206
|
+
if (remainingContent.length > 100) {
|
|
207
|
+
warnings.push(`Possibly unclosed string starting at line ${lineNumber}, column ${stringStart + 1}`);
|
|
208
|
+
}
|
|
209
|
+
else {
|
|
210
|
+
errors.push(`Unclosed string '${inString}' at line ${lineNumber}, column ${stringStart + 1}`);
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
return { errors, warnings };
|
|
215
|
+
}
|
|
216
|
+
/**
|
|
217
|
+
* Checks for unclosed template literals
|
|
218
|
+
*/
|
|
219
|
+
export function checkTemplateLiterals(content) {
|
|
220
|
+
const errors = [];
|
|
221
|
+
let inTemplate = false;
|
|
222
|
+
let templateStartLine = 0;
|
|
223
|
+
let lineNumber = 1;
|
|
224
|
+
let inLineComment = false;
|
|
225
|
+
let inBlockComment = false;
|
|
226
|
+
let inString = null;
|
|
227
|
+
for (let i = 0; i < content.length; i++) {
|
|
228
|
+
const char = content[i];
|
|
229
|
+
const nextChar = content[i + 1];
|
|
230
|
+
if (char === '\n') {
|
|
231
|
+
lineNumber++;
|
|
232
|
+
inLineComment = false;
|
|
233
|
+
continue;
|
|
234
|
+
}
|
|
235
|
+
// Handle comments
|
|
236
|
+
if (!inString && !inTemplate && !inBlockComment && char === '/' && nextChar === '/') {
|
|
237
|
+
inLineComment = true;
|
|
238
|
+
continue;
|
|
239
|
+
}
|
|
240
|
+
if (!inString && !inTemplate && !inLineComment && char === '/' && nextChar === '*') {
|
|
241
|
+
inBlockComment = true;
|
|
242
|
+
i++;
|
|
243
|
+
continue;
|
|
244
|
+
}
|
|
245
|
+
if (inBlockComment && char === '*' && nextChar === '/') {
|
|
246
|
+
inBlockComment = false;
|
|
247
|
+
i++;
|
|
248
|
+
continue;
|
|
249
|
+
}
|
|
250
|
+
if (inLineComment || inBlockComment) {
|
|
251
|
+
continue;
|
|
252
|
+
}
|
|
253
|
+
// Handle escape sequences
|
|
254
|
+
if ((inString || inTemplate) && char === '\\') {
|
|
255
|
+
i++;
|
|
256
|
+
continue;
|
|
257
|
+
}
|
|
258
|
+
// Handle regular strings
|
|
259
|
+
if ((char === '"' || char === "'") && !inTemplate) {
|
|
260
|
+
if (inString === char) {
|
|
261
|
+
inString = null;
|
|
262
|
+
}
|
|
263
|
+
else if (!inString) {
|
|
264
|
+
inString = char;
|
|
265
|
+
}
|
|
266
|
+
continue;
|
|
267
|
+
}
|
|
268
|
+
if (inString) {
|
|
269
|
+
continue;
|
|
270
|
+
}
|
|
271
|
+
// Handle template literals
|
|
272
|
+
if (char === '`') {
|
|
273
|
+
if (inTemplate) {
|
|
274
|
+
inTemplate = false;
|
|
275
|
+
}
|
|
276
|
+
else {
|
|
277
|
+
inTemplate = true;
|
|
278
|
+
templateStartLine = lineNumber;
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
if (inTemplate) {
|
|
283
|
+
errors.push(`Unclosed template literal starting at line ${templateStartLine}`);
|
|
284
|
+
}
|
|
285
|
+
return { errors };
|
|
286
|
+
}
|
|
@@ -27,8 +27,13 @@ export declare class ToolExecutor {
|
|
|
27
27
|
* Validates ListFilesInput structure and types
|
|
28
28
|
*/
|
|
29
29
|
private validateListFilesInput;
|
|
30
|
+
/**
|
|
31
|
+
* Tokenizes a single command (no pipes) into tokens respecting quotes
|
|
32
|
+
*/
|
|
33
|
+
private tokenizeCommand;
|
|
30
34
|
/**
|
|
31
35
|
* Parses a command string into executable and arguments safely
|
|
36
|
+
* Supports piped commands (e.g., "grep foo | wc -l")
|
|
32
37
|
*/
|
|
33
38
|
private parseCommand;
|
|
34
39
|
execute(toolName: ToolName, input: unknown): Promise<ToolResult>;
|
|
@@ -1,26 +1,31 @@
|
|
|
1
1
|
import * as fs from 'fs/promises';
|
|
2
2
|
import * as path from 'path';
|
|
3
3
|
import { spawn } from 'child_process';
|
|
4
|
+
import { shouldValidate, validate } from './CodeValidator.js';
|
|
4
5
|
// Whitelist of allowed commands for security
|
|
5
6
|
const ALLOWED_COMMANDS = new Set([
|
|
6
|
-
|
|
7
|
-
'npx',
|
|
8
|
-
|
|
9
|
-
'
|
|
7
|
+
// Package managers
|
|
8
|
+
'npm', 'npx', 'pnpm', 'yarn', 'bun',
|
|
9
|
+
// Build tools & runtimes
|
|
10
|
+
'node', 'tsc', 'vite', 'esbuild', 'rollup', 'webpack',
|
|
11
|
+
// Version control
|
|
10
12
|
'git',
|
|
11
|
-
|
|
12
|
-
'
|
|
13
|
-
|
|
14
|
-
'
|
|
15
|
-
|
|
16
|
-
'
|
|
17
|
-
|
|
18
|
-
'
|
|
19
|
-
|
|
20
|
-
'
|
|
13
|
+
// File operations
|
|
14
|
+
'mkdir', 'touch', 'rm', 'cp', 'mv', 'chmod', 'ln',
|
|
15
|
+
// File inspection
|
|
16
|
+
'cat', 'ls', 'pwd', 'head', 'tail', 'wc', 'file', 'stat',
|
|
17
|
+
// Search and text processing
|
|
18
|
+
'grep', 'find', 'sed', 'awk', 'sort', 'uniq', 'diff', 'tr', 'cut',
|
|
19
|
+
// System utilities
|
|
20
|
+
'echo', 'which', 'whereis', 'env', 'basename', 'dirname', 'realpath',
|
|
21
|
+
// Archive tools
|
|
22
|
+
'tar', 'zip', 'unzip', 'gzip', 'gunzip',
|
|
23
|
+
// Testing
|
|
24
|
+
'jest', 'vitest', 'mocha', 'playwright', 'cypress',
|
|
21
25
|
]);
|
|
22
26
|
// Dangerous shell metacharacters that indicate command injection attempts
|
|
23
|
-
|
|
27
|
+
// Note: | (pipe) is allowed and handled specially for piped commands
|
|
28
|
+
const DANGEROUS_PATTERNS = /[;&`$(){}[\]<>!\\]/;
|
|
24
29
|
export class ToolExecutor {
|
|
25
30
|
workingDirectory;
|
|
26
31
|
ui;
|
|
@@ -62,7 +67,11 @@ export class ToolExecutor {
|
|
|
62
67
|
if (typeof obj.content !== 'string') {
|
|
63
68
|
throw new Error('Invalid input: content must be a string');
|
|
64
69
|
}
|
|
65
|
-
return {
|
|
70
|
+
return {
|
|
71
|
+
path: obj.path.trim(),
|
|
72
|
+
content: obj.content,
|
|
73
|
+
skipValidation: obj.skipValidation === true
|
|
74
|
+
};
|
|
66
75
|
}
|
|
67
76
|
/**
|
|
68
77
|
* Validates ReadFileInput structure and types
|
|
@@ -116,14 +125,9 @@ export class ToolExecutor {
|
|
|
116
125
|
};
|
|
117
126
|
}
|
|
118
127
|
/**
|
|
119
|
-
*
|
|
128
|
+
* Tokenizes a single command (no pipes) into tokens respecting quotes
|
|
120
129
|
*/
|
|
121
|
-
|
|
122
|
-
// Check for dangerous shell metacharacters
|
|
123
|
-
if (DANGEROUS_PATTERNS.test(cmdString)) {
|
|
124
|
-
throw new Error('Command contains dangerous shell metacharacters');
|
|
125
|
-
}
|
|
126
|
-
// Simple tokenization - split on whitespace, respecting quotes
|
|
130
|
+
tokenizeCommand(cmdString) {
|
|
127
131
|
const tokens = [];
|
|
128
132
|
let current = '';
|
|
129
133
|
let inQuote = null;
|
|
@@ -156,12 +160,38 @@ export class ToolExecutor {
|
|
|
156
160
|
if (inQuote) {
|
|
157
161
|
throw new Error('Unclosed quote in command');
|
|
158
162
|
}
|
|
159
|
-
|
|
160
|
-
|
|
163
|
+
return tokens;
|
|
164
|
+
}
|
|
165
|
+
/**
|
|
166
|
+
* Parses a command string into executable and arguments safely
|
|
167
|
+
* Supports piped commands (e.g., "grep foo | wc -l")
|
|
168
|
+
*/
|
|
169
|
+
parseCommand(cmdString) {
|
|
170
|
+
// Check for dangerous shell metacharacters (pipe is allowed)
|
|
171
|
+
if (DANGEROUS_PATTERNS.test(cmdString)) {
|
|
172
|
+
throw new Error('Command contains dangerous shell metacharacters');
|
|
173
|
+
}
|
|
174
|
+
// Check if command contains pipes
|
|
175
|
+
const pipeSegments = cmdString.split('|').map(s => s.trim()).filter(s => s.length > 0);
|
|
176
|
+
const isPiped = pipeSegments.length > 1;
|
|
177
|
+
// Validate each command in the pipeline against the whitelist
|
|
178
|
+
const pipeChain = [];
|
|
179
|
+
for (const segment of pipeSegments) {
|
|
180
|
+
const tokens = this.tokenizeCommand(segment);
|
|
181
|
+
if (tokens.length === 0) {
|
|
182
|
+
throw new Error('Empty command in pipeline');
|
|
183
|
+
}
|
|
184
|
+
const segmentCmd = tokens[0];
|
|
185
|
+
if (!ALLOWED_COMMANDS.has(segmentCmd)) {
|
|
186
|
+
throw new Error(`Command not allowed: ${segmentCmd}. Allowed commands: ${Array.from(ALLOWED_COMMANDS).join(', ')}`);
|
|
187
|
+
}
|
|
188
|
+
pipeChain.push(segment);
|
|
161
189
|
}
|
|
162
|
-
|
|
163
|
-
const
|
|
164
|
-
|
|
190
|
+
// For the return value, use the first command's info
|
|
191
|
+
const firstTokens = this.tokenizeCommand(pipeSegments[0]);
|
|
192
|
+
const cmd = firstTokens[0];
|
|
193
|
+
const args = firstTokens.slice(1);
|
|
194
|
+
return { cmd, args, isPiped, pipeChain };
|
|
165
195
|
}
|
|
166
196
|
async execute(toolName, input) {
|
|
167
197
|
switch (toolName) {
|
|
@@ -188,6 +218,27 @@ export class ToolExecutor {
|
|
|
188
218
|
// Validate path doesn't escape working directory
|
|
189
219
|
const fullPath = this.validatePath(validatedInput.path);
|
|
190
220
|
const dir = path.dirname(fullPath);
|
|
221
|
+
// Run syntax validation for code files (unless skipped)
|
|
222
|
+
if (!validatedInput.skipValidation && shouldValidate(validatedInput.path)) {
|
|
223
|
+
const validationResult = validate(validatedInput.path, validatedInput.content);
|
|
224
|
+
// If there are errors, don't write the file
|
|
225
|
+
if (!validationResult.valid) {
|
|
226
|
+
const errorDetails = validationResult.errors.join('\n - ');
|
|
227
|
+
this.ui.printToolCall('write_file', `Writing: ${validatedInput.path}`);
|
|
228
|
+
this.ui.printToolResult(false, 'Syntax validation failed');
|
|
229
|
+
return {
|
|
230
|
+
success: false,
|
|
231
|
+
output: '',
|
|
232
|
+
error: `Syntax validation failed for ${validatedInput.path}:\n - ${errorDetails}\n\nFix the syntax errors and try again.`,
|
|
233
|
+
};
|
|
234
|
+
}
|
|
235
|
+
// Print warnings but continue
|
|
236
|
+
if (validationResult.warnings.length > 0) {
|
|
237
|
+
for (const warning of validationResult.warnings) {
|
|
238
|
+
this.ui.printWarning(warning);
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
}
|
|
191
242
|
// Create directory if it doesn't exist
|
|
192
243
|
await fs.mkdir(dir, { recursive: true });
|
|
193
244
|
// Write the file
|
|
@@ -243,17 +294,8 @@ export class ToolExecutor {
|
|
|
243
294
|
// Validate input structure
|
|
244
295
|
const validatedInput = this.validateRunCommandInput(input);
|
|
245
296
|
// Parse command into executable and arguments
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
if (!ALLOWED_COMMANDS.has(cmd)) {
|
|
249
|
-
this.ui.printToolCall('run_command', `Command: ${validatedInput.command}`);
|
|
250
|
-
this.ui.printToolResult(false, `Command not allowed: ${cmd}`);
|
|
251
|
-
return {
|
|
252
|
-
success: false,
|
|
253
|
-
output: '',
|
|
254
|
-
error: `Command not allowed: ${cmd}. Allowed commands: ${Array.from(ALLOWED_COMMANDS).join(', ')}`,
|
|
255
|
-
};
|
|
256
|
-
}
|
|
297
|
+
// This also validates all commands in a pipeline against the whitelist
|
|
298
|
+
const { cmd, args, isPiped, pipeChain } = this.parseCommand(validatedInput.command);
|
|
257
299
|
// Validate and resolve cwd if provided
|
|
258
300
|
let cwd = this.workingDirectory;
|
|
259
301
|
if (validatedInput.cwd) {
|
|
@@ -270,14 +312,21 @@ export class ToolExecutor {
|
|
|
270
312
|
error: 'User declined to run this command',
|
|
271
313
|
};
|
|
272
314
|
}
|
|
273
|
-
//
|
|
315
|
+
// For piped commands, use shell with pre-validated command string
|
|
316
|
+
// For non-piped commands, use spawn without shell for security
|
|
274
317
|
return new Promise((resolve) => {
|
|
275
|
-
const child =
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
318
|
+
const child = isPiped
|
|
319
|
+
? spawn('sh', ['-c', pipeChain.join(' | ')], {
|
|
320
|
+
cwd,
|
|
321
|
+
stdio: ['inherit', 'pipe', 'pipe'],
|
|
322
|
+
timeout: 60000, // 60 second timeout
|
|
323
|
+
})
|
|
324
|
+
: spawn(cmd, args, {
|
|
325
|
+
cwd,
|
|
326
|
+
stdio: ['inherit', 'pipe', 'pipe'],
|
|
327
|
+
timeout: 60000, // 60 second timeout
|
|
328
|
+
shell: false, // Explicitly disable shell for security
|
|
329
|
+
});
|
|
281
330
|
let stdout = '';
|
|
282
331
|
let stderr = '';
|
|
283
332
|
child.stdout?.on('data', (data) => {
|
package/dist/ui/TerminalUI.js
CHANGED