neuro-cli 4.1.2 → 4.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -117,7 +117,14 @@ export class BaseAgent {
117
117
  const streamCallbacks = {
118
118
  onToken: (token) => callbacks?.onToken?.(token),
119
119
  onThinking: (thinking) => callbacks?.onThinking?.(thinking),
120
- onToolCall: (tc) => callbacks?.onToolCall?.(tc.function.name, JSON.parse(tc.function.arguments)),
120
+ onToolCall: (tc) => {
121
+ let args = {};
122
+ try {
123
+ args = JSON.parse(tc.function.arguments);
124
+ }
125
+ catch { /* malformed LLM args */ }
126
+ callbacks?.onToolCall?.(tc.function.name, args);
127
+ },
121
128
  };
122
129
  // Call LLM
123
130
  try {
@@ -183,9 +190,9 @@ export class BaseAgent {
183
190
  break;
184
191
  }
185
192
  }
186
- if (iteration > maxIter) {
193
+ if (iteration > maxIter && execution.status === 'running') {
187
194
  execution.status = 'completed';
188
- execution.result = 'Max iterations reached';
195
+ execution.result = `Max iterations reached (${maxIter}). The task may not be fully completed. Consider using a higher effort level or switching to /orchestrate mode for complex tasks.`;
189
196
  }
190
197
  execution.endTime = Date.now();
191
198
  execution.iterations = iteration;
@@ -204,12 +211,17 @@ export class BaseAgent {
204
211
  * Quick single-turn query (no tool loop)
205
212
  */
206
213
  async query(prompt) {
207
- const messages = [
208
- { role: 'system', content: this.config.systemPrompt, timestamp: Date.now() },
209
- { role: 'user', content: prompt, timestamp: Date.now() },
210
- ];
211
- const response = await this.client.quickChat(this.config.model || 'anthropic/claude-sonnet-4', messages);
212
- return response.content;
214
+ try {
215
+ const messages = [
216
+ { role: 'system', content: this.config.systemPrompt, timestamp: Date.now() },
217
+ { role: 'user', content: prompt, timestamp: Date.now() },
218
+ ];
219
+ const response = await this.client.quickChat(this.config.model || 'anthropic/claude-sonnet-4', messages);
220
+ return response.content;
221
+ }
222
+ catch (error) {
223
+ return `Error: ${error instanceof Error ? error.message : String(error)}`;
224
+ }
213
225
  }
214
226
  }
215
227
  //# sourceMappingURL=base.js.map
@@ -59,7 +59,17 @@ Respond with a JSON plan in this exact format:
59
59
  { role: 'system', content: 'You are an expert task orchestrator. Always respond with valid JSON.', timestamp: Date.now() },
60
60
  { role: 'user', content: planPrompt, timestamp: Date.now() },
61
61
  ];
62
- const response = await this.client.quickChat(this.config.model || 'anthropic/claude-sonnet-4', messages);
62
+ let response;
63
+ try {
64
+ response = await this.client.quickChat(this.config.model || 'anthropic/claude-sonnet-4', messages);
65
+ }
66
+ catch {
67
+ // API error during planning, fallback to direct approach
68
+ return {
69
+ reasoning: 'API error during planning, falling back to direct approach',
70
+ tasks: [{ agent: 'Coder', task, dependsOn: [] }],
71
+ };
72
+ }
63
73
  try {
64
74
  // Extract JSON from response
65
75
  const jsonMatch = response.content.match(/```json\n([\s\S]*?)\n```/) ||
@@ -117,7 +127,11 @@ Respond with a JSON plan in this exact format:
117
127
  return t.dependsOn.every(dep => completedTasks.has(dep));
118
128
  });
119
129
  if (readyTasks.length === 0) {
120
- // Deadlock or all tasks completed
130
+ // Deadlock detection - log unresolved tasks
131
+ const unresolved = plan.tasks.filter(t => !completedTasks.has(t.agent + ':' + t.task));
132
+ if (unresolved.length > 0) {
133
+ callbacks?.onThinking?.(`⚠️ Deadlock detected. Unresolved tasks: ${unresolved.map(t => t.agent + ':' + t.task).join(', ')}`);
134
+ }
121
135
  break;
122
136
  }
