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 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
+ }
@@ -1,5 +1,6 @@
1
1
  interface StartOptions {
2
2
  directory: string;
3
+ model?: string;
3
4
  }
4
5
  export declare function startCommand(options: StartOptions): Promise<void>;
5
6
  export {};
@@ -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...');
@@ -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 = 'sonnet';
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 haiku`);
115
+ this.ui.printError(`Unknown model: ${modelName}. Use: sonnet, opus, haiku, or opus-4.6`);
113
116
  }
114
117
  }
115
118
  break;
@@ -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;
@@ -2,4 +2,6 @@ export const MODEL_MAP = {
2
2
  sonnet: 'claude-sonnet-4-20250514',
3
3
  opus: 'claude-opus-4-20250514',
4
4
  haiku: 'claude-haiku-4-20250514',
5
+ 'opus-4.6': 'claude-opus-4-6-20260201',
5
6
  };
7
+ export const DEFAULT_MODEL = 'opus-4.6';
@@ -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";
@@ -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
- 'npm',
7
- 'npx',
8
- 'node',
9
- 'mkdir',
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
- 'pnpm',
12
- 'yarn',
13
- 'bun',
14
- 'tsc',
15
- 'vite',
16
- 'esbuild',
17
- 'cat',
18
- 'ls',
19
- 'pwd',
20
- 'echo',
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
- const DANGEROUS_PATTERNS = /[;&|`$(){}[\]<>!\\]/;
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 { path: obj.path.trim(), content: obj.content };
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
- * Parses a command string into executable and arguments safely
128
+ * Tokenizes a single command (no pipes) into tokens respecting quotes
120
129
  */
121
- parseCommand(cmdString) {
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
- if (tokens.length === 0) {
160
- throw new Error('Empty command');
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
- const cmd = tokens[0];
163
- const args = tokens.slice(1);
164
- return { cmd, args };
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
- const { cmd, args } = this.parseCommand(validatedInput.command);
247
- // Check if command is in whitelist
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
- // Use spawn instead of exec for security (no shell interpretation)
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 = spawn(cmd, args, {
276
- cwd,
277
- stdio: ['inherit', 'pipe', 'pipe'],
278
- timeout: 60000, // 60 second timeout
279
- shell: false, // Explicitly disable shell for security
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) => {
@@ -110,6 +110,7 @@ export class TerminalUI {
110
110
  sonnet: 'Claude Sonnet 4',
111
111
  opus: 'Claude Opus 4',
112
112
  haiku: 'Claude Haiku 4',
113
+ 'opus-4.6': 'Claude Opus 4.6',
113
114
  };
114
115
  console.log(chalk.green(`Switched to ${modelNames[model]}`));
115
116
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "threewzrd",
3
- "version": "1.0.0",
3
+ "version": "1.0.3",
4
4
  "description": "AI-powered CLI for generating Three.js projects from natural language",
5
5
  "main": "dist/cli.js",
6
6
  "bin": {