@mmmbuto/nexuscli 0.9.5 → 0.9.6

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/README.md CHANGED
@@ -27,7 +27,13 @@ NexusCLI is a lightweight, Termux-first AI cockpit to orchestrate Claude Code, C
27
27
 
28
28
  ---
29
29
 
30
- ## Highlights (v0.9.5)
30
+ ## Highlights (v0.9.6)
31
+
32
+ - **Jobs Runner Restored**: `jobs` route works again after cleanup regression
33
+ - **Termux-Safe Execution**: no hardcoded `/bin` or `/usr/bin` paths for job tools
34
+ - **Cleaner Errors**: job SSE errors now display correctly in UI
35
+
36
+ ### v0.9.5
31
37
 
32
38
  - **GPT-5.2 Codex**: Added latest frontier agentic coding model as default
33
39
  - Updated Codex model catalog to match latest OpenAI CLI
@@ -57,7 +63,7 @@ NexusCLI is a lightweight, Termux-first AI cockpit to orchestrate Claude Code, C
57
63
  |--------|--------|----------|
58
64
  | **Claude (native)** | Opus 4.5, Sonnet 4.5, Haiku 4.5 | Anthropic |
59
65
  | **Claude-compatible** | DeepSeek (deepseek-*), GLM-4.6 | DeepSeek, Z.ai |
60
- | **Codex** | GPT-5.1, GPT-5.1 Codex (Mini/Max) | OpenAI |
66
+ | **Codex** | GPT-5.2 Codex, GPT-5.2, GPT-5.1 Codex (Mini/Max), GPT-5.1 | OpenAI |
61
67
  | **Gemini** | Gemini 3 Pro Preview, Gemini 3 Flash Preview | Google |
62
68
 
63
69
  ---
