@mmmbuto/nexuscli 0.8.1 → 0.8.3

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
@@ -28,7 +28,7 @@ NexusCLI is a lightweight AI cockpit (Termux-first) to orchestrate Claude Code,
28
28
 
29
29
  ---
30
30
 
31
- ## Highlights (v0.8.0)
31
+ ## Highlights (v0.8.3)
32
32
 
33
33
  - Multi-engine: Claude, Codex, Gemini
34
34
  - Native resume: same engine resumes the session; switching engines uses handoff with summary/history
@@ -36,6 +36,7 @@ NexusCLI is a lightweight AI cockpit (Termux-first) to orchestrate Claude Code,
36
36
  - Session import: on startup it imports native sessions from ~/.claude ~/.codex ~/.gemini; manual endpoint `POST /api/v1/sessions/import`
37
37
  - Voice input (Whisper), auto HTTPS for remote microphone
38
38
  - Mobile-first UI with SSE streaming and explicit workspace selection
39
+ - Termux: postinstall installs `ripgrep`; Claude wrapper auto-patches missing `vendor/ripgrep/arm64-android/rg`; Codex parser exposes threadId to prevent crash on exit
39
40
 
40
41
  ## Supported Engines
41
42
 
@@ -2,6 +2,7 @@ const fs = require('fs');
2
2
  const path = require('path');
3
3
  const pty = require('../lib/pty-adapter');
4
4
  const OutputParser = require('./output-parser');
5
+ const BaseCliWrapper = require('./base-cli-wrapper');
5
6
  const { getApiKey } = require('../db');
6
7
 
7
8
  /**
@@ -18,11 +19,15 @@ const { getApiKey } = require('../db');
18
19
  * - Parses stdout for tool use, thinking, file ops
19
20
  * - Emits status events → SSE stream
20
21
  * - Returns final response text → saved in DB
22
+ *
23
+ * @version 0.5.0 - Extended BaseCliWrapper for interrupt support
21
24
  */
