@mmmbuto/nexuscli 0.9.3 → 0.9.5

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.
@@ -59,8 +59,8 @@
59
59
 
60
60
  <!-- Prevent Scaling on iOS -->
61
61
  <meta name="format-detection" content="telephone=no" />
62
- <script type="module" crossorigin src="/assets/index-x6Jl2qtq.js"></script>
63
- <link rel="stylesheet" crossorigin href="/assets/index-Ci39i_2l.css">
62
+ <script type="module" crossorigin src="/assets/index-CvOYN8ds.js"></script>
63
+ <link rel="stylesheet" crossorigin href="/assets/index-BAfxoAUN.css">
64
64
  </head>
65
65
  <body>
66
66
  <div id="root"></div>
@@ -1,5 +1,5 @@
1
1
  // NexusCLI Service Worker
2
- const CACHE_VERSION = 'nexuscli-v1766174198487';
2
+ const CACHE_VERSION = 'nexuscli-v1766701518522';
3
3
  const STATIC_CACHE = `${CACHE_VERSION}-static`;
4
4
  const DYNAMIC_CACHE = `${CACHE_VERSION}-dynamic`;
5
5
 
@@ -80,11 +80,21 @@ function getCliTools() {
80
80
  enabled: true,
81
81
  endpoint: '/api/v1/codex',
82
82
  models: [
83
+ {
84
+ id: 'gpt-5.2-codex',
85
+ name: 'gpt-5.2-codex',
86
+ label: 'GPT-5.2 Codex',
87
+ description: '🤖 Latest frontier agentic coding model',
88
+ category: 'codex',
89
+ reasoningEfforts: ['low', 'medium', 'high', 'xhigh'],
90
+ defaultReasoning: 'xhigh',
91
+ default: true
92
+ },
83
93
  {
84
94
  id: 'gpt-5.2',
85
95
  name: 'gpt-5.2',
86
96
  label: 'GPT-5.2',
87
- description: '🧠 Next Gen Reasoning',
97
+ description: '🧠 Latest frontier model',
88
98
  category: 'codex',
89
99
  reasoningEfforts: ['low', 'medium', 'high', 'xhigh'],
90
100
  defaultReasoning: 'xhigh'
@@ -93,11 +103,10 @@ function getCliTools() {
93
103
  id: 'gpt-5.1-codex-max',
94
104
  name: 'gpt-5.1-codex-max',
95
105
  label: 'GPT-5.1 Codex Max',
96
- description: '💎 Extra High reasoning (best)',
106
+ description: '💎 Deep and fast reasoning',
97
107
  category: 'codex',
98
108
  reasoningEfforts: ['low', 'medium', 'high', 'xhigh'],
99
- defaultReasoning: 'xhigh',
100
- default: true
109
+ defaultReasoning: 'xhigh'
101
110
  },
102
111
  {
103
112
  id: 'gpt-5.1-codex',
@@ -0,0 +1,63 @@
1
+ /**
2
+ * Rate Limiting Middleware for Chat Endpoints
3
+ *
4
+ * Protects AI chat endpoints from abuse by limiting requests per user.
5
+ * Uses express-rate-limit with user-based keying via JWT.
6
+ */
7
+
8
+ const rateLimit = require('express-rate-limit');
9
+
10
+ /**
11
+ * Chat endpoints rate limiter
12
+ * - 10 requests per minute per user
13
+ * - Applies to: /api/v1/chat, /api/v1/codex, /api/v1/gemini
14
+ */
15
+ const chatRateLimiter = rateLimit({
16
+ windowMs: 60 * 1000, // 1 minute window
17
+ max: 10, // 10 requests per window
18
+ standardHeaders: true, // Return rate limit info in headers
19
+ legacyHeaders: false,
20
+
21
+ // Key by user ID from JWT (set by authMiddleware)
22
+ keyGenerator: (req) => {
23
+ return req.user?.id || req.ip;
24
+ },
25
+
26
+ // Custom error response
27
+ handler: (req, res) => {
28
+ res.status(429).json({
29
+ error: 'Too many requests',
30
+ message: 'Rate limit exceeded. Please wait before sending more messages.',
31
+ retryAfter: Math.ceil(req.rateLimit.resetTime / 1000)
32
+ });
33
+ },
34
+
35
+ // Skip rate limiting for interrupt endpoints
36
+ skip: (req) => {
37
+ return req.path.endsWith('/interrupt');
38
+ }
39
+ });
40
+
41
+ /**
42
+ * General API rate limiter (for non-chat endpoints)
43
+ * - 60 requests per minute per IP
44
+ */
45
+ const apiRateLimiter = rateLimit({
46
+ windowMs: 60 * 1000,
47
+ max: 60,
48
+ standardHeaders: true,
49
+ legacyHeaders: false,
50
+ keyGenerator: (req) => req.user?.id || req.ip,
51
+ handler: (req, res) => {
52
+ res.status(429).json({
53
+ error: 'Too many requests',
54
+ message: 'API rate limit exceeded.',
55
+ retryAfter: Math.ceil(req.rateLimit.resetTime / 1000)
56
+ });
57
+ }
58
+ });
59
+
60
+ module.exports = {
61
+ chatRateLimiter,
62
+ apiRateLimiter
63
+ };
@@ -12,6 +12,7 @@ const pkg = require('../../package.json');
12
12
 
13
13
  // Import middleware
14
14
  const { authMiddleware } = require('./middleware/auth');
15
+ const { chatRateLimiter } = require('./middleware/rate-limit');
15
16
 
16
17
  // Import routes
17
18
  const authRouter = require('./routes/auth');
@@ -74,9 +75,9 @@ app.use('/api/v1/sessions', authMiddleware, sessionsRouter);
74
75
  app.use('/api/v1/conversations', authMiddleware, conversationsRouter);
75
76
  app.use('/api/v1/conversations', authMiddleware, messagesRouter);
76
77
  app.use('/api/v1/jobs', authMiddleware, jobsRouter);
77
- app.use('/api/v1/chat', authMiddleware, chatRouter);
78
- app.use('/api/v1/codex', authMiddleware, codexRouter);
79
- app.use('/api/v1/gemini', authMiddleware, geminiRouter);
78
+ app.use('/api/v1/chat', authMiddleware, chatRateLimiter, chatRouter);
79
+ app.use('/api/v1/codex', authMiddleware, chatRateLimiter, codexRouter);
80
+ app.use('/api/v1/gemini', authMiddleware, chatRateLimiter, geminiRouter);
80
81
  app.use('/api/v1/upload', authMiddleware, uploadRouter); // File upload
81
82
 
82
83
  // STT routes
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mmmbuto/nexuscli",
3
- "version": "0.9.3",
3
+ "version": "0.9.5",
4
4
  "description": "NexusCLI - TRI CLI Control Plane (Claude/Codex/Gemini)",
5
5
  "main": "lib/server/server.js",
6
6
  "bin": {
@@ -1,164 +0,0 @@
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
- }
14
-
15
- /**
16
- * Execute command via PTY
17
- * @param {Object} params - { jobId, tool, command, workingDir, timeout, onStatus }
18
- * @returns {Promise<Object>} - { exitCode, stdout, stderr, duration }
19
- */
20
- async execute({ jobId, tool = 'bash', command, workingDir, timeout = 30000, onStatus }) {
21
- return new Promise((resolve, reject) => {
22
- const parser = new OutputParser();
23
- const startTime = Date.now();
24
-
25
- // Determine shell command
26
- let shellCmd = '/bin/bash';
27
- let args = ['-c', command];
28
-
29
- // Tool-specific handling
30
- if (tool === 'bash') {
31
- shellCmd = '/bin/bash';
32
- args = ['-c', command];
33
- } else if (tool === 'python') {
34
- shellCmd = '/usr/bin/python3';
35
- args = ['-c', command];
36
- } else if (tool === 'node') {
37
- shellCmd = '/usr/bin/node';
38
- args = ['-e', command];
39
- } else {
40
- // Generic: assume tool is in PATH
41
- shellCmd = tool;
42
- args = command.split(' ');
43
- }
44
-
45
- console.log(`[CliWrapper] Executing: ${tool} - ${command.substring(0, 50)}...`);
46
- console.log(`[CliWrapper] Working dir: ${workingDir || this.workspaceDir}`);
47
-
48
- // Spawn PTY process
49
- let ptyProcess;
50
- try {
51
- ptyProcess = pty.spawn(shellCmd, args, {
52
- name: 'xterm-color',
53
- cols: 80,
54
- rows: 30,
55
- cwd: workingDir || this.workspaceDir,
56
- env: process.env,
57
- });
58
- } catch (err) {
59
- return reject(err);
60
- }
61
-
62
- this.activeJobs.set(jobId, ptyProcess);
63
-
64
- let stdout = '';
65
- let stderr = '';
66
-
67
- // Handle timeout
68
- const timer = setTimeout(() => {
69
- console.log(`[CliWrapper] Job ${jobId} timeout`);
70
- ptyProcess.kill('SIGTERM');
71
-
72
- if (onStatus) {
73
- onStatus({
74
- type: 'error',
75
- error: 'Command timeout exceeded',
76
- timeout
77
- });
78
- }
79
- }, timeout);
80
-
81
- // Handle process errors
82
- if (typeof ptyProcess.on === 'function') {
83
- ptyProcess.on('error', (err) => {
84
- console.error('[CliWrapper] Spawn error:', err);
85
- reject(err);
86
- });
87
- } else if (typeof ptyProcess.onError === 'function') {
88
- ptyProcess.onError((err) => {
89
- console.error('[CliWrapper] Spawn error:', err);
90
- reject(err);
91
- });
92
- }
93
-
94
- // Handle output
95
- ptyProcess.onData((data) => {
96
- stdout += data;
97
-
98
- // Parse output and emit status events
99
- if (onStatus) {
100
- try {
101
- const events = parser.parse(data);
102
- events.forEach(event => {
103
- onStatus(event);
104
- });
105
- } catch (parseError) {
106
- console.error('[CliWrapper] Parser error:', parseError);
107
- }
108
- }
109
- });
110
-
111
- // Handle exit
112
- ptyProcess.onExit(({ exitCode }) => {
113
- clearTimeout(timer);
114
- this.activeJobs.delete(jobId);
115
-
116
- const duration = Date.now() - startTime;
117
-
118
- // Clean ANSI escape codes
119
- const cleanStdout = stdout
120
- .replace(/\x1B\[[0-9;]*[a-zA-Z]/g, '') // ANSI codes
121
- .replace(/\x1B\[\?[0-9;]*[a-zA-Z]/g, '') // Private modes
122
- .trim();
123
-
124
- console.log(`[CliWrapper] Job ${jobId} exit: ${exitCode} (${duration}ms)`);
125
-
126
- if (exitCode !== 0) {
127
- // Extract stderr (if any)
128
- stderr = cleanStdout; // In PTY mode, stderr is mixed with stdout
129
- }
130
-
131
- resolve({
132
- exitCode,
133
- stdout: cleanStdout,
134
- stderr,
135
- duration
136
- });
137
- });
138
- });
139
- }
140
-
141
- /**
142
- * Kill running job
143
- * @param {string} jobId - Job ID
144
- */
145
- kill(jobId) {
146
- const ptyProcess = this.activeJobs.get(jobId);
147
- if (ptyProcess) {
148
- ptyProcess.kill('SIGTERM');
149
- this.activeJobs.delete(jobId);
150
- console.log(`[CliWrapper] Killed job ${jobId}`);
151
- return true;
152
- }
153
- return false;
154
- }
155
-
156
- /**
157
- * Get active job count
158
- */
159
- getActiveJobCount() {
160
- return this.activeJobs.size;
161
- }
162
- }
163
-
164
- module.exports = CliWrapper;
@@ -1,132 +0,0 @@
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;
@@ -1,81 +0,0 @@
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 };