threewzrd 1.0.7 → 1.0.8

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.
@@ -5,7 +5,7 @@ import { ToolExecutor } from '../tools/ToolExecutor.js';
5
5
  import { THREEJS_SYSTEM_PROMPT } from '../prompts/system.js';
6
6
  // Limits to prevent hitting rate limits
7
7
  const MAX_HISTORY_MESSAGES = 20; // Keep last N messages
8
- const MAX_TOKENS = 4096; // Reduced from 8192
8
+ const MAX_TOKENS = 16384; // Needs to be large enough for file contents in tool calls
9
9
  const MAX_RETRIES = 3;
10
10
  const RETRY_DELAY_MS = 5000; // 5 seconds base delay
11
11
  export class AgentEngine {
@@ -93,6 +93,10 @@ export class AgentEngine {
93
93
  if (!isFirstText) {
94
94
  this.ui.endStreaming();
95
95
  }
96
+ // Check for truncated response
97
+ if (response.stop_reason === 'max_tokens') {
98
+ this.ui.printWarning('Response was truncated due to length limit. Some tool calls may be incomplete.');
99
+ }
96
100
  // Process all content blocks
97
101
  for (const block of response.content) {
98
102
  contentBlocks.push(block);
@@ -108,9 +112,15 @@ export class AgentEngine {
108
112
  // No tool calls, we're done
109
113
  return false;
110
114
  }
115
+ // Show tool processing spinner
116
+ this.ui.startToolProcessing(toolUseBlocks.length);
111
117
  // Execute all tool calls
112
118
  const toolResults = [];
113
119
  for (const toolUse of toolUseBlocks) {
120
+ // Update spinner with current tool name
121
+ this.ui.updateToolProcessing(toolUse.name);
122
+ // Stop spinner before tool execution (which prints its own output)
123
+ this.ui.stopToolProcessing();
114
124
  const result = await this.toolExecutor.execute(toolUse.name, toolUse.input);
115
125
  // Truncate large outputs to save tokens
116
126
  let output = result.success ? result.output : `Error: ${result.error}`;
@@ -123,17 +133,23 @@ export class AgentEngine {
123
133
  content: output,
124
134
  is_error: !result.success,
125
135
  });
136
+ // Restart spinner if more tools remain
137
+ const remainingTools = toolUseBlocks.length - toolResults.length;
138
+ if (remainingTools > 0) {
139
+ this.ui.startToolProcessing(remainingTools);
140
+ }
126
141
  }
127
142
  // Add tool results to history
128
143
  this.conversationHistory.push({
129
144
  role: 'user',
130
145
  content: toolResults,
131
146
  });
132
- // Continue the loop if we have tool results to process
133
- return response.stop_reason === 'tool_use';
147
+ // Always continue after tool execution - model must see results
148
+ return true;
134
149
  }
135
150
  catch (error) {
136
151
  this.ui.stopThinking();
152
+ this.ui.stopToolProcessing();
137
153
  // Handle rate limit errors with retry
138
154
  if (error instanceof Anthropic.RateLimitError) {
139
155
  if (retryCount < MAX_RETRIES) {
@@ -58,20 +58,21 @@ export class ToolExecutor {
58
58
  */
59
59
  validateWriteFileInput(input) {
60
60
  if (!input || typeof input !== 'object') {
61
- throw new Error('Invalid input: expected object');
61
+ throw new Error(`Invalid input: expected object, got ${typeof input}: ${JSON.stringify(input)}`);
62
62
  }
63
63
  const obj = input;
64
64
  if (typeof obj.path !== 'string' || !obj.path.trim()) {
65
- throw new Error('Invalid input: path must be a non-empty string');
65
+ throw new Error(`Invalid input: path must be a non-empty string. Received keys: ${Object.keys(obj).join(', ')}`);
66
+ }
67
+ // Content is required - reject null/undefined
68
+ if (obj.content === null || obj.content === undefined) {
69
+ throw new Error(`Invalid input: content is required. Received keys: [${Object.keys(obj).join(', ')}], content type: ${typeof obj.content}`);
66
70
  }
67
71
  // Coerce content to string - handle various types the model might return
68
72
  let content;
69
73
  if (typeof obj.content === 'string') {
70
74
  content = obj.content;
71
75
  }
72
- else if (obj.content === null || obj.content === undefined) {
73
- content = '';
74
- }
75
76
  else if (Array.isArray(obj.content)) {
76
77
  // Model sometimes returns content as an array of strings
77
78
  content = obj.content.map(item => String(item)).join('\n');
@@ -84,6 +85,10 @@ export class ToolExecutor {
84
85
  // Fallback for numbers, booleans, etc.
85
86
  content = String(obj.content);
86
87
  }
88
+ // Reject empty content - likely a model error
89
+ if (!content.trim()) {
90
+ throw new Error('Invalid input: content cannot be empty');
91
+ }
87
92
  return {
88
93
  path: obj.path.trim(),
89
94
  content,
@@ -7,9 +7,13 @@ export declare class TerminalUI {
7
7
  private rl;
8
8
  private isStreaming;
9
9
  private thinkingSpinner;
10
+ private toolSpinner;
10
11
  constructor();
11
12
  startThinking(message?: string): void;
12
13
  stopThinking(): void;
14
+ startToolProcessing(toolCount: number): void;
15
+ updateToolProcessing(toolName: string): void;
16
+ stopToolProcessing(): void;
13
17
  confirm(message: string): Promise<boolean>;
14
18
  select(question: string, options: SelectOption[]): Promise<string>;
15
19
  printBanner(): void;
@@ -5,6 +5,7 @@ export class TerminalUI {
5
5
  rl;
6
6
  isStreaming = false;
7
7
  thinkingSpinner = null;
8
+ toolSpinner = null;
8
9
  constructor() {
9
10
  this.rl = readline.createInterface({
10
11
  input: process.stdin,
@@ -29,6 +30,26 @@ export class TerminalUI {
29
30
  process.stdin.resume();
30
31
  }
31
32
  }
33
+ // Tool processing spinner
34
+ startToolProcessing(toolCount) {
35
+ const plural = toolCount > 1 ? 's' : '';
36
+ this.toolSpinner = ora({
37
+ text: chalk.yellow(`Executing ${toolCount} tool${plural}...`),
38
+ spinner: 'dots',
39
+ discardStdin: false,
40
+ }).start();
41
+ }
42
+ updateToolProcessing(toolName) {
43
+ if (this.toolSpinner) {
44
+ this.toolSpinner.text = chalk.yellow(`Executing: ${toolName}...`);
45
+ }
46
+ }
47
+ stopToolProcessing() {
48
+ if (this.toolSpinner) {
49
+ this.toolSpinner.stop();
50
+ this.toolSpinner = null;
51
+ }
52
+ }
32
53
  // Confirmation prompt for dangerous actions
33
54
  async confirm(message) {
34
55
  // Ensure any spinner is stopped
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "threewzrd",
3
- "version": "1.0.7",
3
+ "version": "1.0.8",
4
4
  "description": "AI-powered CLI for generating Three.js projects from natural language",
5
5
  "main": "dist/cli.js",
6
6
  "bin": {