neuro-cli 4.1.0 → 4.1.2

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.
@@ -1,3 +1,4 @@
1
+ import { Interface as ReadLineInterface } from 'readline';
1
2
  export type PermissionMode = 'manual' | 'auto' | 'plan' | 'yolo';
2
3
  export interface ApprovalResult {
3
4
  approved: boolean;
@@ -39,6 +40,7 @@ export declare class ApprovalSystem {
39
40
  private persistDecisions;
40
41
  private consecutiveAutoApproves;
41
42
  private rl;
43
+ private mainRl;
42
44
  private stats;
43
45
  private pendingBatch;
44
46
  private batchTimer;
@@ -65,6 +67,11 @@ export declare class ApprovalSystem {
65
67
  private isWhitelisted;
66
68
  private getPattern;
67
69
  private getAlwaysKey;
70
+ /**
71
+ * Set the main readline interface from index.ts
72
+ * This prevents creating a second readline on the same stdin
73
+ */
74
+ setMainReadline(rl: ReadLineInterface): void;
68
75
  private readline;
69
76
  close(): void;
70
77
  addAutoApprove(toolName: string): void;
@@ -23,6 +23,7 @@ export class ApprovalSystem {
23
23
  persistDecisions = true;
24
24
  consecutiveAutoApproves = 0;
25
25
  rl = null;
26
+ mainRl = null;
26
27
  stats = new Map();
27
28
  pendingBatch = [];
28
29
  batchTimer = null;
@@ -333,10 +334,38 @@ export class ApprovalSystem {
333
334
  getAlwaysKey(toolName, args) {
334
335
  return `${toolName}:${this.getPattern(args)}`;
335
336
  }
337
+ /**
338
+ * Set the main readline interface from index.ts
339
+ * This prevents creating a second readline on the same stdin
340
+ */
341
+ setMainReadline(rl) {
342
+ this.mainRl = rl;
343
+ }
336
344
  readline(prompt) {
337
- if (!this.rl)
338
- this.rl = createInterface({ input: process.stdin, output: process.stdout });
339
- return new Promise((resolve) => { this.rl.question(prompt, (answer) => resolve(answer)); });
345
+ // Pause the main readline to prevent it from also reading stdin input
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();
354
+ }
355
+ 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
+ });
368
+ });
340
369
  }