123
137
  // Execute ready tasks (could be parallelized in future)
@@ -146,15 +160,27 @@ Respond with a JSON plan in this exact format:
146
160
  fullTask = `${fullTask}\n\n## Context from previous agents:\n${depResults}`;
147
161
  }
148
162
  }
149
- const result = await agent.run(fullTask, {
150
- onToken: (token) => callbacks?.onToken?.(token),
151
- onToolCall: (name, args) => callbacks?.onToolCall?.(name, args),
152
- onToolResult: (name, result, isError) => callbacks?.onToolResult?.(name, result, isError),
153
- onApprovalNeeded: async (name, args, risk) => {
154
- return callbacks?.onApprovalNeeded?.(name, args, risk) ?? true;
155
- },
156
- onThinking: (thinking) => callbacks?.onThinking?.(thinking),
157
- });
163
+ let result;
164
+ try {
165
+ result = await agent.run(fullTask, {
166
+ onToken: (token) => callbacks?.onToken?.(token),
167
+ onToolCall: (name, args) => callbacks?.onToolCall?.(name, args),
168
+ onToolResult: (name, result, isError) => callbacks?.onToolResult?.(name, result, isError),
169
+ onApprovalNeeded: async (name, args, risk) => {
170
+ return callbacks?.onApprovalNeeded?.(name, args, risk) ?? true;
171
+ },
172
+ onThinking: (thinking) => callbacks?.onThinking?.(thinking),
173
+ });
174
+ }
175
+ catch (error) {
176
+ const errMsg = error instanceof Error ? error.message : String(error);
177
+ result = {
178
+ content: `Agent ${subTask.agent} failed: ${errMsg}`,
179
+ toolCallsMade: 0, iterations: 0,
180
+ usage: { inputTokens: 0, outputTokens: 0, cost: 0 },
181
+ execution: { agentName: subTask.agent, task: subTask.task, startTime: Date.now(), iterations: 0, tokensUsed: 0, status: 'failed' },
182
+ };
183
+ }
158
184
  agentResults.set(subTask.agent + ':' + subTask.task, result);
159
185
  totalUsage.inputTokens += result.usage.inputTokens;
160
186
  totalUsage.outputTokens += result.usage.outputTokens;
@@ -173,13 +173,26 @@ export class OpenRouterClient {
173
173
  const { done, value } = await reader.read();
174
174
  if (done)
175
175
  break;
176
- const chunk = decoder.decode(value, { stream: true });
177
- parser.feed(chunk);
176
+ try {
177
+ const chunk = decoder.decode(value, { stream: true });
178
+ parser.feed(chunk);
179
+ }
180
+ catch {
181
+ // Skip malformed SSE chunks
182
+ }
178
183
  }
179
184
  }
180
185
  catch (error) {
181
186
  callbacks.onError?.(error);
182
- throw error;
187
+ // Return partial result instead of crashing the agent
188
+ const content = fullContent.join('');
189
+ const toolCallList = Array.from(toolCalls.values());
190
+ const cost = calculateCost(modelId, inputTokens, outputTokens);
191
+ const usage = { inputTokens, outputTokens, cost };
192
+ this.totalUsage.inputTokens += inputTokens;
193
+ this.totalUsage.outputTokens += outputTokens;
194
+ this.totalUsage.cost += cost;
195
+ return { content, toolCalls: toolCallList, usage };
183
196
  }
184
197
  const content = fullContent.join('');
185
198
  const toolCallList = Array.from(toolCalls.values());