22
- class ClaudeWrapper {
25
+ class ClaudeWrapper extends BaseCliWrapper {
23
26
  constructor(options = {}) {
27
+ super(); // Initialize activeProcesses from BaseCliWrapper
24
28
  this.claudePath = this.resolveClaudePath(options.claudePath);
25
29
  this.workspaceDir = options.workspaceDir || process.env.NEXUSCLI_WORKSPACE || process.cwd();
30
+ this.rgPatched = false;
26
31
  }
27
32
 
28
33
  isExistingSession(sessionId, workspacePath) {
@@ -75,6 +80,60 @@ class ClaudeWrapper {
75
80
  return null;
76
81
  }
77
82
 
83
+ /**
84
+ * Termux fix: Claude Code bundles ripgrep only for arm64-linux, but on Android
85
+ * it looks for vendor/ripgrep/arm64-android/rg. Create a symlink on the fly so
86
+ * fresh installs work without manual steps.
87
+ */
88
+ ensureRipgrepForTermux() {
89
+ if (this.rgPatched) return;
90
+ const isTermux = (process.env.PREFIX || '').includes('/com.termux/');
91
+ if (!isTermux || !this.claudePath) return;
92
+
93
+ try {
94
+ // claudePath points to cli.js or wrapper binary; vendor lives alongside
95
+ const cliDir = path.dirname(this.claudePath);
96
+ const vendorDir = path.join(cliDir, 'vendor', 'ripgrep');
97
+ const linuxRg = path.join(vendorDir, 'arm64-linux', 'rg');
98
+ const androidDir = path.join(vendorDir, 'arm64-android');
99
+ const androidRg = path.join(androidDir, 'rg');
100
+
101
+ if (!fs.existsSync(vendorDir)) return;
102
+
103
+ // Prefer bundled arm64-linux rg; fallback to system rg if bundle missing
104
+ let rgSource = null;
105
+ if (fs.existsSync(linuxRg)) {
106
+ rgSource = linuxRg;
107
+ } else {
108
+ // Try system rg (Termux package ripgrep)
109
+ try {
110
+ const whichRg = require('child_process')
111
+ .execSync('which rg', { encoding: 'utf8' })
112
+ .trim();
113
+ if (whichRg && fs.existsSync(whichRg)) {
114
+ rgSource = whichRg;
115
+ console.log('[ClaudeWrapper] Using system ripgrep for Termux:', whichRg);
116
+ }
117
+ } catch (_) {
118
+ // leave rgSource null; we’ll exit gracefully
119
+ }
120
+ }
121
+
122
+ if (!rgSource) return;
123
+ if (fs.existsSync(androidRg)) {
124
+ this.rgPatched = true;
125
+ return;
126
+ }
127
+
128
+ fs.mkdirSync(androidDir, { recursive: true });
129
+ fs.symlinkSync(rgSource, androidRg);
130
+ console.log('[ClaudeWrapper] Created ripgrep symlink for Termux:', androidRg, '→', rgSource);
131
+ this.rgPatched = true;
132
+ } catch (err) {
133
+ console.warn('[ClaudeWrapper] Failed to patch ripgrep for Termux:', err.message);
134
+ }
135
+ }
136
+
78
137
  /**
79
138
  * Send message to Claude Code CLI
80
139
  *
@@ -125,6 +184,9 @@ class ClaudeWrapper {
125
184
  // Use provided workspace or fallback to default
126
185
  const cwd = workspacePath || this.workspaceDir;
127
186
 
187
+ // Termux compatibility: make sure ripgrep path exists before spawn
188
+ this.ensureRipgrepForTermux();
189
+
128
190
  // Build environment - detect DeepSeek models and configure API accordingly
129
191
  const spawnEnv = { ...process.env };
130
192
  const isDeepSeek = model.startsWith('deepseek-');
@@ -200,8 +262,28 @@ class ClaudeWrapper {
200
262
  return reject(new Error(msg));
201
263
  }
202
264
 
265
+ // Register process for interrupt capability
266
+ this.registerProcess(conversationId, ptyProcess, 'pty');
267
+
203
268
  let stdout = '';
204
269
 
270
+ // Timeout after 10 minutes (same as Codex wrapper)
271
+ const timeout = setTimeout(() => {
272
+ console.error('[ClaudeWrapper] Timeout after 10 minutes');
273
+ if (onStatus) {
274
+ onStatus({ type: 'error', category: 'timeout', message: 'Claude CLI timeout after 10 minutes' });
275
+ }
276
+ try {
277
+ ptyProcess.kill();
278
+ } catch (e) {
279
+ console.error('[ClaudeWrapper] Failed to kill timed-out process:', e.message);
280
+ }
281
+ if (!promiseSettled) {
282
+ promiseSettled = true;
283
+ reject(new Error('Claude CLI timeout after 10 minutes'));
284
+ }
285
+ }, 600000);
286
+
205
287
  // Process output chunks
206
288
  ptyProcess.onData((data) => {
207
289
  stdout += data;
@@ -255,6 +337,12 @@ class ClaudeWrapper {
255
337
  }
256
338
 
257
339
  ptyProcess.onExit(({ exitCode }) => {
340
+ // Clear timeout on exit
341
+ clearTimeout(timeout);
342
+
343
+ // Unregister process on exit
344
+ this.unregisterProcess(conversationId);
345
+
258
346
  console.log(`[ClaudeWrapper] Exit code: ${exitCode}`);
259
347
 
260
348
  if (exitCode !== 0) {
@@ -263,6 +263,13 @@ class CodexOutputParser {
263
263
  return this.usage;
264
264
  }
265
265
 
266
+ /**
267
+ * Get thread ID (native Codex session ID)
268
+ */
269
+ getThreadId() {
270
+ return this.threadId;
271
+ }
272
+
266
273
  /**
267
274
  * Reset parser state for new request
268
275
  */
@@ -270,6 +277,7 @@ class CodexOutputParser {
270
277
  this.buffer = '';
271
278
  this.finalResponse = '';
272
279
  this.usage = null;
280
+ this.threadId = null;
273
281
  this.pendingCommands.clear();
274
282
  }
275
283
  }
@@ -137,7 +137,8 @@ async function main() {
137
137
  console.log('');
138
138
 
139
139
  // Required packages
140
- const packages = ['termux-api', 'termux-tools'];
140
+ // - ripgrep is needed by Claude CLI (vendor lookup expects it on Termux)
141
+ const packages = ['termux-api', 'termux-tools', 'ripgrep'];
141
142
 
142
143
  log(colors.cyan('Installing Termux packages:'));
143
144
  for (const pkg of packages) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mmmbuto/nexuscli",
3
- "version": "0.8.1",
3
+ "version": "0.8.3",
4
4
  "description": "NexusCLI - TRI CLI Control Plane (Claude/Codex/Gemini)",
5
5
  "main": "lib/server/server.js",
6
6
  "bin": {