341
370
  close() {
342
371
  if (this.rl) {
@@ -3,6 +3,7 @@
3
3
  // Shows file changes before applying them
4
4
  // ============================================================
5
5
  import { readFileSync, existsSync } from 'fs';
6
+ import { createInterface } from 'readline';
6
7
  import chalk from 'chalk';
7
8
  export class DiffPreview {
8
9
  /**
@@ -128,7 +129,7 @@ export class DiffPreview {
128
129
  */
129
130
  static async confirmDiff(diff) {
130
131
  DiffPreview.renderDiff(diff);
131
- const rl = require('readline').createInterface({ input: process.stdin, output: process.stdout });
132
+ const rl = createInterface({ input: process.stdin, output: process.stdout });
132
133
  return new Promise((resolve) => {
133
134
  rl.question(chalk.cyan(' Apply these changes? [y/n]: '), (answer) => {
134
135
  rl.close();
@@ -1,5 +1,5 @@
1
1
  // ============================================================
2
- // NeuroCLI - NeuroEngine v4.1.0
2
+ // NeuroCLI - NeuroEngine v4.1.2
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.0',
486
+ currentVersion: '4.1.2',
487
487
  autoCheck: true,
488
488
  autoUpdate: false,
489
489
  });
@@ -6,6 +6,7 @@
6
6
  import { join, resolve, relative, normalize } from 'path';
7
7
  import { existsSync, mkdirSync, readFileSync, writeFileSync, copyFileSync } from 'fs';
8
8
  import chalk from 'chalk';
9
+ import { normalizeCrossPlatformPath, isWindowsAbsolutePath } from '../utils/crosspath.js';
9
10
  export const DEFAULT_SANDBOX_CONFIG = {
10
11
  enabled: false,
11
12
  rootDir: process.cwd(),
@@ -312,11 +313,21 @@ export class Sandbox {
312
313
  }
313
314
  // --- Private Helpers ---
314
315
  resolvePath(filePath) {
316
+ // Expand home directory
317
+ if (filePath.startsWith('~/') || filePath === '~') {
318
+ const homeDir = process.env.HOME || process.env.USERPROFILE || require('os').homedir();
319
+ const expanded = filePath === '~' ? homeDir : join(homeDir, filePath.slice(2));
320
+ return normalize(expanded);
321
+ }
322
+ // Windows absolute path (C:\... or C:/...)
323
+ if (isWindowsAbsolutePath(filePath)) {
324
+ return normalizeCrossPlatformPath(filePath);
325
+ }
326
+ // POSIX absolute path
315
327
  if (filePath.startsWith('/'))
316
328
  return normalize(filePath);
317
- if (filePath.startsWith('~/'))
318
- return normalize(join(require('os').homedir(), filePath.slice(2)));
319
- return normalize(resolve(this.config.rootDir, filePath));
329
+ // Relative path - resolve against rootDir
330
+ return normalize(resolve(this.config.rootDir, normalizeCrossPlatformPath(filePath)));
320
331
  }
321
332
  isUnderRootDir(absPath) {
322
333
  const relativePath = relative(this.config.rootDir, absPath);
package/dist/index.js CHANGED
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  // ============================================================
3
3
  // NeuroCLI - Advanced AI Terminal Coding Assistant
4
- // Main Entry Point - v4.1.0 with auto-updater
4
+ // Main Entry Point - v4.1.2 with cross-platform path fix
5
5
  // ============================================================
6
6
  import { Command } from 'commander';
7
7
  import { createInterface } from 'readline';
@@ -16,7 +16,7 @@ import { HeadlessMode } from './core/headless.js';
16
16
  import { ShellCompletionGenerator } from './core/shell-completion.js';
17
17
  import chalk from 'chalk';
18
18
  import { AutoUpdater } from './core/updater.js';
19
- const VERSION = '4.1.0';
19
+ const VERSION = '4.1.2';
20
20
  // ---- CLI Setup ----
21
21
  const program = new Command();
22
22
  program
@@ -430,14 +430,22 @@ async function startInteractive(options) {
430
430
  completer: completionEngine.complete,
431
431
  });
432
432
  rl.prompt();
433
+ // Wire the main readline to the approval system to prevent dual-readline bug
434
+ // (double character input when approval prompt and main rl both read stdin)
435
+ engine.approval.setMainReadline(rl);
433
436
  let currentMode = 'auto';
434
437
  let currentAgent;
438
+ let isProcessing = false;
435
439
  rl.on('line', async (line) => {
436
440
  const input = line.trim();
437
441
  if (!input) {
438
442
  rl.prompt();
439
443
  return;
440
444
  }
445
+ // Prevent input while processing (approval prompt may be active)
446
+ if (isProcessing) {
447
+ return;
448
+ }
441
449
  // Add to history
442
450
  completionEngine.addHistory(input);
443
451
  // Handle slash commands
@@ -909,7 +917,7 @@ async function startInteractive(options) {
909
917
  }
910
918
  break;
911
919
  case 'doctor':
912
- console.log(chalk.bold('\nNeuroCLI v4.1.0 Health Check:\n'));
920
+ console.log(chalk.bold('\nNeuroCLI v4.1.2 Health Check:\n'));
913
921
  console.log(` API Key: ${config.apiKey ? chalk.green('configured') : chalk.red('MISSING')}`);
914
922
  console.log(` Default Model: ${chalk.cyan(config.defaultModel)} ${MODELS[config.defaultModel] ? chalk.green('valid') : chalk.red('INVALID')}`);
915
923
  console.log(` MCP Servers: ${chalk.cyan(String(engine.mcpClient.listServers().length))}`);
@@ -1112,11 +1120,15 @@ async function startInteractive(options) {
1112
1120
  }
1113
1121
  // Process message with the engine
1114
1122
  try {
1123
+ isProcessing = true;
1115
1124
  await engine.processMessage(input, currentMode, currentAgent);
1116
1125
  }
1117
1126
  catch (error) {
1118
1127
  engine.ui.error(error instanceof Error ? error.message : String(error));
1119
1128
  }
1129
+ finally {
1130
+ isProcessing = false;
1131
+ }
1120
1132
  rl.prompt();
1121
1133
  });
1122
1134
  rl.on('close', () => {
@@ -1128,7 +1140,7 @@ async function startInteractive(options) {
1128
1140
  }
1129
1141
  function printHelp(engine) {
1130
1142
  const t = engine.ui.theme;
1131
- console.log(`\n ${t.bold('NeuroCLI v4.1.0 Commands:')}\n`);
1143
+ console.log(`\n ${t.bold('NeuroCLI v4.1.2 Commands:')}\n`);
1132
1144
  console.log(` ${t.tool('/help')} Show this help message`);
1133
1145
  console.log(` ${t.tool('/model [id]')} Switch or list models`);
1134
1146
  console.log(` ${t.tool('/agent [name]')} Switch or list agents`);
@@ -5,6 +5,7 @@
5
5
  import { readFileSync, writeFileSync, existsSync, mkdirSync, unlinkSync, statSync, readdirSync } from 'fs';
6
6
  import { join, relative, extname } from 'path';
7
7
  import { execSync } from 'child_process';
8
+ import { resolvePath } from '../utils/crosspath.js';
8
9
  const MAX_FILE_SIZE = 1024 * 1024; // 1MB
9
10
  const MAX_OUTPUT_LENGTH = 50000;
10
11
  function truncateOutput(output, maxLength = MAX_OUTPUT_LENGTH) {
@@ -32,7 +33,7 @@ export const readFileTool = {
32
33
  definition: readFileDef,
33
34
  risk: 'low',
34
35
  async execute(args, context) {
35
- const filePath = join(context.workingDirectory, args.path);
36
+ const filePath = resolvePath(context.workingDirectory, args.path);
36
37
  if (!existsSync(filePath))
37
38
  return `Error: File not found: ${args.path}`;
38
39
  const stat = statSync(filePath);
@@ -78,7 +79,7 @@ export const writeFileTool = {
78
79
  };
79
80
  },
80
81
  async execute(args, context) {
81
- const filePath = join(context.workingDirectory, args.path);
82
+ const filePath = resolvePath(context.workingDirectory, args.path);
82
83
  const dir = join(filePath, '..');
83
84
  if (!existsSync(dir)) {
84
85
  mkdirSync(dir, { recursive: true });
@@ -114,7 +115,7 @@ export const editFileTool = {
114
115
  };
115
116
  },
116
117
  async execute(args, context) {
117
- const filePath = join(context.workingDirectory, args.path);
118
+ const filePath = resolvePath(context.workingDirectory, args.path);
118
119
  if (!existsSync(filePath))
119
120
  return `Error: File not found: ${args.path}`;
120
121
  let content = readFileSync(filePath, 'utf-8');
@@ -157,7 +158,7 @@ export const deleteFileTool = {
157
158
  };
158
159
  },
159
160
  async execute(args, context) {
160
- const filePath = join(context.workingDirectory, args.path);
161
+ const filePath = resolvePath(context.workingDirectory, args.path);
161
162
  if (!existsSync(filePath))
162
163
  return `Error: File not found: ${args.path}`;
163
164
  unlinkSync(filePath);
@@ -183,7 +184,7 @@ export const listDirectoryTool = {
183
184
  definition: listDirDef,
184
185
  risk: 'low',
185
186
  async execute(args, context) {
186
- const dirPath = join(context.workingDirectory, args.path || '.');
187
+ const dirPath = resolvePath(context.workingDirectory, args.path || '.');
187
188
  if (!existsSync(dirPath))
188
189
  return `Error: Directory not found: ${args.path}`;
189
190
  const stat = statSync(dirPath);
@@ -259,7 +260,7 @@ export const searchFilesTool = {
259
260
  risk: 'low',
260
261
  async execute(args, context) {
261
262
  const pattern = args.pattern;
262
- const searchPath = join(context.workingDirectory, args.path || '.');
263
+ const searchPath = resolvePath(context.workingDirectory, args.path || '.');
263
264
  const fileType = args.file_type;
264
265
  const maxResults = args.max_results || 50;
265
266
  const caseInsensitive = args.case_insensitive || false;
@@ -332,7 +333,7 @@ export const applyDiffTool = {
332
333
  };
333
334
  },
334
335
  async execute(args, context) {
335
- const filePath = join(context.workingDirectory, args.path);
336
+ const filePath = resolvePath(context.workingDirectory, args.path);
336
337
  if (!existsSync(filePath))
337
338
  return `Error: File not found: ${args.path}`;
338
339
  try {
@@ -0,0 +1,28 @@
1
+ /**
2
+ * Check if a path looks like a Windows absolute path (drive letter)
3
+ * Matches: C:\, C:/, D:\foo, etc.
4
+ */
5
+ export declare function isWindowsAbsolutePath(p: string): boolean;
6
+ /**
7
+ * Normalize a path for cross-platform compatibility:
8
+ * 1. Convert backslashes to forward slashes
9
+ * 2. Detect Windows absolute paths (C:\...) and treat them as absolute
10
+ * 3. Resolve ~ to home directory
11
+ * 4. Handle relative paths correctly
12
+ */
13
+ export declare function normalizeCrossPlatformPath(inputPath: string): string;
14
+ /**
15
+ * Resolve a path relative to a working directory with cross-platform support.
16
+ * This is the main function to use in file tools.
17
+ *
18
+ * - Absolute paths (POSIX or Windows) are used as-is after normalization
19
+ * - Relative paths are resolved against workingDirectory
20
+ * - Home directory (~) is expanded
21
+ * - Backslashes are normalized to forward slashes
22
+ */
23
+ export declare function resolvePath(workingDirectory: string, inputPath: string): string;
24
+ /**
25
+ * Check if a path is absolute on any platform (POSIX or Windows)
26
+ */
27
+ export declare function isAnyAbsolutePath(p: string): boolean;
28
+ //# sourceMappingURL=crosspath.d.ts.map
@@ -0,0 +1,81 @@
1
+ // ============================================================
2
+ // NeuroCLI - Cross-Platform Path Utility
3
+ // Handles Windows, macOS, and Linux path differences
4
+ // ============================================================
5
+ import { join, resolve, isAbsolute, normalize } from 'path';
6
+ /**
7
+ * Check if a path looks like a Windows absolute path (drive letter)
8
+ * Matches: C:\, C:/, D:\foo, etc.
9
+ */
10
+ export function isWindowsAbsolutePath(p) {
11
+ return /^[A-Za-z]:[/\\]/.test(p);
12
+ }
13
+ /**
14
+ * Normalize a path for cross-platform compatibility:
15
+ * 1. Convert backslashes to forward slashes
16
+ * 2. Detect Windows absolute paths (C:\...) and treat them as absolute
17
+ * 3. Resolve ~ to home directory
18
+ * 4. Handle relative paths correctly
19
+ */
20
+ export function normalizeCrossPlatformPath(inputPath) {
21
+ let p = inputPath;
22
+ // Step 1: Expand home directory
23
+ if (p.startsWith('~/') || p === '~') {
24
+ const homeDir = process.env.HOME || process.env.USERPROFILE || '';
25
+ p = p === '~' ? homeDir : join(homeDir, p.slice(2));
26
+ return normalize(p);
27
+ }
28
+ // Step 2: Detect Windows absolute path with drive letter
29
+ if (isWindowsAbsolutePath(p)) {
30
+ // Normalize backslashes to forward slashes, then let Node normalize
31
+ p = p.replace(/\\/g, '/');
32
+ // On Windows, Node's path.resolve handles drive letters natively
33
+ // On non-Windows, we still need to handle this for the case where
34
+ // the LLM sends a Windows path from a user's context
35
+ if (process.platform === 'win32') {
36
+ return normalize(p);
37
+ }
38
+ // On non-Windows systems, a Windows path like C:/Users/foo
39
+ // won't resolve. This likely means the LLM is confused about
40
+ // the OS. We still normalize it so it can be used as-is.
41
+ return normalize(p);
42
+ }
43
+ // Step 3: Convert backslashes to forward slashes for mixed paths
44
+ // (handles cases where LLM or user provides backslash paths on any OS)
45
+ p = p.replace(/\\/g, '/');
46
+ // Step 4: If already absolute (POSIX), normalize directly
47
+ if (isAbsolute(p)) {
48
+ return normalize(p);
49
+ }
50
+ // Step 5: Relative path - return as-is (caller will resolve against working dir)
51
+ return normalize(p);
52
+ }
53
+ /**
54
+ * Resolve a path relative to a working directory with cross-platform support.
55
+ * This is the main function to use in file tools.
56
+ *
57
+ * - Absolute paths (POSIX or Windows) are used as-is after normalization
58
+ * - Relative paths are resolved against workingDirectory
59
+ * - Home directory (~) is expanded
60
+ * - Backslashes are normalized to forward slashes
61
+ */
62
+ export function resolvePath(workingDirectory, inputPath) {
63
+ const normalized = normalizeCrossPlatformPath(inputPath);
64
+ // If it's already an absolute path after normalization, use it directly
65
+ if (isAbsolute(normalized)) {
66
+ return normalized;
67
+ }
68
+ // Windows absolute paths that became normalized (C:/...)
69
+ if (isWindowsAbsolutePath(normalized)) {
70
+ return normalized;
71
+ }
72
+ // Relative path: resolve against working directory
73
+ return resolve(workingDirectory, normalized);
74
+ }
75
+ /**
76
+ * Check if a path is absolute on any platform (POSIX or Windows)
77
+ */
78
+ export function isAnyAbsolutePath(p) {
79
+ return isAbsolute(p) || isWindowsAbsolutePath(p);
80
+ }
81
+ //# sourceMappingURL=crosspath.js.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "neuro-cli",
3
- "version": "4.1.0",
3
+ "version": "4.1.2",
4
4
  "description": "Advanced AI-powered terminal coding assistant with multi-agent orchestration, sub-agent spawning, ACP protocol, OS-level sandboxing, spec-driven development, smart monitoring, multi-model routing, MCP Apps, auto-updater, and 23+ free models",
5
5
  "main": "dist/index.js",
6
6
  "bin": {