threewzrd 1.0.6 → 1.0.7

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.
@@ -58,17 +58,14 @@ export class AgentEngine {
58
58
  }
59
59
  async runAgentLoop() {
60
60
  let continueLoop = true;
61
- let turn = 1;
62
61
  while (continueLoop) {
63
- continueLoop = await this.runSingleTurn(turn);
64
- turn++;
62
+ continueLoop = await this.runSingleTurn();
65
63
  }
66
64
  }
67
- async runSingleTurn(turn, retryCount = 0) {
65
+ async runSingleTurn(retryCount = 0) {
68
66
  try {
69
- // Show thinking indicator with turn info
70
- const thinkingMessage = turn === 1 ? 'Thinking' : 'Processing';
71
- this.ui.startThinking(thinkingMessage, turn);
67
+ // Show thinking indicator
68
+ this.ui.startThinking('Thinking');
72
69
  // Create the API request with streaming
73
70
  const stream = this.client.messages.stream({
74
71
  model: MODEL_MAP[this.model],
@@ -113,13 +110,8 @@ export class AgentEngine {
113
110
  }
114
111
  // Execute all tool calls
115
112
  const toolResults = [];
116
- const totalTools = toolUseBlocks.length;
117
- for (let i = 0; i < toolUseBlocks.length; i++) {
118
- const toolUse = toolUseBlocks[i];
119
- const toolProgress = totalTools > 1 ? ` (${i + 1}/${totalTools})` : '';
120
- this.ui.startThinking(`Executing ${toolUse.name}${toolProgress}`, turn);
113
+ for (const toolUse of toolUseBlocks) {
121
114
  const result = await this.toolExecutor.execute(toolUse.name, toolUse.input);
122
- this.ui.stopThinking();
123
115
  // Truncate large outputs to save tokens
124
116
  let output = result.success ? result.output : `Error: ${result.error}`;
125
117
  if (output.length > 2000) {
@@ -148,7 +140,7 @@ export class AgentEngine {
148
140
  const delay = RETRY_DELAY_MS * Math.pow(2, retryCount);
149
141
  this.ui.printWarning(`Rate limited. Retrying in ${delay / 1000}s...`);
150
142
  await this.sleep(delay);
151
- return this.runSingleTurn(turn, retryCount + 1);
143
+ return this.runSingleTurn(retryCount + 1);
152
144
  }
153
145
  this.ui.printError('Rate limit exceeded. Please wait a moment and try again.');
154
146
  return false;
@@ -9,7 +9,6 @@ export declare class ThreeJsWizard {
9
9
  private workingDirectory;
10
10
  private isRunning;
11
11
  private hasOnboarded;
12
- private currentMode;
13
12
  constructor(options?: WizardOptions);
14
13
  start(): Promise<void>;
15
14
  private handleCommand;
@@ -9,7 +9,6 @@ export class ThreeJsWizard {
9
9
  workingDirectory;
10
10
  isRunning = false;
11
11
  hasOnboarded = false;
12
- currentMode = 'single-shot';
13
12
  constructor(options) {
14
13
  this.workingDirectory = process.cwd();
15
14
  this.ui = new TerminalUI();
@@ -43,7 +42,6 @@ export class ThreeJsWizard {
43
42
  if (!this.hasOnboarded && isEmptyDir) {
44
43
  const preferences = await runOnboarding(this.ui);
45
44
  this.hasOnboarded = true;
46
- this.currentMode = preferences.mode;
47
45
  // Process the initial project request
48
46
  const contextMessage = buildContextMessage(preferences);
49
47
  await this.engine.processMessage(contextMessage);
@@ -55,8 +53,7 @@ export class ThreeJsWizard {
55
53
  // Main REPL loop
56
54
  while (this.isRunning) {
57
55
  try {
58
- const { text: input, mode } = await this.ui.promptWithMode(this.currentMode);
59
- this.currentMode = mode;
56
+ const input = await this.ui.prompt();
60
57
  if (!input) {
61
58
  continue;
62
59
  }
@@ -65,13 +62,8 @@ export class ThreeJsWizard {
65
62
  await this.handleCommand(input);
66
63
  continue;
67
64
  }
68
- // Build message with mode context
69
- const modePrefix = mode === 'planning'
70
- ? '[PLANNING MODE] Output a detailed implementation plan before coding.\n\n'
71
- : '';
72
- const fullMessage = modePrefix + input;
73
65
  // Process user message through agent
74
- await this.engine.processMessage(fullMessage);
66
+ await this.engine.processMessage(input);
75
67
  // Track created files
76
68
  for (const file of this.engine.getCreatedFiles()) {
77
69
  this.projectManager.addFile(file);
@@ -4,12 +4,10 @@ export declare const MODEL_MAP: Record<ModelId, string>;
4
4
  export declare const DEFAULT_MODEL: ModelId;
5
5
  export type ProjectLanguage = 'javascript' | 'typescript';
6
6
  export type ProjectTarget = 'browser' | 'mobile' | 'desktop';
7
- export type ExecutionMode = 'single-shot' | 'planning';
8
7
  export interface ProjectPreferences {
9
8
  language: ProjectLanguage;
10
9
  target: ProjectTarget;
11
10
  description: string;
12
- mode: ExecutionMode;
13
11
  }
14
12
  export interface ProjectConfig {
15
13
  name: string;
@@ -1,7 +1,6 @@
1
1
  import * as fs from 'fs/promises';
2
2
  import * as path from 'path';
3
3
  import { spawn } from 'child_process';
4
- import chalk from 'chalk';
5
4
  import { shouldValidate, validate } from './CodeValidator.js';
6
5
  // Whitelist of allowed commands for security
7
6
  const ALLOWED_COMMANDS = new Set([
@@ -233,7 +232,6 @@ export class ToolExecutor {
233
232
  try {
234
233
  // Validate input structure
235
234
  const validatedInput = this.validateWriteFileInput(input);
236
- this.ui.startToolSpinner('write_file', `Writing ${validatedInput.path}`);
237
235
  // Validate path doesn't escape working directory
238
236
  const fullPath = this.validatePath(validatedInput.path);
239
237
  const dir = path.dirname(fullPath);
@@ -243,7 +241,8 @@ export class ToolExecutor {
243
241
  // If there are errors, don't write the file
244
242
  if (!validationResult.valid) {
245
243
  const errorDetails = validationResult.errors.join('\n - ');
246
- this.ui.failToolSpinner('Syntax validation failed');
244
+ this.ui.printToolCall('write_file', `Writing: ${validatedInput.path}`);
245
+ this.ui.printToolResult(false, 'Syntax validation failed');
247
246
  return {
248
247
  success: false,
249
248
  output: '',
@@ -262,7 +261,8 @@ export class ToolExecutor {
262
261
  // Write the file
263
262
  await fs.writeFile(fullPath, validatedInput.content, 'utf-8');
264
263
  this.createdFiles.add(validatedInput.path);
265
- this.ui.succeedToolSpinner(`Wrote ${validatedInput.path}`);
264
+ this.ui.printToolCall('write_file', `Writing: ${validatedInput.path}`);
265
+ this.ui.printToolResult(true, '');
266
266
  return {
267
267
  success: true,
268
268
  output: `Successfully wrote ${validatedInput.path}`,
@@ -270,7 +270,9 @@ export class ToolExecutor {
270
270
  }
271
271
  catch (error) {
272
272
  const errorMessage = error instanceof Error ? error.message : String(error);
273
- this.ui.failToolSpinner(`Failed to write: ${errorMessage}`);
273
+ const displayPath = input?.path || 'unknown';
274
+ this.ui.printToolCall('write_file', `Writing: ${displayPath}`);
275
+ this.ui.printToolResult(false, errorMessage);
274
276
  return {
275
277
  success: false,
276
278
  output: '',
@@ -282,11 +284,11 @@ export class ToolExecutor {
282
284
  try {
283
285
  // Validate input structure
284
286
  const validatedInput = this.validateReadFileInput(input);
285
- this.ui.startToolSpinner('read_file', `Reading ${validatedInput.path}`);
286
287
  // Validate path doesn't escape working directory
287
288
  const fullPath = this.validatePath(validatedInput.path);
288
289
  const content = await fs.readFile(fullPath, 'utf-8');
289
- this.ui.succeedToolSpinner(`Read ${validatedInput.path}`);
290
+ this.ui.printToolCall('read_file', `Reading: ${validatedInput.path}`);
291
+ this.ui.printToolResult(true, '');
290
292
  return {
291
293
  success: true,
292
294
  output: content,
@@ -294,7 +296,9 @@ export class ToolExecutor {
294
296
  }
295
297
  catch (error) {
296
298
  const errorMessage = error instanceof Error ? error.message : String(error);
297
- this.ui.failToolSpinner(`Failed to read: ${errorMessage}`);
299
+ const displayPath = input?.path || 'unknown';
300
+ this.ui.printToolCall('read_file', `Reading: ${displayPath}`);
301
+ this.ui.printToolResult(false, errorMessage);
298
302
  return {
299
303
  success: false,
300
304
  output: '',
@@ -314,20 +318,17 @@ export class ToolExecutor {
314
318
  if (validatedInput.cwd) {
315
319
  cwd = this.validatePath(validatedInput.cwd);
316
320
  }
317
- // Show command for user confirmation
318
- console.log();
319
- console.log(chalk.yellow(`[Command] `) + chalk.gray(validatedInput.command));
321
+ this.ui.printToolCall('run_command', `Command: ${validatedInput.command}`);
320
322
  // Ask for user confirmation before running any command
321
323
  const approved = await this.ui.confirm(`Run this command?`);
322
324
  if (!approved) {
323
- console.log(chalk.red(' Declined'));
325
+ this.ui.printToolResult(false, 'User declined');
324
326
  return {
325
327
  success: false,
326
328
  output: '',
327
329
  error: 'User declined to run this command',
328
330
  };
329
331
  }
330
- this.ui.startToolSpinner('run_command', `Running: ${validatedInput.command}`);
331
332
  // For piped commands, use shell with pre-validated command string
332
333
  // For non-piped commands, use spawn without shell for security
333
334
  return new Promise((resolve) => {
@@ -354,14 +355,14 @@ export class ToolExecutor {
354
355
  child.on('close', (code) => {
355
356
  const output = stdout + (stderr ? `\nStderr: ${stderr}` : '');
356
357
  if (code === 0) {
357
- this.ui.succeedToolSpinner('Command completed');
358
+ this.ui.printToolResult(true, '');
358
359
  resolve({
359
360
  success: true,
360
361
  output: output || 'Command completed successfully',
361
362
  });
362
363
  }
363
364
  else {
364
- this.ui.failToolSpinner(`Exit code: ${code}`);
365
+ this.ui.printToolResult(false, `Exit code: ${code}`);
365
366
  resolve({
366
367
  success: false,
367
368
  output: '',
@@ -370,7 +371,7 @@ export class ToolExecutor {
370
371
  }
371
372
  });
372
373
  child.on('error', (error) => {
373
- this.ui.failToolSpinner(error.message);
374
+ this.ui.printToolResult(false, error.message);
374
375
  resolve({
375
376
  success: false,
376
377
  output: '',
@@ -381,7 +382,9 @@ export class ToolExecutor {
381
382
  }
382
383
  catch (error) {
383
384
  const errorMessage = error instanceof Error ? error.message : String(error);
384
- this.ui.failToolSpinner(`Failed: ${errorMessage}`);
385
+ const displayCmd = input?.command || 'unknown';
386
+ this.ui.printToolCall('run_command', `Command: ${displayCmd}`);
387
+ this.ui.printToolResult(false, errorMessage);
385
388
  return {
386
389
  success: false,
387
390
  output: '',
@@ -393,16 +396,15 @@ export class ToolExecutor {
393
396
  try {
394
397
  // Validate input structure
395
398
  const validatedInput = this.validateListFilesInput(input);
396
- const displayPath = validatedInput.path || '.';
397
- this.ui.startToolSpinner('list_files', `Listing ${displayPath}`);
398
399
  // Validate path doesn't escape working directory
399
400
  const targetPath = validatedInput.path
400
401
  ? this.validatePath(validatedInput.path)
401
402
  : this.workingDirectory;
403
+ this.ui.printToolCall('list_files', `Listing: ${validatedInput.path || '.'}`);
402
404
  const files = await this.listFilesRecursive(targetPath, validatedInput.recursive ?? false);
403
405
  // Format output
404
406
  const relativePaths = files.map(f => path.relative(this.workingDirectory, f));
405
- this.ui.succeedToolSpinner(`Listed ${relativePaths.length} items`);
407
+ this.ui.printToolResult(true, '');
406
408
  return {
407
409
  success: true,
408
410
  output: relativePaths.length > 0
@@ -412,7 +414,9 @@ export class ToolExecutor {
412
414
  }
413
415
  catch (error) {
414
416
  const errorMessage = error instanceof Error ? error.message : String(error);
415
- this.ui.failToolSpinner(`Failed to list: ${errorMessage}`);
417
+ const displayPath = input?.path || '.';
418
+ this.ui.printToolCall('list_files', `Listing: ${displayPath}`);
419
+ this.ui.printToolResult(false, errorMessage);
416
420
  return {
417
421
  success: false,
418
422
  output: '',
@@ -1,4 +1,4 @@
1
- import { ModelId, ExecutionMode } from '../core/types.js';
1
+ import { ModelId } from '../core/types.js';
2
2
  export interface SelectOption {
3
3
  label: string;
4
4
  value: string;
@@ -7,15 +7,9 @@ export declare class TerminalUI {
7
7
  private rl;
8
8
  private isStreaming;
9
9
  private thinkingSpinner;
10
- private toolSpinner;
11
10
  constructor();
12
- startThinking(message?: string, turn?: number): void;
13
- updateThinking(message: string, turn?: number): void;
11
+ startThinking(message?: string): void;
14
12
  stopThinking(): void;
15
- startToolSpinner(toolName: string, detail: string): void;
16
- succeedToolSpinner(message?: string): void;
17
- failToolSpinner(message: string): void;
18
- stopToolSpinner(): void;
19
13
  confirm(message: string): Promise<boolean>;
20
14
  select(question: string, options: SelectOption[]): Promise<string>;
21
15
  printBanner(): void;
@@ -37,11 +31,6 @@ export declare class TerminalUI {
37
31
  streamText(text: string): void;
38
32
  endStreaming(): void;
39
33
  prompt(): Promise<string>;
40
- promptWithMode(defaultMode?: ExecutionMode): Promise<{
41
- text: string;
42
- mode: ExecutionMode;
43
- }>;
44
- printModeInfo(mode: ExecutionMode): void;
45
34
  close(): void;
46
35
  clearScreen(): void;
47
36
  }
@@ -5,28 +5,20 @@ export class TerminalUI {
5
5
  rl;
6
6
  isStreaming = false;
7
7
  thinkingSpinner = null;
8
- toolSpinner = null;
9
8
  constructor() {
10
9
  this.rl = readline.createInterface({
11
10
  input: process.stdin,
12
11
  output: process.stdout,
13
12
  });
14
13
  }
15
- // Thinking indicator with turn info
16
- startThinking(message = 'Thinking', turn) {
17
- const turnInfo = turn ? chalk.gray(` [Turn ${turn}]`) : '';
14
+ // Thinking indicator
15
+ startThinking(message = 'Thinking') {
18
16
  this.thinkingSpinner = ora({
19
- text: chalk.cyan(message) + turnInfo,
17
+ text: chalk.cyan(message),
20
18
  spinner: 'dots',
21
19
  discardStdin: false, // Don't interfere with readline
22
20
  }).start();
23
21
  }
24
- updateThinking(message, turn) {
25
- if (this.thinkingSpinner) {
26
- const turnInfo = turn ? chalk.gray(` [Turn ${turn}]`) : '';
27
- this.thinkingSpinner.text = chalk.cyan(message) + turnInfo;
28
- }
29
- }
30
22
  stopThinking() {
31
23
  if (this.thinkingSpinner) {
32
24
  this.thinkingSpinner.stop();
@@ -37,32 +29,6 @@ export class TerminalUI {
37
29
  process.stdin.resume();
38
30
  }
39
31
  }
40
- // Tool execution spinner
41
- startToolSpinner(toolName, detail) {
42
- this.toolSpinner = ora({
43
- text: chalk.yellow(`${toolName}: `) + chalk.gray(detail),
44
- spinner: 'dots',
45
- discardStdin: false,
46
- }).start();
47
- }
48
- succeedToolSpinner(message) {
49
- if (this.toolSpinner) {
50
- this.toolSpinner.succeed(message ? chalk.green(message) : undefined);
51
- this.toolSpinner = null;
52
- }
53
- }
54
- failToolSpinner(message) {
55
- if (this.toolSpinner) {
56
- this.toolSpinner.fail(chalk.red(message));
57
- this.toolSpinner = null;
58
- }
59
- }
60
- stopToolSpinner() {
61
- if (this.toolSpinner) {
62
- this.toolSpinner.stop();
63
- this.toolSpinner = null;
64
- }
65
- }
66
32
  // Confirmation prompt for dangerous actions
67
33
  async confirm(message) {
68
34
  // Ensure any spinner is stopped
@@ -112,18 +78,12 @@ export class TerminalUI {
112
78
  console.log(chalk.gray(' lighting, animations, and more - just describe what'));
113
79
  console.log(chalk.gray(' you want in plain English.'));
114
80
  console.log();
115
- console.log(chalk.gray(' Modes: ') + chalk.cyan('⚡ Quick') + chalk.gray(' | ') + chalk.magenta('📋 Plan') + chalk.gray(' (Tab to switch)'));
116
81
  console.log(chalk.gray(' Commands: ') + chalk.yellow('/help') + chalk.gray(' | ') + chalk.yellow('/clear') + chalk.gray(' | ') + chalk.yellow('/exit'));
117
82
  console.log();
118
83
  console.log(chalk.gray(' ─────────────────────────────────────────'));
119
84
  console.log();
120
85
  }
121
86
  printHelp() {
122
- console.log();
123
- console.log(chalk.yellow('Execution Modes:'));
124
- console.log(chalk.cyan(' ⚡ Quick Mode') + chalk.gray(' - Direct implementation (default)'));
125
- console.log(chalk.magenta(' 📋 Plan Mode') + chalk.gray(' - Detailed architecture plan first'));
126
- console.log(chalk.gray(' Press Tab while typing to switch modes'));
127
87
  console.log();
128
88
  console.log(chalk.yellow('Commands:'));
129
89
  console.log(chalk.cyan(' /help') + chalk.gray(' - Show this help message'));
@@ -216,100 +176,6 @@ export class TerminalUI {
216
176
  });
217
177
  });
218
178
  }
219
- // Mode-aware prompt with Tab toggle for execution mode
220
- async promptWithMode(defaultMode = 'single-shot') {
221
- let currentMode = defaultMode;
222
- // Print mode indicator and instructions
223
- const printModeBar = () => {
224
- const singleShot = currentMode === 'single-shot'
225
- ? chalk.bgCyan.black(' ⚡ Quick ')
226
- : chalk.gray(' ⚡ Quick ');
227
- const planning = currentMode === 'planning'
228
- ? chalk.bgMagenta.white(' 📋 Plan ')
229
- : chalk.gray(' 📋 Plan ');
230
- // Clear line and reprint
231
- process.stdout.write('\r\x1b[K');
232
- process.stdout.write(` ${singleShot} ${planning} ${chalk.gray('Tab to switch')}\n`);
233
- };
234
- return new Promise((resolve) => {
235
- printModeBar();
236
- console.log();
237
- // Close the existing readline interface to fully release stdin
238
- this.rl.close();
239
- // Store the current input
240
- let inputBuffer = '';
241
- // Set up raw mode for keypress detection
242
- if (process.stdin.isTTY) {
243
- process.stdin.setRawMode(true);
244
- }
245
- process.stdin.resume();
246
- const promptPrefix = chalk.magenta(' › ');
247
- process.stdout.write(promptPrefix);
248
- const handleKeypress = (chunk) => {
249
- const key = chunk.toString();
250
- // Tab key - toggle mode
251
- if (key === '\t') {
252
- currentMode = currentMode === 'single-shot' ? 'planning' : 'single-shot';
253
- // Move cursor up, clear the mode bar, reprint it, move back down
254
- process.stdout.write('\x1b[2A'); // Move up 2 lines
255
- printModeBar();
256
- console.log();
257
- process.stdout.write(promptPrefix + inputBuffer);
258
- return;
259
- }
260
- // Enter key - submit
261
- if (key === '\r' || key === '\n') {
262
- process.stdout.write('\n');
263
- cleanup();
264
- resolve({ text: inputBuffer.trim(), mode: currentMode });
265
- return;
266
- }
267
- // Ctrl+C - exit
268
- if (key === '\x03') {
269
- cleanup();
270
- process.exit(0);
271
- }
272
- // Backspace
273
- if (key === '\x7f' || key === '\b') {
274
- if (inputBuffer.length > 0) {
275
- inputBuffer = inputBuffer.slice(0, -1);
276
- process.stdout.write('\b \b');
277
- }
278
- return;
279
- }
280
- // Escape sequences (arrow keys, etc.) - ignore for simplicity
281
- if (key.startsWith('\x1b')) {
282
- return;
283
- }
284
- // Regular character
285
- if (key.length === 1 && key.charCodeAt(0) >= 32) {
286
- inputBuffer += key;
287
- process.stdout.write(key);
288
- }
289
- };
290
- const cleanup = () => {
291
- process.stdin.removeListener('data', handleKeypress);
292
- if (process.stdin.isTTY) {
293
- process.stdin.setRawMode(false);
294
- }
295
- // Recreate readline interface
296
- this.rl = readline.createInterface({
297
- input: process.stdin,
298
- output: process.stdout,
299
- });
300
- };
301
- process.stdin.on('data', handleKeypress);
302
- });
303
- }
304
- printModeInfo(mode) {
305
- if (mode === 'single-shot') {
306
- console.log(chalk.cyan(' ⚡ Quick Mode: ') + chalk.gray('Direct implementation without formal planning'));
307
- }
308
- else {
309
- console.log(chalk.magenta(' 📋 Plan Mode: ') + chalk.gray('Detailed architecture plan before implementation'));
310
- }
311
- console.log();
312
- }
313
179
  close() {
314
180
  this.rl.close();
315
181
  }
@@ -12,33 +12,27 @@ export async function runOnboarding(ui) {
12
12
  { label: 'Mobile (React Native, etc.)', value: 'mobile' },
13
13
  { label: 'Desktop (Electron, etc.)', value: 'desktop' },
14
14
  ]);
15
- // Ask for project description with mode toggle
15
+ // Ask for project description
16
16
  console.log();
17
- console.log(chalk.cyan(' Describe what you\'d like to build:'));
17
+ console.log(chalk.cyan(' Now describe what you\'d like to build:'));
18
18
  console.log(chalk.gray(' (e.g., "A 3D solar system with orbiting planets")'));
19
19
  console.log();
20
- const { text: description, mode } = await ui.promptWithMode('single-shot');
20
+ const description = await ui.prompt();
21
21
  console.log();
22
- ui.printModeInfo(mode);
23
22
  console.log(chalk.gray(' ─────────────────────────────────────────'));
24
23
  console.log();
25
24
  return {
26
25
  language,
27
26
  target,
28
27
  description,
29
- mode,
30
28
  };
31
29
  }
32
30
  export function buildContextMessage(prefs) {
33
- const modeInstruction = prefs.mode === 'planning'
34
- ? `\n\nIMPORTANT: The user has requested Planning Mode. You MUST output a detailed implementation plan with Architecture Overview, Dependencies, File Structure, and Execution Steps BEFORE writing any code.`
35
- : `\n\nThe user has requested Single-Shot Mode. Plan internally and proceed directly to implementation without outputting a formal plan.`;
36
31
  return `The user wants to create a Three.js project with these preferences:
37
32
  - Language: ${prefs.language}
38
33
  - Target platform: ${prefs.target}
39
- - Execution mode: ${prefs.mode === 'planning' ? 'Planning Mode (detailed plan first)' : 'Single-Shot Mode (direct implementation)'}
40
34
 
41
- Their project description: ${prefs.description}${modeInstruction}
35
+ Their project description: ${prefs.description}
42
36
 
43
37
  Please create the project structure and initial files based on these requirements. Start by setting up the basic Three.js scene.`;
44
38
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "threewzrd",
3
- "version": "1.0.6",
3
+ "version": "1.0.7",
4
4
  "description": "AI-powered CLI for generating Three.js projects from natural language",
5
5
  "main": "dist/cli.js",
6
6
  "bin": {