@mmmbuto/nexuscrew 0.1.0-beta.1 → 0.2.1

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 (49) hide show
  1. package/README.md +67 -120
  2. package/bin/nexuscrew.js +468 -264
  3. package/frontend/dist/apple-touch-icon.png +0 -0
  4. package/frontend/dist/assets/{index-C7bAndew.js → index-BG1N7bSL.js} +1710 -1702
  5. package/frontend/dist/assets/index-CiAtinNP.css +1 -0
  6. package/frontend/dist/favicon.svg +20 -0
  7. package/frontend/dist/icon-192.png +0 -0
  8. package/frontend/dist/icon-512.png +0 -0
  9. package/frontend/dist/index.html +11 -3
  10. package/frontend/dist/site.webmanifest +29 -0
  11. package/lib/config/hosts.js +67 -0
  12. package/lib/config/manager.js +379 -0
  13. package/lib/config/models.js +408 -0
  14. package/lib/server/db/adapter.js +274 -0
  15. package/lib/server/db/drivers/sql-js.js +75 -0
  16. package/lib/server/db/migrate.js +174 -0
  17. package/lib/server/db/migrations/001_base_schema.sql +70 -0
  18. package/lib/server/middleware/auth.js +134 -0
  19. package/lib/server/middleware/rate-limit.js +63 -0
  20. package/lib/server/models/User.js +128 -0
  21. package/lib/server/routes/auth.js +168 -0
  22. package/lib/server/routes/hosts.js +11 -20
  23. package/lib/server/routes/keys.js +28 -0
  24. package/lib/server/routes/models.js +59 -4
  25. package/lib/server/routes/runtimes.js +34 -0
  26. package/lib/server/routes/send.js +76 -12
  27. package/lib/server/routes/sessions.js +39 -10
  28. package/lib/server/routes/speech.js +46 -0
  29. package/lib/server/routes/status.js +8 -6
  30. package/lib/server/routes/upload.js +135 -0
  31. package/lib/server/routes/wake-lock.js +95 -0
  32. package/lib/server/routes/workspaces.js +101 -0
  33. package/lib/server/server.js +66 -33
  34. package/lib/server/services/attachment-manager.js +57 -0
  35. package/lib/server/services/context-bridge.js +425 -0
  36. package/lib/server/services/runtime-manager.js +462 -0
  37. package/lib/server/services/speech-manager.js +76 -0
  38. package/lib/server/services/summary-generator.js +309 -0
  39. package/lib/server/services/workspace-manager.js +79 -0
  40. package/lib/services/engine-discovery.js +198 -13
  41. package/lib/services/log-watcher.js +40 -22
  42. package/lib/services/remote-pane-watcher.js +155 -0
  43. package/lib/services/session-store.js +60 -64
  44. package/lib/services/tmux-manager.js +127 -116
  45. package/lib/setup/postinstall.js +38 -0
  46. package/lib/utils/paths.js +124 -0
  47. package/lib/utils/termux.js +182 -0
  48. package/package.json +8 -4
  49. package/frontend/dist/assets/index-OENqI1_9.css +0 -1
@@ -0,0 +1,462 @@
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;
@@ -0,0 +1,76 @@
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;