@@ -195,7 +208,13 @@ export class OpenRouterClient {
195
208
  * Handle non-streaming response
196
209
  */
197
210
  async handleNonStreamingResponse(response, modelId) {
198
- const data = await response.json();
211
+ let data;
212
+ try {
213
+ data = await response.json();
214
+ }
215
+ catch {
216
+ throw new Error('Invalid JSON response from OpenRouter API');
217
+ }
199
218
  const content = data.choices?.[0]?.message?.content || '';
200
219
  const toolCalls = data.choices?.[0]?.message?.tool_calls || [];
201
220
  const inputTokens = data.usage?.prompt_tokens || 0;
@@ -19,7 +19,7 @@ export const DEFAULT_CONFIG = {
19
19
  temperature: 0.7,
20
20
  maxTokens: 4096,
21
21
  tools: ['read_file', 'search_files', 'list_directory'],
22
- maxIterations: 3,
22
+ maxIterations: 10,
23
23
  },
24
24
  coder: {
25
25
  name: 'Coder',
@@ -29,7 +29,7 @@ export const DEFAULT_CONFIG = {
29
29
  temperature: 0.4,
30
30
  maxTokens: 16384,
31
31
  tools: ['read_file', 'write_file', 'edit_file', 'search_files', 'list_directory', 'run_command', 'apply_diff'],
32
- maxIterations: 15,
32
+ maxIterations: 50,
33
33
  },
34
34
  reviewer: {
35
35
  name: 'Reviewer',
@@ -39,7 +39,7 @@ export const DEFAULT_CONFIG = {
39
39
  temperature: 0.3,
40
40
  maxTokens: 8192,
41
41
  tools: ['read_file', 'search_files', 'list_directory'],
42
- maxIterations: 5,
42
+ maxIterations: 15,
43
43
  },
44
44
  researcher: {
45
45
  name: 'Researcher',
@@ -49,7 +49,7 @@ export const DEFAULT_CONFIG = {
49
49
  temperature: 0.5,
50
50
  maxTokens: 8192,
51
51
  tools: ['read_file', 'search_files', 'list_directory', 'web_search', 'web_fetch'],
52
- maxIterations: 8,
52
+ maxIterations: 20,
53
53
  },
54
54
  tester: {
55
55
  name: 'Tester',
@@ -59,7 +59,7 @@ export const DEFAULT_CONFIG = {
59
59
  temperature: 0.3,
60
60
  maxTokens: 8192,
61
61
  tools: ['read_file', 'write_file', 'search_files', 'run_command', 'list_directory'],
62
- maxIterations: 10,
62
+ maxIterations: 25,
63
63
  },
64
64
  debugger: {
65
65
  name: 'Debugger',
@@ -69,7 +69,7 @@ export const DEFAULT_CONFIG = {
69
69
  temperature: 0.2,
70
70
  maxTokens: 8192,
71
71
  tools: ['read_file', 'edit_file', 'search_files', 'run_command', 'list_directory', 'apply_diff'],
72
- maxIterations: 12,
72
+ maxIterations: 20,
73
73
  },
74
74
  architect: {
75
75
  name: 'Architect',
@@ -79,7 +79,7 @@ export const DEFAULT_CONFIG = {
79
79
  temperature: 0.7,
80
80
  maxTokens: 8192,
81
81
  tools: ['read_file', 'search_files', 'list_directory'],
82
- maxIterations: 5,
82
+ maxIterations: 15,
83
83
  },
84
84
  devops: {
85
85
  name: 'DevOps',
@@ -89,7 +89,7 @@ export const DEFAULT_CONFIG = {
89
89
  temperature: 0.3,
90
90
  maxTokens: 8192,
91
91
  tools: ['read_file', 'write_file', 'edit_file', 'run_command', 'search_files', 'list_directory'],
92
- maxIterations: 10,
92
+ maxIterations: 25,
93
93
  },
94
94
  },
95
95
  tools: {
@@ -344,27 +344,36 @@ export class ApprovalSystem {
344
344
  readline(prompt) {
345
345
  // Pause the main readline to prevent it from also reading stdin input
346
346
  // This is the root cause of double character input (YY instead of Y)
347
- const mainInputStream = this.mainRl?.input;
348
- const wasMainPaused = mainInputStream?.isPaused?.();
349
- if (this.mainRl && mainInputStream && mainInputStream.isPaused()) {
350
- // Already paused, skip
351
- }
352
- else if (this.mainRl) {
353
- this.mainRl.pause();
347
+ if (this.mainRl) {
348
+ const mainInputStream = this.mainRl?.input;
349
+ if (mainInputStream && !mainInputStream.isPaused()) {
350
+ this.mainRl.pause();
351
+ }
354
352
  }
355
353
  return new Promise((resolve) => {
356
- // Reuse the main readline if available, otherwise create a temporary one
357
- const rl = this.mainRl || this.rl || createInterface({ input: process.stdin, output: process.stdout });
358
- if (!this.rl && !this.mainRl)
359
- this.rl = rl;
360
- rl.question(prompt, (answer) => {
361
- // Resume the main readline after getting the answer
362
- if (this.mainRl) {
363
- this.mainRl.resume();
364
- this.mainRl.prompt();
365
- }
366
- resolve(answer);
367
- });
354
+ // Use the main readline's question method directly - it handles pause/resume internally
355
+ if (this.mainRl) {
356
+ this.mainRl.question(prompt, (answer) => {
357
+ // Resume the main readline after getting the answer
358
+ if (this.mainRl) {
359
+ this.mainRl.resume();
360
+ this.mainRl.prompt();
361
+ }
362
+ resolve(answer);
363
+ });
364
+ }
365
+ else if (this.rl) {
366
+ this.rl.question(prompt, (answer) => {
367
+ resolve(answer);
368
+ });
369
+ }
370
+ else {
371
+ const tempRl = createInterface({ input: process.stdin, output: process.stdout });
372
+ this.rl = tempRl;
373
+ tempRl.question(prompt, (answer) => {
374
+ resolve(answer);
375
+ });
376
+ }
368
377
  });
369
378
  }
370
379
  close() {
@@ -157,6 +157,10 @@ export declare class NeuroEngine {
157
157
  * Now delegates to ModelRouter for more sophisticated analysis
158
158
  */
159
159
  private assessComplexity;
160
+ /**
161
+ * Select the best agent for a given task category
162
+ */
163
+ private selectAgentForCategory;
160
164
  /**
161
165
  * Switch the active model
162
166
  */
@@ -1,5 +1,5 @@
1
1
  // ============================================================
2
- // NeuroCLI - NeuroEngine v4.1.2
2
+ // NeuroCLI - NeuroEngine v4.1.3
3
3
  // The main engine that ties everything together
4
4
  // Now with: Sandbox, Plugin SDK, Enhanced MCP, Enhanced Approval,
5
5
  // Model Router, Prompt Cache, Undo/Redo, Output Styles,
@@ -483,7 +483,7 @@ export class NeuroEngine {
483
483
  this.gitWorktree = new GitWorktreeManager(process.cwd());
484
484
  // Auto-Updater
485
485
  this.updater = new AutoUpdater({
486
- currentVersion: '4.1.2',
486
+ currentVersion: '4.1.3',
487
487
  autoCheck: true,
488
488
  autoUpdate: false,
489
489
  });
@@ -537,7 +537,7 @@ Always consider the strengths of each agent when delegating:
537
537
  temperature: 0.7,
538
538
  maxTokens: 4096,
539
539
  tools: [],
540
- maxIterations: 5,
540
+ maxIterations: 20,
541
541
  };
542
542
  this.orchestrator = new Orchestrator(orchestratorConfig, this.client, this.registry, process.cwd(), this.sessionManager.getCurrent()?.id || 'default');
543
543
  // Register all agents with orchestrator
@@ -622,7 +622,7 @@ Always consider the strengths of each agent when delegating:
622
622
  const cwd = process.cwd();
623
623
  // Built-in agents
624
624
  for (const [key, agentConfig] of Object.entries(this.config.agents)) {
625
- const overrideConfig = { ...agentConfig, model: this.config.defaultModel };
625
+ const overrideConfig = { ...agentConfig };
626
626
  const agent = new BaseAgent(overrideConfig, this.client, this.registry, cwd, sessionId);
627
627
  this.agents.set(agentConfig.name, agent);
628
628
  }
@@ -729,8 +729,12 @@ Always consider the strengths of each agent when delegating:
729
729
  agent.configModel = this.config.defaultModel;
730
730
  }
731
731
  this.ui.startStreaming();
732
- result = await agent.run(message, callbacks);
733
- this.ui.endStreaming();
732
+ try {
733
+ result = await agent.run(message, callbacks);
734
+ }
735
+ finally {
736
+ this.ui.endStreaming();
737
+ }
734
738
  // Restore original model
735
739
  agent.configModel = originalModel;
736
740
  }
@@ -746,15 +750,44 @@ Always consider the strengths of each agent when delegating:
746
750
  }
747
751
  else {
748
752
  const complexity = routeDecision?.complexity || this.assessComplexity(message);
753
+ const category = routeDecision?.category || this.modelRouter.getCategory(message);
749
754
  if (complexity === 'simple') {
750
- const agent = this.agents.get('Coder');
755
+ // Route to the best agent based on task category
756
+ const targetAgentName = this.selectAgentForCategory(category);
757
+ const agent = this.agents.get(targetAgentName);
751
758
  if (agent) {
759
+ this.ui.info(`Using ${targetAgentName} agent (${category} task)`);
752
760
  this.ui.startStreaming();
753
- result = await agent.run(message, callbacks);
754
- this.ui.endStreaming();
761
+ try {
762
+ result = await agent.run(message, callbacks);
763
+ }
764
+ finally {
765
+ this.ui.endStreaming();
766
+ }
755
767
  }
756
768
  else {
757
- throw new Error('Coder agent not initialized');
769
+ // Fallback to Coder
770
+ const coderAgent = this.agents.get('Coder');
771
+ if (coderAgent) {
772
+ this.ui.startStreaming();
773
+ try {
774
+ result = await coderAgent.run(message, callbacks);
775
+ }
776
+ finally {
777
+ this.ui.endStreaming();
778
+ }
779
+ }
780
+ else {
781
+ this.ui.warning('No agents initialized, using orchestration mode');
782
+ const orchestrateResult = await this.orchestrator.orchestrate(message, callbacks);
783
+ result = {
784
+ content: orchestrateResult.content,
785
+ toolCallsMade: 0,
786
+ iterations: orchestrateResult.execution.iterations,
787
+ usage: orchestrateResult.totalUsage,
788
+ execution: orchestrateResult.execution,
789
+ };
790
+ }
758
791
  }
759
792
  }
760
793
  else {
@@ -888,6 +921,22 @@ Always consider the strengths of each agent when delegating:
888
921
  const decision = this.modelRouter.route(message);
889
922
  return decision.complexity;
890
923
  }
924
+ /**
925
+ * Select the best agent for a given task category
926
+ */
927
+ selectAgentForCategory(category) {
928
+ const categoryAgentMap = {
929
+ code: 'Coder',
930
+ reasoning: 'Architect',
931
+ creative: 'Coder',
932
+ analysis: 'Researcher',
933
+ conversation: 'Coder',
934
+ debugging: 'Debugger',
935
+ review: 'Reviewer',
936
+ refactoring: 'Coder',
937
+ };
938
+ return categoryAgentMap[category] || 'Coder';
939
+ }
891
940
  /**
892
941
  * Switch the active model
893
942
  */
@@ -72,7 +72,7 @@ const COMPLEXITY_SIGNALS = {
72
72
  /\b(hello|hi|thanks|yes|no|ok|sure|done|correct|right|please)\b/i,
73
73
  ],
74
74
  lengthThreshold: 80,
75
- maxSteps: 1,
75
+ maxSteps: 5,
76
76
  },
77
77
  moderate: {
78
78
  patterns: [
@@ -83,7 +83,7 @@ const COMPLEXITY_SIGNALS = {
83
83
  /\b(refactor|clean|simplify|optimize|restructure)\b/i,
84
84
  ],
85
85
  lengthThreshold: 300,
86
- maxSteps: 3,
86
+ maxSteps: 15,
87
87
  },
88
88
  complex: {
89
89
  patterns: [
@@ -97,7 +97,7 @@ const COMPLEXITY_SIGNALS = {
97
97
  /\b(security\s+(audit|review|hardening)|performance\s+(optim|tuning|profiling)|scalab(ility|le))\b/i,
98
98
  ],
99
99
  lengthThreshold: 500,
100
- maxSteps: 6,
100
+ maxSteps: 30,
101
101
  },
102
102
  };
103
103
  // ---------------------------------------------------------------------------