@@ -0,0 +1,167 @@
1
+ const pty = require('./pty-adapter');
2
+ const OutputParser = require('./output-parser');
3
+
4
+ /**
5
+ * CLI Wrapper - Generic CLI tool execution (adapted from NexusChat claude-wrapper)
6
+ * Spawns PTY processes for any CLI tool (bash, git, docker, python, etc.)
7
+ * Streams output via SSE
8
+ */
9
+ class CliWrapper {
10
+ constructor(options = {}) {
11
+ this.workspaceDir = options.workspaceDir || process.cwd();
12
+ this.activeJobs = new Map();
13
+ this.defaultShell = process.env.SHELL || 'bash';
14
+ this.defaultPython = 'python3';
15
+ this.defaultNode = process.execPath || 'node';
16
+ }
17
+
18
+ /**
19
+ * Execute command via PTY
20
+ * @param {Object} params - { jobId, tool, command, workingDir, timeout, onStatus }
21
+ * @returns {Promise<Object>} - { exitCode, stdout, stderr, duration }
22
+ */
23
+ async execute({ jobId, tool = 'bash', command, workingDir, timeout = 30000, onStatus }) {
24
+ return new Promise((resolve, reject) => {
25
+ const parser = new OutputParser();
26
+ const startTime = Date.now();
27
+
28
+ // Determine shell command
29
+ let shellCmd = this.defaultShell;
30
+ let args = ['-c', command];
31
+
32
+ // Tool-specific handling
33
+ if (tool === 'bash') {
34
+ shellCmd = this.defaultShell;
35
+ args = ['-c', command];
36
+ } else if (tool === 'python') {
37
+ shellCmd = this.defaultPython;
38
+ args = ['-c', command];
39
+ } else if (tool === 'node') {
40
+ shellCmd = this.defaultNode;
41
+ args = ['-e', command];
42
+ } else {
43
+ // Generic: assume tool is in PATH
44
+ shellCmd = tool;
45
+ args = command.split(' ');
46
+ }
47
+
48
+ console.log(`[CliWrapper] Executing: ${tool} - ${command.substring(0, 50)}...`);
49
+ console.log(`[CliWrapper] Working dir: ${workingDir || this.workspaceDir}`);
50
+
51
+ // Spawn PTY process
52
+ let ptyProcess;
53
+ try {
54
+ ptyProcess = pty.spawn(shellCmd, args, {
55
+ name: 'xterm-color',
56
+ cols: 80,
57
+ rows: 30,
58
+ cwd: workingDir || this.workspaceDir,
59
+ env: process.env,
60
+ });
61
+ } catch (err) {
62
+ return reject(err);
63
+ }
64
+
65
+ this.activeJobs.set(jobId, ptyProcess);
66
+
67
+ let stdout = '';
68
+ let stderr = '';
69
+
70
+ // Handle timeout
71
+ const timer = setTimeout(() => {
72
+ console.log(`[CliWrapper] Job ${jobId} timeout`);
73
+ ptyProcess.kill('SIGTERM');
74
+
75
+ if (onStatus) {
76
+ onStatus({
77
+ type: 'error',
78
+ error: 'Command timeout exceeded',
79
+ timeout
80
+ });
81
+ }
82
+ }, timeout);
83
+
84
+ // Handle process errors
85
+ if (typeof ptyProcess.on === 'function') {
86
+ ptyProcess.on('error', (err) => {
87
+ console.error('[CliWrapper] Spawn error:', err);
88
+ reject(err);
89
+ });
90
+ } else if (typeof ptyProcess.onError === 'function') {
91
+ ptyProcess.onError((err) => {
92
+ console.error('[CliWrapper] Spawn error:', err);
93
+ reject(err);
94
+ });
95
+ }
96
+
97
+ // Handle output
98
+ ptyProcess.onData((data) => {
99
+ stdout += data;
100
+
101
+ // Parse output and emit status events
102
+ if (onStatus) {
103
+ try {
104
+ const events = parser.parse(data);
105
+ events.forEach(event => {
106
+ onStatus(event);
107
+ });
108
+ } catch (parseError) {
109
+ console.error('[CliWrapper] Parser error:', parseError);
110
+ }
111
+ }
112
+ });
113
+
114
+ // Handle exit
115
+ ptyProcess.onExit(({ exitCode }) => {
116
+ clearTimeout(timer);
117
+ this.activeJobs.delete(jobId);
118
+
119
+ const duration = Date.now() - startTime;
120
+
121
+ // Clean ANSI escape codes
122
+ const cleanStdout = stdout
123
+ .replace(/\x1B\[[0-9;]*[a-zA-Z]/g, '') // ANSI codes
124
+ .replace(/\x1B\[\?[0-9;]*[a-zA-Z]/g, '') // Private modes
125
+ .trim();
126
+
127
+ console.log(`[CliWrapper] Job ${jobId} exit: ${exitCode} (${duration}ms)`);
128
+
129
+ if (exitCode !== 0) {
130
+ // Extract stderr (if any)
131
+ stderr = cleanStdout; // In PTY mode, stderr is mixed with stdout
132
+ }
133
+
134
+ resolve({
135
+ exitCode,
136
+ stdout: cleanStdout,
137
+ stderr,
138
+ duration
139
+ });
140
+ });
141
+ });
142
+ }
143
+
144
+ /**
145
+ * Kill running job
146
+ * @param {string} jobId - Job ID
147
+ */
148
+ kill(jobId) {
149
+ const ptyProcess = this.activeJobs.get(jobId);
150
+ if (ptyProcess) {
151
+ ptyProcess.kill('SIGTERM');
152
+ this.activeJobs.delete(jobId);
153
+ console.log(`[CliWrapper] Killed job ${jobId}`);
154
+ return true;
155
+ }
156
+ return false;
157
+ }
158
+
159
+ /**
160
+ * Get active job count
161
+ */
162
+ getActiveJobCount() {
163
+ return this.activeJobs.size;
164
+ }
165
+ }
166
+
167
+ module.exports = CliWrapper;
@@ -0,0 +1,132 @@
1
+ /**
2
+ * OutputParser - Parse CLI stdout/stderr into structured events
3
+ * Adapted from NexusChat output-parser.js
4
+ */
5
+ class OutputParser {
6
+ constructor() {
7
+ this.state = 'idle';
8
+ this.buffer = '';
9
+ this.lineBuffer = '';
10
+ }
11
+
12
+ /**
13
+ * Regex patterns for detecting CLI output markers
14
+ */
15
+ static PATTERNS = {
16
+ // Tool execution patterns (generic)
17
+ toolExecution: /(?:Running|Executing)\s+(\w+)(?:\s+(?:command|tool))?:\s*(.+)/i,
18
+
19
+ // Common CLI patterns
20
+ errorPattern: /(?:Error|ERROR|Failed|FAILED|Exception):\s*(.+)/i,
21
+ warningPattern: /(?:Warning|WARNING|warn):\s*(.+)/i,
22
+
23
+ // ANSI escape codes
24
+ ansiCodes: /\x1B\[[0-9;]*[a-zA-Z]/g,
25
+ ansiPrivate: /\x1B\[\?[0-9;]*[a-zA-Z]/g,
26
+ };
27
+
28
+ /**
29
+ * Parse chunk of stdout and return array of events
30
+ * @param {string} chunk - Raw stdout chunk from PTY
31
+ * @returns {Array} Array of event objects
32
+ */
33
+ parse(chunk) {
34
+ const events = [];
35
+
36
+ // Add to buffer
37
+ this.buffer += chunk;
38
+ this.lineBuffer += chunk;
39
+
40
+ // Process line by line
41
+ const lines = this.lineBuffer.split('\n');
42
+ this.lineBuffer = lines.pop(); // Keep incomplete line
43
+
44
+ for (const line of lines) {
45
+ const lineEvents = this.parseLine(line);
46
+ events.push(...lineEvents);
47
+ }
48
+
49
+ // Also emit raw output chunks
50
+ const cleanChunk = this.cleanAnsi(chunk);
51
+ if (cleanChunk && cleanChunk.trim()) {
52
+ events.push({
53
+ type: 'output_chunk',
54
+ stream: 'stdout',
55
+ text: cleanChunk,
56
+ isIncremental: true,
57
+ });
58
+ }
59
+
60
+ return events;
61
+ }
62
+
63
+ /**
64
+ * Parse single line
65
+ * @param {string} line - Single line of output
66
+ * @returns {Array} Events from this line
67
+ */
68
+ parseLine(line) {
69
+ const events = [];
70
+ const cleanLine = this.cleanAnsi(line);
71
+
72
+ // Check for tool execution
73
+ const toolMatch = cleanLine.match(OutputParser.PATTERNS.toolExecution);
74
+ if (toolMatch) {
75
+ const [, tool, command] = toolMatch;
76
+ events.push({
77
+ type: 'status',
78
+ category: 'tool',
79
+ tool,
80
+ message: `${tool}: ${command.substring(0, 60)}${command.length > 60 ? '...' : ''}`,
81
+ icon: '🔧',
82
+ timestamp: new Date().toISOString(),
83
+ });
84
+ }
85
+
86
+ // Check for errors
87
+ if (OutputParser.PATTERNS.errorPattern.test(cleanLine)) {
88
+ const [, errorMsg] = cleanLine.match(OutputParser.PATTERNS.errorPattern);
89
+ events.push({
90
+ type: 'status',
91
+ category: 'warning',
92
+ message: `Error: ${errorMsg}`,
93
+ icon: '⚠️',
94
+ timestamp: new Date().toISOString(),
95
+ });
96
+ }
97
+
98
+ // Check for warnings
99
+ if (OutputParser.PATTERNS.warningPattern.test(cleanLine)) {
100
+ const [, warnMsg] = cleanLine.match(OutputParser.PATTERNS.warningPattern);
101
+ events.push({
102
+ type: 'status',
103
+ category: 'warning',
104
+ message: `Warning: ${warnMsg}`,
105
+ icon: '⚠️',
106
+ timestamp: new Date().toISOString(),
107
+ });
108
+ }
109
+
110
+ return events;
111
+ }
112
+
113
+ /**
114
+ * Clean ANSI escape codes from text
115
+ */
116
+ cleanAnsi(text) {
117
+ return text
118
+ .replace(OutputParser.PATTERNS.ansiCodes, '')
119
+ .replace(OutputParser.PATTERNS.ansiPrivate, '');
120
+ }
121
+
122
+ /**
123
+ * Reset parser state
124
+ */
125
+ reset() {
126
+ this.state = 'idle';
127
+ this.buffer = '';
128
+ this.lineBuffer = '';
129
+ }
130
+ }
131
+
132
+ module.exports = OutputParser;
@@ -0,0 +1,81 @@
1
+ /**
2
+ * PTY Adapter for NexusCLI (Termux)
3
+ * Provides node-pty-like interface using child_process.spawn
4
+ * Termux-only: no native node-pty compilation needed
5
+ *
6
+ * @version 0.5.0 - Added stdin support for interrupt (ESC key)
7
+ */
8
+
9
+ const { spawn: cpSpawn } = require('child_process');
10
+
11
+ /**
12
+ * Spawn a process with node-pty-like interface
13
+ * @param {string} command - Command to spawn
14
+ * @param {string[]} args - Arguments
15
+ * @param {Object} options - Spawn options (cwd, env)
16
+ * @returns {Object} PTY-like interface with onData, onExit, kill
17
+ */
18
+ function spawn(command, args, options = {}) {
19
+ const proc = cpSpawn(command, args, {
20
+ cwd: options.cwd,
21
+ env: options.env,
22
+ shell: false,
23
+ stdio: ['pipe', 'pipe', 'pipe'], // stdin enabled for interrupt support
24
+ });
25
+
26
+ const dataHandlers = [];
27
+ const exitHandlers = [];
28
+ const errorHandlers = [];
29
+
30
+ proc.stdout.on('data', (buf) => {
31
+ const data = buf.toString();
32
+ console.log('[PTY-Adapter] stdout:', data.substring(0, 200));
33
+ dataHandlers.forEach((fn) => fn(data));
34
+ });
35
+
36
+ proc.stderr.on('data', (buf) => {
37
+ const data = buf.toString();
38
+ console.log('[PTY-Adapter] stderr:', data.substring(0, 200));
39
+ dataHandlers.forEach((fn) => fn(data));
40
+ });
41
+
42
+ proc.on('close', (code) => {
43
+ exitHandlers.forEach((fn) => fn({ exitCode: code ?? 0 }));
44
+ });
45
+
46
+ proc.on('error', (err) => {
47
+ console.error('[PTY-Adapter] Error:', err.message);
48
+ errorHandlers.forEach((fn) => fn(err));
49
+ });
50
+
51
+ return {
52
+ onData: (fn) => dataHandlers.push(fn),
53
+ onExit: (fn) => exitHandlers.push(fn),
54
+ onError: (fn) => errorHandlers.push(fn),
55
+ write: (data) => proc.stdin?.writable && proc.stdin.write(data),
56
+ /**
57
+ * Send ESC key (0x1B) to interrupt CLI
58
+ * Used to gracefully stop Claude/Gemini CLI execution
59
+ * @returns {boolean} true if ESC was sent successfully
60
+ */
61
+ sendEsc: () => {
62
+ if (proc.stdin?.writable) {
63
+ proc.stdin.write('\x1B');
64
+ return true;
65
+ }
66
+ return false;
67
+ },
68
+ kill: (signal = 'SIGTERM') => proc.kill(signal),
69
+ pid: proc.pid,
70
+ };
71
+ }
72
+
73
+ /**
74
+ * Check if native PTY is available
75
+ * Always returns false on Termux (we use spawn adapter)
76
+ */
77
+ function isPtyAvailable() {
78
+ return false;
79
+ }
80
+
81
+ module.exports = { spawn, isPtyAvailable };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mmmbuto/nexuscli",
3
- "version": "0.9.5",
3
+ "version": "0.9.6",
4
4
  "description": "NexusCLI - TRI CLI Control Plane (Claude/Codex/Gemini)",
5
5
  "main": "lib/server/server.js",
6
6
  "bin": {