@mmmbuto/nexuscrew 0.2.1 → 0.2.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.
Files changed (52) hide show
  1. package/README.md +122 -67
  2. package/bin/nexuscrew.js +194 -470
  3. package/frontend/dist/assets/{index-BG1N7bSL.js → index-BsldfYnr.js} +1704 -1711
  4. package/frontend/dist/assets/index-DcQGg3dp.css +1 -0
  5. package/frontend/dist/index.html +3 -11
  6. package/lib/server/routes/hosts.js +22 -12
  7. package/lib/server/routes/launchers.js +12 -0
  8. package/lib/server/routes/models.js +16 -63
  9. package/lib/server/routes/runtime.js +10 -0
  10. package/lib/server/routes/send.js +97 -258
  11. package/lib/server/routes/sessions.js +13 -42
  12. package/lib/server/routes/status.js +18 -22
  13. package/lib/server/routes/tmux.js +88 -0
  14. package/lib/server/server.js +60 -81
  15. package/lib/services/engine-discovery.js +192 -445
  16. package/lib/services/log-watcher.js +22 -40
  17. package/lib/services/session-store.js +88 -62
  18. package/lib/services/tmux-manager.js +111 -173
  19. package/package.json +5 -9
  20. package/frontend/dist/apple-touch-icon.png +0 -0
  21. package/frontend/dist/assets/index-CiAtinNP.css +0 -1
  22. package/frontend/dist/favicon.svg +0 -20
  23. package/frontend/dist/icon-192.png +0 -0
  24. package/frontend/dist/icon-512.png +0 -0
  25. package/frontend/dist/site.webmanifest +0 -29
  26. package/lib/config/hosts.js +0 -67
  27. package/lib/config/manager.js +0 -379
  28. package/lib/config/models.js +0 -408
  29. package/lib/server/db/adapter.js +0 -274
  30. package/lib/server/db/drivers/sql-js.js +0 -75
  31. package/lib/server/db/migrate.js +0 -174
  32. package/lib/server/db/migrations/001_base_schema.sql +0 -70
  33. package/lib/server/middleware/auth.js +0 -134
  34. package/lib/server/middleware/rate-limit.js +0 -63
  35. package/lib/server/models/User.js +0 -128
  36. package/lib/server/routes/auth.js +0 -168
  37. package/lib/server/routes/keys.js +0 -28
  38. package/lib/server/routes/runtimes.js +0 -34
  39. package/lib/server/routes/speech.js +0 -46
  40. package/lib/server/routes/upload.js +0 -135
  41. package/lib/server/routes/wake-lock.js +0 -95
  42. package/lib/server/routes/workspaces.js +0 -101
  43. package/lib/server/services/attachment-manager.js +0 -57
  44. package/lib/server/services/context-bridge.js +0 -425
  45. package/lib/server/services/runtime-manager.js +0 -462
  46. package/lib/server/services/speech-manager.js +0 -76
  47. package/lib/server/services/summary-generator.js +0 -309
  48. package/lib/server/services/workspace-manager.js +0 -79
  49. package/lib/services/remote-pane-watcher.js +0 -155
  50. package/lib/setup/postinstall.js +0 -38
  51. package/lib/utils/paths.js +0 -124
  52. package/lib/utils/termux.js +0 -182
