@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.
- package/README.md +67 -120
- package/bin/nexuscrew.js +468 -264
- package/frontend/dist/apple-touch-icon.png +0 -0
- package/frontend/dist/assets/{index-C7bAndew.js → index-BG1N7bSL.js} +1710 -1702
- package/frontend/dist/assets/index-CiAtinNP.css +1 -0
- package/frontend/dist/favicon.svg +20 -0
- package/frontend/dist/icon-192.png +0 -0
- package/frontend/dist/icon-512.png +0 -0
- package/frontend/dist/index.html +11 -3
- package/frontend/dist/site.webmanifest +29 -0
- package/lib/config/hosts.js +67 -0
- package/lib/config/manager.js +379 -0
- package/lib/config/models.js +408 -0
- package/lib/server/db/adapter.js +274 -0
- package/lib/server/db/drivers/sql-js.js +75 -0
- package/lib/server/db/migrate.js +174 -0
- package/lib/server/db/migrations/001_base_schema.sql +70 -0
- package/lib/server/middleware/auth.js +134 -0
- package/lib/server/middleware/rate-limit.js +63 -0
- package/lib/server/models/User.js +128 -0
- package/lib/server/routes/auth.js +168 -0
- package/lib/server/routes/hosts.js +11 -20
- package/lib/server/routes/keys.js +28 -0
- package/lib/server/routes/models.js +59 -4
- package/lib/server/routes/runtimes.js +34 -0
- package/lib/server/routes/send.js +76 -12
- package/lib/server/routes/sessions.js +39 -10
- package/lib/server/routes/speech.js +46 -0
- package/lib/server/routes/status.js +8 -6
- package/lib/server/routes/upload.js +135 -0
- package/lib/server/routes/wake-lock.js +95 -0
- package/lib/server/routes/workspaces.js +101 -0
- package/lib/server/server.js +66 -33
- package/lib/server/services/attachment-manager.js +57 -0
- package/lib/server/services/context-bridge.js +425 -0
- package/lib/server/services/runtime-manager.js +462 -0
- package/lib/server/services/speech-manager.js +76 -0
- package/lib/server/services/summary-generator.js +309 -0
- package/lib/server/services/workspace-manager.js +79 -0
- package/lib/services/engine-discovery.js +198 -13
- package/lib/services/log-watcher.js +40 -22
- package/lib/services/remote-pane-watcher.js +155 -0
- package/lib/services/session-store.js +60 -64
- package/lib/services/tmux-manager.js +127 -116
- package/lib/setup/postinstall.js +38 -0
- package/lib/utils/paths.js +124 -0
- package/lib/utils/termux.js +182 -0
- package/package.json +8 -4
- package/frontend/dist/assets/index-OENqI1_9.css +0 -1
|
@@ -0,0 +1,408 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared model catalog for NexusCrew.
|
|
3
|
+
* Unified endpoint: all engines use /api/v1/send
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
const UNIFIED_ENDPOINT = '/api/v1/send';
|
|
7
|
+
|
|
8
|
+
const CATALOG = {
|
|
9
|
+
claude: {
|
|
10
|
+
id: 'claude',
|
|
11
|
+
name: 'Claude Code',
|
|
12
|
+
icon: 'Terminal',
|
|
13
|
+
endpoint: UNIFIED_ENDPOINT,
|
|
14
|
+
thinkModes: ['think', 'no-think'],
|
|
15
|
+
defaultThinkMode: 'think',
|
|
16
|
+
lanes: {
|
|
17
|
+
native: {
|
|
18
|
+
runtimeId: 'claude-native',
|
|
19
|
+
label: 'Native',
|
|
20
|
+
models: [
|
|
21
|
+
{
|
|
22
|
+
id: 'sonnet',
|
|
23
|
+
name: 'sonnet',
|
|
24
|
+
label: 'Claude Sonnet 4.6',
|
|
25
|
+
description: 'Latest balanced Claude coding model',
|
|
26
|
+
providerId: 'anthropic',
|
|
27
|
+
default: true,
|
|
28
|
+
},
|
|
29
|
+
{
|
|
30
|
+
id: 'opus',
|
|
31
|
+
name: 'opus',
|
|
32
|
+
label: 'Claude Opus 4.6',
|
|
33
|
+
description: 'Highest capability Claude model',
|
|
34
|
+
providerId: 'anthropic',
|
|
35
|
+
},
|
|
36
|
+
{
|
|
37
|
+
id: 'haiku',
|
|
38
|
+
name: 'haiku',
|
|
39
|
+
label: 'Claude Haiku 4.5',
|
|
40
|
+
description: 'Fast Claude lane',
|
|
41
|
+
providerId: 'anthropic',
|
|
42
|
+
},
|
|
43
|
+
],
|
|
44
|
+
},
|
|
45
|
+
custom: {
|
|
46
|
+
runtimeId: 'claude-custom',
|
|
47
|
+
label: 'Custom',
|
|
48
|
+
models: [
|
|
49
|
+
{
|
|
50
|
+
id: 'deepseek-reasoner',
|
|
51
|
+
name: 'deepseek-reasoner',
|
|
52
|
+
label: 'DeepSeek Reasoner',
|
|
53
|
+
description: 'Claude CLI via DeepSeek Anthropic-compatible API',
|
|
54
|
+
providerId: 'deepseek',
|
|
55
|
+
},
|
|
56
|
+
{
|
|
57
|
+
id: 'deepseek-chat',
|
|
58
|
+
name: 'deepseek-chat',
|
|
59
|
+
label: 'DeepSeek Chat',
|
|
60
|
+
description: 'Fast DeepSeek chat over Claude CLI',
|
|
61
|
+
providerId: 'deepseek',
|
|
62
|
+
},
|
|
63
|
+
{
|
|
64
|
+
id: 'glm-4.7',
|
|
65
|
+
name: 'glm-4.7',
|
|
66
|
+
label: 'GLM 4.7',
|
|
67
|
+
description: 'Z.ai Anthropic-compatible model',
|
|
68
|
+
providerId: 'zai',
|
|
69
|
+
},
|
|
70
|
+
{
|
|
71
|
+
id: 'glm-5',
|
|
72
|
+
name: 'glm-5',
|
|
73
|
+
label: 'GLM 5',
|
|
74
|
+
description: 'Z.ai latest custom runtime',
|
|
75
|
+
providerId: 'zai',
|
|
76
|
+
},
|
|
77
|
+
{
|
|
78
|
+
id: 'qwen3.5-plus',
|
|
79
|
+
name: 'qwen3.5-plus',
|
|
80
|
+
label: 'Qwen 3.5 Plus',
|
|
81
|
+
description: 'Alibaba custom provider over Claude CLI',
|
|
82
|
+
providerId: 'alibaba',
|
|
83
|
+
},
|
|
84
|
+
{
|
|
85
|
+
id: 'qwen3-max-2026-01-23',
|
|
86
|
+
name: 'qwen3-max-2026-01-23',
|
|
87
|
+
label: 'Qwen 3 Max',
|
|
88
|
+
description: 'Alibaba custom provider over Claude CLI',
|
|
89
|
+
providerId: 'alibaba',
|
|
90
|
+
},
|
|
91
|
+
{
|
|
92
|
+
id: 'kimi-k2.5',
|
|
93
|
+
name: 'kimi-k2.5',
|
|
94
|
+
label: 'Kimi K2.5',
|
|
95
|
+
description: 'Moonshot-compatible custom lane',
|
|
96
|
+
providerId: 'alibaba',
|
|
97
|
+
},
|
|
98
|
+
{
|
|
99
|
+
id: 'MiniMax-M2.7',
|
|
100
|
+
name: 'MiniMax-M2.7',
|
|
101
|
+
label: 'MiniMax M2.7',
|
|
102
|
+
description: 'MiniMax custom lane over Claude CLI',
|
|
103
|
+
providerId: 'minimax',
|
|
104
|
+
},
|
|
105
|
+
],
|
|
106
|
+
},
|
|
107
|
+
},
|
|
108
|
+
},
|
|
109
|
+
|
|
110
|
+
codex: {
|
|
111
|
+
id: 'codex',
|
|
112
|
+
name: 'Codex',
|
|
113
|
+
icon: 'Code2',
|
|
114
|
+
endpoint: UNIFIED_ENDPOINT,
|
|
115
|
+
lanes: {
|
|
116
|
+
native: {
|
|
117
|
+
runtimeId: 'codex-native',
|
|
118
|
+
label: 'Native',
|
|
119
|
+
models: [
|
|
120
|
+
{
|
|
121
|
+
id: 'gpt-5.4',
|
|
122
|
+
name: 'gpt-5.4',
|
|
123
|
+
label: 'GPT-5.4',
|
|
124
|
+
description: 'Latest flagship reasoning model',
|
|
125
|
+
providerId: 'openai',
|
|
126
|
+
reasoningEfforts: ['low', 'medium', 'high', 'xhigh'],
|
|
127
|
+
defaultReasoning: 'high',
|
|
128
|
+
},
|
|
129
|
+
{
|
|
130
|
+
id: 'gpt-5.3-codex',
|
|
131
|
+
name: 'gpt-5.3-codex',
|
|
132
|
+
label: 'GPT-5.3 Codex',
|
|
133
|
+
description: 'Current codex-specialized model',
|
|
134
|
+
providerId: 'openai',
|
|
135
|
+
reasoningEfforts: ['low', 'medium', 'high', 'xhigh'],
|
|
136
|
+
defaultReasoning: 'high',
|
|
137
|
+
default: true,
|
|
138
|
+
},
|
|
139
|
+
{
|
|
140
|
+
id: 'gpt-5.2-codex',
|
|
141
|
+
name: 'gpt-5.2-codex',
|
|
142
|
+
label: 'GPT-5.2 Codex',
|
|
143
|
+
description: 'Stable codex model',
|
|
144
|
+
providerId: 'openai',
|
|
145
|
+
reasoningEfforts: ['low', 'medium', 'high', 'xhigh'],
|
|
146
|
+
defaultReasoning: 'high',
|
|
147
|
+
},
|
|
148
|
+
{
|
|
149
|
+
id: 'gpt-5.1-codex-max',
|
|
150
|
+
name: 'gpt-5.1-codex-max',
|
|
151
|
+
label: 'GPT-5.1 Codex Max',
|
|
152
|
+
description: 'Deep codex reasoning',
|
|
153
|
+
providerId: 'openai',
|
|
154
|
+
reasoningEfforts: ['low', 'medium', 'high', 'xhigh'],
|
|
155
|
+
defaultReasoning: 'high',
|
|
156
|
+
},
|
|
157
|
+
{
|
|
158
|
+
id: 'codex-mini-latest',
|
|
159
|
+
name: 'codex-mini-latest',
|
|
160
|
+
label: 'Codex Mini Latest',
|
|
161
|
+
description: 'Fast latest codex lane',
|
|
162
|
+
providerId: 'openai',
|
|
163
|
+
reasoningEfforts: ['medium', 'high'],
|
|
164
|
+
defaultReasoning: 'medium',
|
|
165
|
+
},
|
|
166
|
+
],
|
|
167
|
+
},
|
|
168
|
+
custom: {
|
|
169
|
+
runtimeId: 'codex-custom',
|
|
170
|
+
label: 'Custom',
|
|
171
|
+
models: [
|
|
172
|
+
{
|
|
173
|
+
id: 'qwen3-coder-plus',
|
|
174
|
+
name: 'qwen3-coder-plus',
|
|
175
|
+
label: 'Qwen 3 Coder Plus',
|
|
176
|
+
description: 'Custom coding provider via codex-lts',
|
|
177
|
+
providerId: 'alibaba',
|
|
178
|
+
reasoningEfforts: ['medium', 'high'],
|
|
179
|
+
defaultReasoning: 'high',
|
|
180
|
+
},
|
|
181
|
+
{
|
|
182
|
+
id: 'qwen3-coder-next',
|
|
183
|
+
name: 'qwen3-coder-next',
|
|
184
|
+
label: 'Qwen 3 Coder Next',
|
|
185
|
+
description: 'Custom coding provider via codex-lts',
|
|
186
|
+
providerId: 'alibaba',
|
|
187
|
+
reasoningEfforts: ['medium', 'high'],
|
|
188
|
+
defaultReasoning: 'high',
|
|
189
|
+
},
|
|
190
|
+
{
|
|
191
|
+
id: 'qwen3.5-plus',
|
|
192
|
+
name: 'qwen3.5-plus',
|
|
193
|
+
label: 'Qwen 3.5 Plus',
|
|
194
|
+
description: 'Custom provider lane via codex-lts',
|
|
195
|
+
providerId: 'alibaba',
|
|
196
|
+
reasoningEfforts: ['medium', 'high'],
|
|
197
|
+
defaultReasoning: 'high',
|
|
198
|
+
},
|
|
199
|
+
{
|
|
200
|
+
id: 'glm-5',
|
|
201
|
+
name: 'glm-5',
|
|
202
|
+
label: 'GLM 5',
|
|
203
|
+
description: 'Z.ai custom lane via codex-lts',
|
|
204
|
+
providerId: 'zai',
|
|
205
|
+
reasoningEfforts: ['medium', 'high'],
|
|
206
|
+
defaultReasoning: 'high',
|
|
207
|
+
},
|
|
208
|
+
{
|
|
209
|
+
id: 'deepseek-ai/DeepSeek-V3.2-TEE',
|
|
210
|
+
name: 'deepseek-ai/DeepSeek-V3.2-TEE',
|
|
211
|
+
label: 'DeepSeek V3.2 TEE',
|
|
212
|
+
description: 'Chutes custom runtime via codex-lts',
|
|
213
|
+
providerId: 'chutes',
|
|
214
|
+
reasoningEfforts: ['medium', 'high'],
|
|
215
|
+
defaultReasoning: 'high',
|
|
216
|
+
},
|
|
217
|
+
],
|
|
218
|
+
},
|
|
219
|
+
},
|
|
220
|
+
},
|
|
221
|
+
|
|
222
|
+
gemini: {
|
|
223
|
+
id: 'gemini',
|
|
224
|
+
name: 'Gemini',
|
|
225
|
+
icon: 'Sparkles',
|
|
226
|
+
endpoint: UNIFIED_ENDPOINT,
|
|
227
|
+
lanes: {
|
|
228
|
+
native: {
|
|
229
|
+
runtimeId: 'gemini-native',
|
|
230
|
+
label: 'Native',
|
|
231
|
+
models: [
|
|
232
|
+
{
|
|
233
|
+
id: 'gemini-3-pro-preview',
|
|
234
|
+
name: 'gemini-3-pro-preview',
|
|
235
|
+
label: 'Gemini 3 Pro',
|
|
236
|
+
description: 'Current Gemini 3 pro line',
|
|
237
|
+
providerId: 'google',
|
|
238
|
+
default: true,
|
|
239
|
+
},
|
|
240
|
+
{
|
|
241
|
+
id: 'gemini-3-flash-preview',
|
|
242
|
+
name: 'gemini-3-flash-preview',
|
|
243
|
+
label: 'Gemini 3 Flash',
|
|
244
|
+
description: 'Current Gemini 3 flash line',
|
|
245
|
+
providerId: 'google',
|
|
246
|
+
},
|
|
247
|
+
{
|
|
248
|
+
id: 'gemini-2.5-pro',
|
|
249
|
+
name: 'gemini-2.5-pro',
|
|
250
|
+
label: 'Gemini 2.5 Pro',
|
|
251
|
+
description: 'Stable Gemini 2.5 pro',
|
|
252
|
+
providerId: 'google',
|
|
253
|
+
},
|
|
254
|
+
{
|
|
255
|
+
id: 'gemini-2.5-flash',
|
|
256
|
+
name: 'gemini-2.5-flash',
|
|
257
|
+
label: 'Gemini 2.5 Flash',
|
|
258
|
+
description: 'Stable Gemini 2.5 flash',
|
|
259
|
+
providerId: 'google',
|
|
260
|
+
},
|
|
261
|
+
],
|
|
262
|
+
},
|
|
263
|
+
custom: {
|
|
264
|
+
runtimeId: 'gemini-custom',
|
|
265
|
+
label: 'Custom',
|
|
266
|
+
models: [],
|
|
267
|
+
},
|
|
268
|
+
},
|
|
269
|
+
},
|
|
270
|
+
|
|
271
|
+
qwen: {
|
|
272
|
+
id: 'qwen',
|
|
273
|
+
name: 'Qwen Code',
|
|
274
|
+
icon: 'Cpu',
|
|
275
|
+
endpoint: UNIFIED_ENDPOINT,
|
|
276
|
+
lanes: {
|
|
277
|
+
native: {
|
|
278
|
+
runtimeId: 'qwen-native',
|
|
279
|
+
label: 'Native',
|
|
280
|
+
models: [
|
|
281
|
+
{
|
|
282
|
+
id: 'qwen3-coder-plus',
|
|
283
|
+
name: 'qwen3-coder-plus',
|
|
284
|
+
label: 'Qwen 3 Coder Plus',
|
|
285
|
+
description: 'Primary Qwen coding model',
|
|
286
|
+
providerId: 'qwen',
|
|
287
|
+
default: true,
|
|
288
|
+
},
|
|
289
|
+
{
|
|
290
|
+
id: 'qwen3-coder-next',
|
|
291
|
+
name: 'qwen3-coder-next',
|
|
292
|
+
label: 'Qwen 3 Coder Next',
|
|
293
|
+
description: 'Latest Qwen coding lane',
|
|
294
|
+
providerId: 'qwen',
|
|
295
|
+
},
|
|
296
|
+
{
|
|
297
|
+
id: 'qwen3.5-plus',
|
|
298
|
+
name: 'qwen3.5-plus',
|
|
299
|
+
label: 'Qwen 3.5 Plus',
|
|
300
|
+
description: 'Higher-capability Qwen lane',
|
|
301
|
+
providerId: 'qwen',
|
|
302
|
+
},
|
|
303
|
+
{
|
|
304
|
+
id: 'qwen3-max',
|
|
305
|
+
name: 'qwen3-max',
|
|
306
|
+
label: 'Qwen 3 Max',
|
|
307
|
+
description: 'Largest Qwen lane',
|
|
308
|
+
providerId: 'qwen',
|
|
309
|
+
},
|
|
310
|
+
],
|
|
311
|
+
},
|
|
312
|
+
custom: {
|
|
313
|
+
runtimeId: 'qwen-custom',
|
|
314
|
+
label: 'Custom',
|
|
315
|
+
models: [
|
|
316
|
+
{
|
|
317
|
+
id: 'glm-4.7',
|
|
318
|
+
name: 'glm-4.7',
|
|
319
|
+
label: 'GLM 4.7',
|
|
320
|
+
description: 'Provider-side custom lane supported by Qwen Code',
|
|
321
|
+
providerId: 'zai',
|
|
322
|
+
},
|
|
323
|
+
{
|
|
324
|
+
id: 'kimi-k2.5',
|
|
325
|
+
name: 'kimi-k2.5',
|
|
326
|
+
label: 'Kimi K2.5',
|
|
327
|
+
description: 'Provider-side custom lane supported by Qwen Code',
|
|
328
|
+
providerId: 'kimi',
|
|
329
|
+
},
|
|
330
|
+
],
|
|
331
|
+
},
|
|
332
|
+
},
|
|
333
|
+
},
|
|
334
|
+
};
|
|
335
|
+
|
|
336
|
+
function flattenEngineModels(engineId, engine) {
|
|
337
|
+
return Object.entries(engine.lanes || {}).flatMap(([laneId, lane]) =>
|
|
338
|
+
(lane.models || []).map((model) => ({
|
|
339
|
+
...model,
|
|
340
|
+
lane: laneId,
|
|
341
|
+
laneLabel: lane.label || laneId,
|
|
342
|
+
runtimeId: model.runtimeId || lane.runtimeId,
|
|
343
|
+
category: engineId,
|
|
344
|
+
engine: engineId,
|
|
345
|
+
endpoint: engine.endpoint,
|
|
346
|
+
custom: laneId === 'custom',
|
|
347
|
+
}))
|
|
348
|
+
);
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
function getCatalog() {
|
|
352
|
+
return JSON.parse(JSON.stringify(CATALOG));
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
function getCliTools() {
|
|
356
|
+
const catalog = getCatalog();
|
|
357
|
+
const tools = {};
|
|
358
|
+
|
|
359
|
+
for (const [engineId, engine] of Object.entries(catalog)) {
|
|
360
|
+
tools[engineId] = {
|
|
361
|
+
id: engineId,
|
|
362
|
+
name: engine.name,
|
|
363
|
+
icon: engine.icon,
|
|
364
|
+
enabled: true,
|
|
365
|
+
endpoint: engine.endpoint,
|
|
366
|
+
thinkModes: engine.thinkModes || [],
|
|
367
|
+
defaultThinkMode: engine.defaultThinkMode,
|
|
368
|
+
lanes: engine.lanes,
|
|
369
|
+
models: flattenEngineModels(engineId, engine),
|
|
370
|
+
};
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
return tools;
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
function getAllModels() {
|
|
377
|
+
return Object.values(getCliTools()).flatMap((cli) => cli.models || []);
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
function getModelById(modelId) {
|
|
381
|
+
if (!modelId) return null;
|
|
382
|
+
return getAllModels().find((model) => model.id === modelId) || null;
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
function getEngineForModel(modelId) {
|
|
386
|
+
return getModelById(modelId)?.engine || null;
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
function isValidModelId(modelId) {
|
|
390
|
+
return Boolean(getModelById(modelId));
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
function getDefaultModelId() {
|
|
394
|
+
const models = getAllModels();
|
|
395
|
+
const withDefault = models.find((m) => m.default);
|
|
396
|
+
return withDefault ? withDefault.id : (models[0]?.id || null);
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
module.exports = {
|
|
400
|
+
UNIFIED_ENDPOINT,
|
|
401
|
+
getCatalog,
|
|
402
|
+
getCliTools,
|
|
403
|
+
getAllModels,
|
|
404
|
+
getModelById,
|
|
405
|
+
getEngineForModel,
|
|
406
|
+
isValidModelId,
|
|
407
|
+
getDefaultModelId,
|
|
408
|
+
};
|
|
@@ -0,0 +1,274 @@
|
|
|
1
|
+
const path = require('path');
|
|
2
|
+
const fs = require('fs');
|
|
3
|
+
|
|
4
|
+
// NexusCrew data directory
|
|
5
|
+
const dbDir = path.join(process.env.HOME, '.nexuscrew');
|
|
6
|
+
const dbPath = path.join(dbDir, 'nexuscrew.db');
|
|
7
|
+
|
|
8
|
+
// Ensure directory exists
|
|
9
|
+
if (!fs.existsSync(dbDir)) {
|
|
10
|
+
fs.mkdirSync(dbDir, { recursive: true });
|
|
11
|
+
console.log(`✅ Created database directory: ${dbDir}`);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
// Use sql.js driver
|
|
15
|
+
const Driver = require('./drivers/sql-js');
|
|
16
|
+
let db = null;
|
|
17
|
+
|
|
18
|
+
console.log(`📦 Using sql.js driver`);
|
|
19
|
+
|
|
20
|
+
// Initialize database
|
|
21
|
+
async function initDb(options = {}) {
|
|
22
|
+
const { skipMigrationCheck = false } = options;
|
|
23
|
+
const driver = new Driver(dbPath);
|
|
24
|
+
await driver.init();
|
|
25
|
+
db = driver;
|
|
26
|
+
|
|
27
|
+
console.log(`✅ Database ready: ${dbPath}`);
|
|
28
|
+
|
|
29
|
+
// Initialize schema
|
|
30
|
+
initSchema();
|
|
31
|
+
|
|
32
|
+
if (!skipMigrationCheck) {
|
|
33
|
+
const needsMigration = await checkMigrationNeeded();
|
|
34
|
+
|
|
35
|
+
if (needsMigration) {
|
|
36
|
+
console.log('[DB] Migration required - running migrations...');
|
|
37
|
+
const { runMigrations } = require('./migrate');
|
|
38
|
+
await runMigrations({ skipInit: true });
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
return db;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function initSchema() {
|
|
46
|
+
db.exec(`
|
|
47
|
+
-- Conversations table (NexusCrew + auth schema)
|
|
48
|
+
CREATE TABLE IF NOT EXISTS conversations (
|
|
49
|
+
id TEXT PRIMARY KEY,
|
|
50
|
+
title TEXT DEFAULT 'New Chat',
|
|
51
|
+
engine TEXT NOT NULL DEFAULT 'claude',
|
|
52
|
+
model TEXT,
|
|
53
|
+
tmux_window TEXT,
|
|
54
|
+
log_path TEXT,
|
|
55
|
+
workspace TEXT DEFAULT '',
|
|
56
|
+
host TEXT DEFAULT 'local',
|
|
57
|
+
status TEXT DEFAULT 'active',
|
|
58
|
+
pinned INTEGER DEFAULT 0,
|
|
59
|
+
metadata TEXT,
|
|
60
|
+
created_at INTEGER DEFAULT (unixepoch()),
|
|
61
|
+
updated_at INTEGER DEFAULT (unixepoch())
|
|
62
|
+
);
|
|
63
|
+
|
|
64
|
+
-- Messages table
|
|
65
|
+
CREATE TABLE IF NOT EXISTS messages (
|
|
66
|
+
id TEXT PRIMARY KEY,
|
|
67
|
+
conversation_id TEXT NOT NULL,
|
|
68
|
+
role TEXT NOT NULL,
|
|
69
|
+
content TEXT DEFAULT '',
|
|
70
|
+
engine TEXT,
|
|
71
|
+
model TEXT,
|
|
72
|
+
tokens_in INTEGER DEFAULT 0,
|
|
73
|
+
tokens_out INTEGER DEFAULT 0,
|
|
74
|
+
created_at INTEGER DEFAULT (unixepoch()),
|
|
75
|
+
FOREIGN KEY (conversation_id) REFERENCES conversations(id)
|
|
76
|
+
);
|
|
77
|
+
|
|
78
|
+
-- Users table (auth)
|
|
79
|
+
CREATE TABLE IF NOT EXISTS users (
|
|
80
|
+
id TEXT PRIMARY KEY,
|
|
81
|
+
username TEXT UNIQUE NOT NULL,
|
|
82
|
+
password_hash TEXT NOT NULL,
|
|
83
|
+
role TEXT NOT NULL DEFAULT 'user',
|
|
84
|
+
is_locked INTEGER NOT NULL DEFAULT 0,
|
|
85
|
+
failed_attempts INTEGER NOT NULL DEFAULT 0,
|
|
86
|
+
last_failed_attempt INTEGER,
|
|
87
|
+
locked_until INTEGER,
|
|
88
|
+
created_at INTEGER NOT NULL,
|
|
89
|
+
last_login INTEGER
|
|
90
|
+
);
|
|
91
|
+
|
|
92
|
+
-- API Keys table
|
|
93
|
+
CREATE TABLE IF NOT EXISTS api_keys (
|
|
94
|
+
provider TEXT PRIMARY KEY,
|
|
95
|
+
api_key TEXT NOT NULL,
|
|
96
|
+
created_at INTEGER NOT NULL,
|
|
97
|
+
updated_at INTEGER NOT NULL
|
|
98
|
+
);
|
|
99
|
+
|
|
100
|
+
-- Login attempts table
|
|
101
|
+
CREATE TABLE IF NOT EXISTS login_attempts (
|
|
102
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
103
|
+
ip_address TEXT NOT NULL,
|
|
104
|
+
username TEXT,
|
|
105
|
+
success INTEGER NOT NULL DEFAULT 0,
|
|
106
|
+
timestamp INTEGER NOT NULL
|
|
107
|
+
);
|
|
108
|
+
|
|
109
|
+
CREATE INDEX IF NOT EXISTS idx_login_attempts_ip
|
|
110
|
+
ON login_attempts(ip_address, timestamp DESC);
|
|
111
|
+
|
|
112
|
+
-- Session summaries table
|
|
113
|
+
CREATE TABLE IF NOT EXISTS session_summaries (
|
|
114
|
+
conversation_id TEXT PRIMARY KEY,
|
|
115
|
+
summary_short TEXT,
|
|
116
|
+
key_decisions TEXT,
|
|
117
|
+
files_modified TEXT,
|
|
118
|
+
updated_at INTEGER,
|
|
119
|
+
FOREIGN KEY (conversation_id) REFERENCES conversations(id)
|
|
120
|
+
);
|
|
121
|
+
|
|
122
|
+
-- Indexes
|
|
123
|
+
CREATE INDEX IF NOT EXISTS idx_messages_conv ON messages(conversation_id, created_at);
|
|
124
|
+
CREATE INDEX IF NOT EXISTS idx_conversations_status ON conversations(status, updated_at);
|
|
125
|
+
CREATE INDEX IF NOT EXISTS idx_users_username ON users(username);
|
|
126
|
+
`);
|
|
127
|
+
|
|
128
|
+
console.log('✅ Database schema initialized');
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Get API key for a provider
|
|
133
|
+
* @param {string} provider - Provider name (e.g., 'deepseek', 'openai')
|
|
134
|
+
* @returns {string|null} API key or null if not found
|
|
135
|
+
*/
|
|
136
|
+
function getApiKey(provider) {
|
|
137
|
+
if (!db) return null;
|
|
138
|
+
try {
|
|
139
|
+
const stmt = db.prepare('SELECT api_key FROM api_keys WHERE provider = ?');
|
|
140
|
+
const row = stmt.get(provider.toLowerCase());
|
|
141
|
+
return row?.api_key || null;
|
|
142
|
+
} catch (err) {
|
|
143
|
+
console.error(`[DB] Error getting API key for ${provider}:`, err.message);
|
|
144
|
+
return null;
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Set API key for a provider
|
|
150
|
+
* @param {string} provider - Provider name
|
|
151
|
+
* @param {string} apiKey - API key value
|
|
152
|
+
* @returns {boolean} Success
|
|
153
|
+
*/
|
|
154
|
+
function setApiKey(provider, apiKey) {
|
|
155
|
+
if (!db) return false;
|
|
156
|
+
try {
|
|
157
|
+
const now = Date.now();
|
|
158
|
+
const stmt = db.prepare(`
|
|
159
|
+
INSERT INTO api_keys (provider, api_key, created_at, updated_at)
|
|
160
|
+
VALUES (?, ?, ?, ?)
|
|
161
|
+
ON CONFLICT(provider) DO UPDATE SET api_key = ?, updated_at = ?
|
|
162
|
+
`);
|
|
163
|
+
stmt.run(provider.toLowerCase(), apiKey, now, now, apiKey, now);
|
|
164
|
+
db.save();
|
|
165
|
+
return true;
|
|
166
|
+
} catch (err) {
|
|
167
|
+
console.error(`[DB] Error setting API key for ${provider}:`, err.message);
|
|
168
|
+
return false;
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* Delete API key for a provider
|
|
174
|
+
* @param {string} provider - Provider name
|
|
175
|
+
* @returns {boolean} Success
|
|
176
|
+
*/
|
|
177
|
+
function deleteApiKey(provider) {
|
|
178
|
+
if (!db) return false;
|
|
179
|
+
try {
|
|
180
|
+
const stmt = db.prepare('DELETE FROM api_keys WHERE provider = ?');
|
|
181
|
+
stmt.run(provider.toLowerCase());
|
|
182
|
+
db.save();
|
|
183
|
+
return true;
|
|
184
|
+
} catch (err) {
|
|
185
|
+
console.error(`[DB] Error deleting API key for ${provider}:`, err.message);
|
|
186
|
+
return false;
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* List all configured API key providers (without exposing keys)
|
|
192
|
+
* @returns {Array} List of provider names
|
|
193
|
+
*/
|
|
194
|
+
function listApiKeyProviders() {
|
|
195
|
+
if (!db) return [];
|
|
196
|
+
try {
|
|
197
|
+
const stmt = db.prepare('SELECT provider, created_at, updated_at FROM api_keys');
|
|
198
|
+
return stmt.all();
|
|
199
|
+
} catch (err) {
|
|
200
|
+
console.error('[DB] Error listing API key providers:', err.message);
|
|
201
|
+
return [];
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
async function checkMigrationNeeded() {
|
|
206
|
+
const currentDb = getDb();
|
|
207
|
+
|
|
208
|
+
if (!currentDb) {
|
|
209
|
+
console.error('[DB] Cannot check migration status - DB not initialized');
|
|
210
|
+
return false;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
try {
|
|
214
|
+
const hasMigrationsStmt = currentDb.prepare(`
|
|
215
|
+
SELECT name FROM sqlite_master
|
|
216
|
+
WHERE type='table' AND name='_migrations'
|
|
217
|
+
`);
|
|
218
|
+
const hasMigrations = hasMigrationsStmt.get();
|
|
219
|
+
|
|
220
|
+
if (!hasMigrations) {
|
|
221
|
+
console.log('[DB] _migrations table not found - first run or migration needed');
|
|
222
|
+
return true;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
console.log('[DB] No migration needed');
|
|
226
|
+
return false;
|
|
227
|
+
} catch (error) {
|
|
228
|
+
console.error('[DB] Error checking migration status:', error);
|
|
229
|
+
return false;
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
function getDb() {
|
|
234
|
+
return db;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
function prepare(sql) {
|
|
238
|
+
return db.prepare(sql);
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
function saveDb() {
|
|
242
|
+
if (db) db.save();
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
// Graceful shutdown
|
|
246
|
+
process.on('exit', () => {
|
|
247
|
+
if (db) {
|
|
248
|
+
db.close();
|
|
249
|
+
console.log('✅ Database connection closed');
|
|
250
|
+
}
|
|
251
|
+
});
|
|
252
|
+
|
|
253
|
+
process.on('SIGINT', () => {
|
|
254
|
+
if (db) {
|
|
255
|
+
db.close();
|
|
256
|
+
console.log('✅ Database connection closed (SIGINT)');
|
|
257
|
+
}
|
|
258
|
+
process.exit(0);
|
|
259
|
+
});
|
|
260
|
+
|
|
261
|
+
process.on('SIGTERM', () => {
|
|
262
|
+
if (db) db.close();
|
|
263
|
+
});
|
|
264
|
+
|
|
265
|
+
module.exports = {
|
|
266
|
+
initDb,
|
|
267
|
+
getDb,
|
|
268
|
+
prepare,
|
|
269
|
+
saveDb,
|
|
270
|
+
getApiKey,
|
|
271
|
+
setApiKey,
|
|
272
|
+
deleteApiKey,
|
|
273
|
+
listApiKeyProviders
|
|
274
|
+
};
|