@@ -1,462 +0,0 @@
1
- const { execFile } = require('child_process');
2
- const { promisify } = require('util');
3
- const { getCliTools, getModelById, getDefaultModelId } = require('../../config/models');
4
-
5
- const execFileAsync = promisify(execFile);
6
-
7
- function isTermux() {
8
- return (
9
- process.env.PREFIX?.includes('com.termux') ||
10
- process.env.TERMUX_VERSION !== undefined
11
- );
12
- }
13
-
14
- function getPlatformId() {
15
- if (isTermux()) return 'termux';
16
- if (process.platform === 'darwin') return 'macos';
17
- if (process.platform === 'linux') return 'linux';
18
- return process.platform;
19
- }
20
-
21
- function npmCommand() {
22
- if (isTermux()) return 'npm';
23
- return 'npm';
24
- }
25
-
26
- function shellJoin(commands) {
27
- return commands.filter(Boolean).join(' && ');
28
- }
29
-
30
- function buildProviderAuth({ providerId, dbKey, envVars = [], displayName, helpUrl, assignOpenAiKey = false }) {
31
- return {
32
- providerId,
33
- dbKey,
34
- envVars,
35
- displayName,
36
- helpUrl,
37
- assignOpenAiKey,
38
- };
39
- }
40
-
41
- function resolveClaudeCustomProfile(model) {
42
- switch (model.id) {
43
- case 'deepseek-reasoner':
44
- case 'deepseek-chat':
45
- return {
46
- env: {
47
- ANTHROPIC_BASE_URL: 'https://api.deepseek.com/anthropic',
48
- ANTHROPIC_MODEL: model.id,
49
- ANTHROPIC_SMALL_FAST_MODEL: 'deepseek-chat',
50
- API_TIMEOUT_MS: '900000',
51
- },
52
- providerAuth: buildProviderAuth({
53
- providerId: 'deepseek',
54
- dbKey: 'deepseek',
55
- envVars: ['DEEPSEEK_API_KEY'],
56
- displayName: 'DeepSeek',
57
- helpUrl: 'https://platform.deepseek.com/api_keys',
58
- }),
59
- };
60
- case 'glm-4.7':
61
- case 'glm-5':
62
- return {
63
- env: {
64
- ANTHROPIC_BASE_URL: 'https://api.z.ai/api/anthropic',
65
- ANTHROPIC_MODEL: model.id === 'glm-5' ? 'GLM-5' : 'GLM-4.7',
66
- ANTHROPIC_SMALL_FAST_MODEL: 'GLM-4.5-Air',
67
- API_TIMEOUT_MS: '3000000',
68
- },
69
- providerAuth: buildProviderAuth({
70
- providerId: 'zai',
71
- dbKey: 'zai',
72
- envVars: ['ZAI_API_KEY', 'ZAI_API_KEY_A', 'ZAI_API_KEY_P'],
73
- displayName: 'Z.ai',
74
- helpUrl: 'https://z.ai',
75
- }),
76
- };
77
- case 'qwen3.5-plus':
78
- case 'qwen3-max-2026-01-23':
79
- case 'kimi-k2.5':
80
- return {
81
- env: {
82
- ANTHROPIC_BASE_URL: 'https://coding-intl.dashscope.aliyuncs.com/apps/anthropic',
83
- ANTHROPIC_MODEL: model.id,
84
- ANTHROPIC_SMALL_FAST_MODEL: 'qwen3.5-plus',
85
- API_TIMEOUT_MS: '3000000',
86
- },
87
- providerAuth: buildProviderAuth({
88
- providerId: 'alibaba',
89
- dbKey: 'alibaba',
90
- envVars: ['ALIBABA_CODE_API_KEY'],
91
- displayName: 'Alibaba Code',
92
- helpUrl: 'https://dashscope.aliyun.com',
93
- }),
94
- };
95
- case 'MiniMax-M2.7':
96
- return {
97
- env: {
98
- ANTHROPIC_BASE_URL: 'https://api.minimax.io/anthropic',
99
- ANTHROPIC_MODEL: 'MiniMax-M2.7',
100
- ANTHROPIC_SMALL_FAST_MODEL: 'MiniMax-M2.7',
101
- API_TIMEOUT_MS: '120000',
102
- },
103
- providerAuth: buildProviderAuth({
104
- providerId: 'minimax',
105
- dbKey: 'minimax',
106
- envVars: ['MINIMAX_API_KEY'],
107
- displayName: 'MiniMax',
108
- helpUrl: 'https://api.minimax.io',
109
- }),
110
- };
111
- default:
112
- return { env: {}, providerAuth: null };
113
- }
114
- }
115
-
116
- function resolveCodexCustomProfile(model) {
117
- if (model.providerId === 'alibaba') {
118
- const providerName = 'alibaba-code';
119
- return {
120
- env: {},
121
- configOverrides: [
122
- `model="${model.id}"`,
123
- `model_provider="${providerName}"`,
124
- `model_providers.${providerName}.name="Alibaba-Code"`,
125
- `model_providers.${providerName}.base_url="https://coding-intl.dashscope.aliyuncs.com/v1"`,
126
- `model_providers.${providerName}.env_key="ALIBABA_CODE_API_KEY"`,
127
- `model_providers.${providerName}.wire_api="chat"`,
128
- ],
129
- providerAuth: buildProviderAuth({
130
- providerId: 'alibaba',
131
- dbKey: 'alibaba',
132
- envVars: ['ALIBABA_CODE_API_KEY'],
133
- displayName: 'Alibaba Code',
134
- helpUrl: 'https://dashscope.aliyun.com',
135
- assignOpenAiKey: true,
136
- }),
137
- };
138
- }
139
-
140
- if (model.providerId === 'zai') {
141
- const providerName = 'zai';
142
- return {
143
- env: {},
144
- configOverrides: [
145
- `model="${model.id}"`,
146
- `model_provider="${providerName}"`,
147
- `model_providers.${providerName}.name="ZAI"`,
148
- `model_providers.${providerName}.base_url="https://api.z.ai/api/coding/paas/v4"`,
149
- `model_providers.${providerName}.env_key="ZAI_API_KEY"`,
150
- `model_providers.${providerName}.wire_api="chat"`,
151
- ],
152
- providerAuth: buildProviderAuth({
153
- providerId: 'zai',
154
- dbKey: 'zai',
155
- envVars: ['ZAI_API_KEY', 'ZAI_API_KEY_A', 'ZAI_API_KEY_P'],
156
- displayName: 'Z.ai',
157
- helpUrl: 'https://z.ai',
158
- assignOpenAiKey: true,
159
- }),
160
- };
161
- }
162
-
163
- if (model.providerId === 'chutes') {
164
- const providerName = 'chutes';
165
- return {
166
- env: {},
167
- configOverrides: [
168
- `model="${model.id}"`,
169
- `model_provider="${providerName}"`,
170
- `model_providers.${providerName}.name="Chutes"`,
171
- `model_providers.${providerName}.base_url="https://llm.chutes.ai/v1"`,
172
- `model_providers.${providerName}.env_key="CHUTES_API_KEY"`,
173
- `model_providers.${providerName}.wire_api="chat"`,
174
- ],
175
- providerAuth: buildProviderAuth({
176
- providerId: 'chutes',
177
- dbKey: 'chutes',
178
- envVars: ['CHUTES_API_KEY'],
179
- displayName: 'Chutes',
180
- helpUrl: 'https://chutes.ai',
181
- assignOpenAiKey: true,
182
- }),
183
- };
184
- }
185
-
186
- return { env: {}, configOverrides: [], providerAuth: null };
187
- }
188
-
189
- function resolveCustomRuntimeProfile(model) {
190
- if (!model?.custom) {
191
- return { env: {}, configOverrides: [], providerAuth: null };
192
- }
193
-
194
- if (model.engine === 'claude') {
195
- return {
196
- configOverrides: [],
197
- ...resolveClaudeCustomProfile(model),
198
- };
199
- }
200
-
201
- if (model.engine === 'codex') {
202
- return resolveCodexCustomProfile(model);
203
- }
204
-
205
- return { env: {}, configOverrides: [], providerAuth: null };
206
- }
207
-
208
- function getRuntimeDefaults(platformId) {
209
- return {
210
- claude: {
211
- native: {
212
- runtimeId: 'claude-native',
213
- command: 'claude',
214
- source: 'npm',
215
- installCommand: `${npmCommand()} install -g @anthropic-ai/claude-code@latest`,
216
- updateCommand: `${npmCommand()} update -g @anthropic-ai/claude-code`,
217
- checkCommand: 'claude --version',
218
- sharedBinary: true,
219
- },
220
- custom: {
221
- runtimeId: 'claude-custom',
222
- command: 'claude',
223
- source: 'shared-cli',
224
- installCommand: `${npmCommand()} install -g @anthropic-ai/claude-code@latest`,
225
- updateCommand: `${npmCommand()} update -g @anthropic-ai/claude-code`,
226
- checkCommand: 'claude --version',
227
- sharedBinary: true,
228
- },
229
- },
230
- codex: {
231
- native: {
232
- runtimeId: 'codex-native',
233
- command: 'codex',
234
- source: 'npm',
235
- installCommand: `${npmCommand()} install -g @openai/codex@latest`,
236
- updateCommand: `${npmCommand()} update -g @openai/codex`,
237
- checkCommand: 'codex --version',
238
- },
239
- custom: {
240
- runtimeId: 'codex-custom',
241
- command: 'codex-lts',
242
- source: platformId === 'termux' ? 'npm' : 'manual',
243
- installCommand: platformId === 'termux'
244
- ? `${npmCommand()} install -g @mmmbuto/codex-cli-termux@0.80.6-lts`
245
- : null,
246
- updateCommand: platformId === 'termux'
247
- ? `${npmCommand()} update -g @mmmbuto/codex-cli-termux`
248
- : null,
249
- checkCommand: 'codex-lts --version',
250
- },
251
- },
252
- gemini: {
253
- native: {
254
- runtimeId: 'gemini-native',
255
- command: 'gemini',
256
- source: 'npm',
257
- installCommand: `${npmCommand()} install -g @google/gemini-cli@latest`,
258
- updateCommand: `${npmCommand()} update -g @google/gemini-cli`,
259
- checkCommand: 'gemini --version',
260
- },
261
- custom: {
262
- runtimeId: 'gemini-custom',
263
- command: 'gemini',
264
- source: 'npm',
265
- installCommand: `${npmCommand()} install -g @google/gemini-cli@latest`,
266
- updateCommand: `${npmCommand()} update -g @google/gemini-cli`,
267
- checkCommand: 'gemini --version',
268
- },
269
- },
270
- qwen: {
271
- native: {
272
- runtimeId: 'qwen-native',
273
- command: 'qwen',
274
- source: platformId === 'termux' ? 'npm' : 'npm',
275
- installCommand: `${npmCommand()} install -g @qwen-code/qwen-code@latest`,
276
- updateCommand: `${npmCommand()} update -g @qwen-code/qwen-code`,
277
- checkCommand: 'qwen --version',
278
- },
279
- custom: {
280
- runtimeId: 'qwen-custom',
281
- command: 'qwen',
282
- source: platformId === 'termux' ? 'npm' : 'npm',
283
- installCommand: `${npmCommand()} install -g @qwen-code/qwen-code@latest`,
284
- updateCommand: `${npmCommand()} update -g @qwen-code/qwen-code`,
285
- checkCommand: 'qwen --version',
286
- },
287
- },
288
- };
289
- }
290
-
291
- class RuntimeManager {
292
- constructor() {
293
- this.platformId = getPlatformId();
294
- }
295
-
296
- getToolCatalog() {
297
- return getCliTools();
298
- }
299
-
300
- getRuntimeDefinitions() {
301
- const defaults = getRuntimeDefaults(this.platformId);
302
- const engines = this.getToolCatalog();
303
-
304
- return Object.fromEntries(
305
- Object.entries(engines).map(([engineId, engine]) => {
306
- const lanes = Object.fromEntries(
307
- Object.entries(engine.lanes || {}).map(([laneId, lane]) => {
308
- const defaultLane = defaults[engineId]?.[laneId] || {};
309
- const command = defaultLane.command;
310
- const runtimeId = lane.runtimeId || defaultLane.runtimeId;
311
- const enabled = true;
312
-
313
- return [laneId, {
314
- engine: engineId,
315
- lane: laneId,
316
- laneLabel: lane.label || laneId,
317
- runtimeId,
318
- command,
319
- enabled,
320
- source: defaultLane.source || 'manual',
321
- installCommand: defaultLane.installCommand || null,
322
- updateCommand: defaultLane.updateCommand || null,
323
- checkCommand: defaultLane.checkCommand || `${command} --version`,
324
- sharedBinary: defaultLane.sharedBinary ?? false,
325
- }];
326
- })
327
- );
328
-
329
- return [engineId, lanes];
330
- })
331
- );
332
- }
333
-
334
- async probeCommand(command) {
335
- if (!command) {
336
- return { available: false, version: null, error: 'command not configured' };
337
- }
338
-
339
- try {
340
- const { stdout, stderr } = await execFileAsync(command, ['--version'], {
341
- timeout: 10000,
342
- env: process.env,
343
- });
344
- return {
345
- available: true,
346
- version: (stdout || stderr || '').trim() || 'unknown',
347
- error: null,
348
- };
349
- } catch (error) {
350
- return {
351
- available: false,
352
- version: null,
353
- error: error.message,
354
- };
355
- }
356
- }
357
-
358
- async getRuntimeInventory() {
359
- const definitions = this.getRuntimeDefinitions();
360
- const tools = this.getToolCatalog();
361
- const inventory = [];
362
-
363
- for (const [engineId, lanes] of Object.entries(definitions)) {
364
- for (const [laneId, runtime] of Object.entries(lanes)) {
365
- const probe = await this.probeCommand(runtime.command);
366
- const laneModels = tools[engineId]?.models?.filter((model) => model.lane === laneId) || [];
367
- inventory.push({
368
- ...runtime,
369
- platform: this.platformId,
370
- status: probe.available ? 'available' : 'missing',
371
- installedVersion: probe.version,
372
- latestVersion: 'upstream',
373
- available: probe.available,
374
- error: probe.error,
375
- models: laneModels.map((model) => ({
376
- id: model.id,
377
- label: model.label,
378
- providerId: model.providerId,
379
- })),
380
- actions: [
381
- runtime.installCommand ? 'install' : null,
382
- runtime.updateCommand ? 'update' : null,
383
- 'check',
384
- ].filter(Boolean),
385
- });
386
- }
387
- }
388
-
389
- return inventory;
390
- }
391
-
392
- async getRuntimeInventoryMap() {
393
- const inventory = await this.getRuntimeInventory();
394
- return Object.fromEntries(inventory.map((item) => [item.runtimeId, item]));
395
- }
396
-
397
- async getRuntimeAwareCliTools() {
398
- const tools = this.getToolCatalog();
399
- const inventoryMap = await this.getRuntimeInventoryMap();
400
-
401
- return Object.fromEntries(Object.entries(tools).map(([engineId, engine]) => {
402
- const models = (engine.models || []).map((model) => {
403
- const runtime = inventoryMap[model.runtimeId];
404
- return {
405
- ...model,
406
- availability: runtime?.status || 'unknown',
407
- runtimeStatus: runtime?.status || 'unknown',
408
- runtimeCommand: runtime?.command || null,
409
- runtimeSource: runtime?.source || null,
410
- available: runtime?.available ?? false,
411
- };
412
- });
413
-
414
- return [engineId, {
415
- ...engine,
416
- models,
417
- }];
418
- }));
419
- }
420
-
421
- resolveModel(modelId) {
422
- return getModelById(modelId) || getModelById(getDefaultModelId());
423
- }
424
-
425
- resolveRuntimeSelection({ engine, lane, runtimeId, modelId }) {
426
- const model = this.resolveModel(modelId);
427
- const resolvedEngine = engine || model?.engine;
428
- const resolvedLane = lane || model?.lane || 'native';
429
- const definitions = this.getRuntimeDefinitions();
430
- const runtime = runtimeId
431
- ? Object.values(definitions[resolvedEngine] || {}).find((entry) => entry.runtimeId === runtimeId)
432
- : definitions[resolvedEngine]?.[resolvedLane];
433
- const customProfile = resolveCustomRuntimeProfile(model);
434
-
435
- return {
436
- engine: resolvedEngine,
437
- lane: resolvedLane,
438
- runtimeId: runtime?.runtimeId || model?.runtimeId || null,
439
- command: runtime?.command || null,
440
- env: customProfile.env || {},
441
- configOverrides: customProfile.configOverrides || [],
442
- providerAuth: customProfile.providerAuth || null,
443
- model,
444
- runtime,
445
- };
446
- }
447
-
448
- resolveAction(runtimeId, action) {
449
- const definitions = this.getRuntimeDefinitions();
450
- for (const lanes of Object.values(definitions)) {
451
- for (const runtime of Object.values(lanes)) {
452
- if (runtime.runtimeId !== runtimeId) continue;
453
- if (action === 'install') return runtime.installCommand;
454
- if (action === 'update') return runtime.updateCommand;
455
- if (action === 'check') return runtime.checkCommand;
456
- }
457
- }
458
- return null;
459
- }
460
- }
461
-
462
- module.exports = RuntimeManager;
@@ -1,76 +0,0 @@
1
- const fs = require('fs');
2
- const path = require('path');
3
- const { execFile } = require('child_process');
4
- const { promisify } = require('util');
5
- const { getConfig } = require('../../config/manager');
6
- const { PATHS } = require('../../utils/paths');
7
-
8
- const execFileAsync = promisify(execFile);
9
-
10
- function resolveBundledWhisper() {
11
- const candidates = [
12
- path.join(PATHS.BIN_DIR, 'whisper-cli'),
13
- path.join(__dirname, '../../../bin/android-arm64/whisper-cli'),
14
- path.join(__dirname, '../../../bin/linux-x64/whisper-cli')
15
- ];
16
- return candidates.find((candidate) => fs.existsSync(candidate)) || 'whisper-cli';
17
- }
18
-
19
- class SpeechManager {
20
- getStatus() {
21
- const config = getConfig();
22
- const whisperBin = config.stt?.whisper?.bin || resolveBundledWhisper();
23
- const whisperModel = config.stt?.whisper?.model || '';
24
-
25
- return {
26
- provider: config.stt?.provider || 'whispercpp',
27
- fallback: config.stt?.fallback || 'browser',
28
- whisper: {
29
- bin: whisperBin,
30
- model: whisperModel,
31
- configured: Boolean(whisperModel),
32
- available: this.checkBinary(whisperBin)
33
- },
34
- tts: {
35
- provider: config.tts?.provider || 'system'
36
- }
37
- };
38
- }
39
-
40
- checkBinary(binPath) {
41
- if (!binPath) return false;
42
- if (binPath === 'whisper-cli') return true;
43
- return fs.existsSync(binPath);
44
- }
45
-
46
- async transcribe({ audioPath, language }) {
47
- const config = getConfig();
48
- const bin = config.stt?.whisper?.bin || resolveBundledWhisper();
49
- const model = config.stt?.whisper?.model;
50
- const threads = String(config.stt?.whisper?.threads || 2);
51
- const lang = language || config.stt?.whisper?.language || 'it';
52
-
53
- if (!model) {
54
- throw new Error('whisper.cpp model not configured');
55
- }
56
-
57
- const args = ['-m', model, '-f', audioPath, '-l', lang, '-t', threads, '--no-timestamps'];
58
- const { stdout, stderr } = await execFileAsync(bin, args, {
59
- timeout: 120000,
60
- maxBuffer: 8 * 1024 * 1024
61
- });
62
-
63
- const text = [stdout, stderr]
64
- .filter(Boolean)
65
- .join('\n')
66
- .split('\n')
67
- .map((line) => line.trim())
68
- .filter((line) => line && !line.startsWith('['))
69
- .join(' ')
70
- .trim();
71
-
72
- return { text };
73
- }
74
- }
75
-
76
- module.exports = SpeechManager;