neoagent 2.4.1-beta.9 → 2.4.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/.env.example +33 -3
- package/LICENSE +111 -56
- package/README.md +8 -3
- package/docs/configuration.md +8 -0
- package/docs/getting-started.md +9 -3
- package/docs/index.md +4 -0
- package/extensions/chrome-browser/background.mjs +36 -4
- package/extensions/chrome-browser/icons/icon128.png +0 -0
- package/extensions/chrome-browser/icons/icon16.png +0 -0
- package/extensions/chrome-browser/icons/icon48.png +0 -0
- package/extensions/chrome-browser/icons/logo.svg +39 -8
- package/extensions/chrome-browser/manifest.json +3 -3
- package/extensions/chrome-browser/popup.html +5 -1
- package/extensions/chrome-browser/popup.js +13 -0
- package/flutter_app/android/app/src/main/AndroidManifest.xml +2 -1
- package/flutter_app/android/app/src/main/res/mipmap-hdpi/ic_launcher.png +0 -0
- package/flutter_app/android/app/src/main/res/mipmap-mdpi/ic_launcher.png +0 -0
- package/flutter_app/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png +0 -0
- package/flutter_app/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png +0 -0
- package/flutter_app/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png +0 -0
- package/flutter_app/assets/branding/app_icon_1024.png +0 -0
- package/flutter_app/assets/branding/app_icon_128.png +0 -0
- package/flutter_app/assets/branding/app_icon_192.png +0 -0
- package/flutter_app/assets/branding/app_icon_256.png +0 -0
- package/flutter_app/assets/branding/app_icon_32.png +0 -0
- package/flutter_app/assets/branding/app_icon_512.png +0 -0
- package/flutter_app/assets/branding/app_icon_64.png +0 -0
- package/flutter_app/assets/branding/app_icon_light_1024.png +0 -0
- package/flutter_app/assets/branding/app_icon_light_128.png +0 -0
- package/flutter_app/assets/branding/app_icon_light_192.png +0 -0
- package/flutter_app/assets/branding/app_icon_light_256.png +0 -0
- package/flutter_app/assets/branding/app_icon_light_32.png +0 -0
- package/flutter_app/assets/branding/app_icon_light_512.png +0 -0
- package/flutter_app/assets/branding/app_icon_light_64.png +0 -0
- package/flutter_app/assets/branding/onboarding_intro.mp4 +0 -0
- package/flutter_app/assets/branding/tray_icon_light_template.png +0 -0
- package/flutter_app/assets/branding/tray_icon_template.png +0 -0
- package/flutter_app/lib/features/location/location_service.dart +3 -0
- package/flutter_app/lib/features/onboarding/onboarding_chrome.dart +391 -382
- package/flutter_app/lib/features/onboarding/onboarding_companion_step.dart +743 -0
- package/flutter_app/lib/features/onboarding/onboarding_messaging_step.dart +18 -16
- package/flutter_app/lib/features/onboarding/onboarding_model_step.dart +19 -18
- package/flutter_app/lib/features/onboarding/onboarding_shell.dart +8 -1
- package/flutter_app/lib/features/onboarding/onboarding_video_step.dart +16 -13
- package/flutter_app/lib/features/onboarding/onboarding_welcome_step.dart +17 -13
- package/flutter_app/lib/main.dart +2 -0
- package/flutter_app/lib/main_account_settings.dart +10 -34
- package/flutter_app/lib/main_admin.dart +14 -1
- package/flutter_app/lib/main_app_shell.dart +377 -340
- package/flutter_app/lib/main_chat.dart +707 -227
- package/flutter_app/lib/main_controller.dart +197 -42
- package/flutter_app/lib/main_devices.dart +436 -4
- package/flutter_app/lib/main_integrations.dart +255 -6
- package/flutter_app/lib/main_launcher.dart +1 -1
- package/flutter_app/lib/main_model_picker.dart +685 -0
- package/flutter_app/lib/main_models.dart +212 -0
- package/flutter_app/lib/main_navigation.dart +1 -9
- package/flutter_app/lib/main_operations.dart +1417 -614
- package/flutter_app/lib/main_settings.dart +673 -871
- package/flutter_app/lib/main_shared.dart +971 -443
- package/flutter_app/lib/main_spacing.dart +4 -4
- package/flutter_app/lib/main_theme.dart +22 -14
- package/flutter_app/lib/main_unified.dart +5 -72
- package/flutter_app/lib/src/android_apk_drop_zone.dart +24 -0
- package/flutter_app/lib/src/android_apk_drop_zone_stub.dart +13 -0
- package/flutter_app/lib/src/android_apk_drop_zone_web.dart +219 -0
- package/flutter_app/lib/src/backend_client.dart +46 -0
- package/flutter_app/lib/src/desktop_companion_actions.dart +30 -7
- package/flutter_app/lib/src/desktop_companion_io.dart +71 -1
- package/flutter_app/lib/src/security/password_strength.dart +84 -0
- package/flutter_app/lib/src/stream_renderer.dart +42 -3
- package/flutter_app/lib/src/theme/palette.dart +76 -34
- package/flutter_app/linux/runner/resources/app_icon.png +0 -0
- package/flutter_app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png +0 -0
- package/flutter_app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png +0 -0
- package/flutter_app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png +0 -0
- package/flutter_app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png +0 -0
- package/flutter_app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png +0 -0
- package/flutter_app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png +0 -0
- package/flutter_app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png +0 -0
- package/flutter_app/pubspec.lock +2 -2
- package/flutter_app/pubspec.yaml +7 -1
- package/flutter_app/tool/branding_source/neoagent-icon-1024.png +0 -0
- package/flutter_app/tool/branding_source/neoagent-icon-128.png +0 -0
- package/flutter_app/tool/branding_source/neoagent-icon-16.png +0 -0
- package/flutter_app/tool/branding_source/neoagent-icon-180.png +0 -0
- package/flutter_app/tool/branding_source/neoagent-icon-192.png +0 -0
- package/flutter_app/tool/branding_source/neoagent-icon-256.png +0 -0
- package/flutter_app/tool/branding_source/neoagent-icon-32.png +0 -0
- package/flutter_app/tool/branding_source/neoagent-icon-48.png +0 -0
- package/flutter_app/tool/branding_source/neoagent-icon-512.png +0 -0
- package/flutter_app/tool/branding_source/neoagent-icon-64.png +0 -0
- package/flutter_app/tool/branding_source/neoagent-icon.svg +43 -0
- package/flutter_app/tool/generate_desktop_branding.py +154 -152
- package/flutter_app/web/favicon.png +0 -0
- package/flutter_app/web/favicon.svg +40 -9
- package/flutter_app/web/favicon_light.svg +43 -0
- package/flutter_app/web/icons/Icon-192-light.png +0 -0
- package/flutter_app/web/icons/Icon-192.png +0 -0
- package/flutter_app/web/icons/Icon-512-light.png +0 -0
- package/flutter_app/web/icons/Icon-512.png +0 -0
- package/flutter_app/web/icons/Icon-maskable-192-light.png +0 -0
- package/flutter_app/web/icons/Icon-maskable-192.png +0 -0
- package/flutter_app/web/icons/Icon-maskable-512-light.png +0 -0
- package/flutter_app/web/icons/Icon-maskable-512.png +0 -0
- package/flutter_app/windows/runner/main.cpp +7 -1
- package/flutter_app/windows/runner/resources/app_icon.ico +0 -0
- package/lib/manager.js +445 -83
- package/package.json +17 -3
- package/runtime/paths.js +3 -1
- package/server/admin/access.js +198 -0
- package/server/admin/admin.css +268 -0
- package/server/admin/admin.js +348 -0
- package/server/admin/analytics.js +128 -0
- package/server/admin/index.html +1015 -0
- package/server/admin/login.html +290 -0
- package/server/admin/logo.svg +43 -0
- package/server/admin/sql.js +134 -0
- package/server/admin/users.js +147 -0
- package/server/config/origins.js +3 -1
- package/server/db/database.js +92 -0
- package/server/db/ftsQuery.js +27 -0
- package/server/http/routes.js +1 -0
- package/server/http/static.js +23 -6
- package/server/index.js +1 -1
- package/server/middleware/adminAuth.js +48 -0
- package/server/middleware/auth.js +1 -40
- package/server/public/.last_build_id +1 -1
- package/server/public/app_icon.png +0 -0
- package/server/public/assets/AssetManifest.bin +1 -1
- package/server/public/assets/AssetManifest.bin.json +1 -1
- package/server/public/assets/assets/branding/app_icon_1024.png +0 -0
- package/server/public/assets/assets/branding/app_icon_256.png +0 -0
- package/server/public/assets/assets/branding/app_icon_512.png +0 -0
- package/server/public/assets/assets/branding/app_icon_light_1024.png +0 -0
- package/server/public/assets/assets/branding/app_icon_light_256.png +0 -0
- package/server/public/assets/assets/branding/app_icon_light_512.png +0 -0
- package/server/public/assets/assets/branding/onboarding_intro.mp4 +0 -0
- package/server/public/assets/assets/branding/tray_icon_light_template.png +0 -0
- package/server/public/assets/assets/branding/tray_icon_template.png +0 -0
- package/server/public/assets/fonts/MaterialIcons-Regular.otf +0 -0
- package/server/public/assets/web/icons/Icon-192.png +0 -0
- package/server/public/favicon.png +0 -0
- package/server/public/favicon.svg +40 -9
- package/server/public/favicon_light.svg +43 -0
- package/server/public/flutter_bootstrap.js +2 -2
- package/server/public/icons/Icon-192-light.png +0 -0
- package/server/public/icons/Icon-192.png +0 -0
- package/server/public/icons/Icon-512-light.png +0 -0
- package/server/public/icons/Icon-512.png +0 -0
- package/server/public/icons/Icon-maskable-192-light.png +0 -0
- package/server/public/icons/Icon-maskable-192.png +0 -0
- package/server/public/icons/Icon-maskable-512-light.png +0 -0
- package/server/public/icons/Icon-maskable-512.png +0 -0
- package/server/public/main.dart.js +88537 -85936
- package/server/routes/admin.js +632 -0
- package/server/routes/agent_profiles.js +3 -0
- package/server/routes/agents.js +1 -1
- package/server/routes/auth.js +3 -1
- package/server/routes/mcp.js +29 -13
- package/server/routes/memory.js +23 -1
- package/server/routes/messaging.js +2 -0
- package/server/routes/screenHistory.js +14 -8
- package/server/routes/settings.js +10 -2
- package/server/routes/stream.js +1 -1
- package/server/routes/triggers.js +4 -4
- package/server/routes/voice_assistant.js +36 -1
- package/server/routes/workspace.js +86 -0
- package/server/services/account/admin_two_factor.js +132 -0
- package/server/services/account/password_policy.js +6 -1
- package/server/services/ai/completion.js +44 -0
- package/server/services/ai/engine.js +321 -500
- package/server/services/ai/imageAnalysis.js +9 -5
- package/server/services/ai/logFormat.js +46 -0
- package/server/services/ai/loopPolicy.js +11 -0
- package/server/services/ai/messagingFallback.js +228 -0
- package/server/services/ai/models.js +194 -55
- package/server/services/ai/providerRetry.js +169 -0
- package/server/services/ai/providers/anthropic.js +15 -4
- package/server/services/ai/providers/google.js +38 -11
- package/server/services/ai/providers/grok.js +19 -68
- package/server/services/ai/providers/grokOauth.js +141 -0
- package/server/services/ai/providers/nvidia.js +154 -0
- package/server/services/ai/providers/ollama.js +76 -39
- package/server/services/ai/providers/openai.js +20 -31
- package/server/services/ai/providers/openaiCodex.js +6 -2
- package/server/services/ai/providers/openaiCompatible.js +70 -0
- package/server/services/ai/providers/openrouter.js +162 -0
- package/server/services/ai/settings.js +30 -0
- package/server/services/ai/systemPrompt.js +51 -29
- package/server/services/ai/taskAnalysis.js +60 -1
- package/server/services/ai/toolEvidence.js +207 -0
- package/server/services/ai/toolSelector.js +8 -1
- package/server/services/ai/tools.js +78 -13
- package/server/services/browser/extension/registry.js +94 -9
- package/server/services/desktop/registry.js +104 -1
- package/server/services/desktop/screenRecorder.js +208 -98
- package/server/services/desktop/screen_recorder_support.js +46 -0
- package/server/services/integrations/google/provider.js +13 -0
- package/server/services/integrations/home_assistant/constants.js +88 -0
- package/server/services/integrations/home_assistant/network.js +207 -0
- package/server/services/integrations/home_assistant/provider.js +249 -0
- package/server/services/integrations/home_assistant/snapshot.js +98 -0
- package/server/services/integrations/home_assistant/tools.js +101 -0
- package/server/services/integrations/manager.js +5 -0
- package/server/services/integrations/registry.js +2 -0
- package/server/services/integrations/trello/provider.js +8 -2
- package/server/services/manager.js +51 -53
- package/server/services/mcp/client.js +208 -268
- package/server/services/mcp/client_support.js +172 -0
- package/server/services/mcp/recovery.js +116 -0
- package/server/services/mcp/tool_operations.js +123 -0
- package/server/services/memory/ingestion.js +185 -370
- package/server/services/memory/ingestion_coverage.js +129 -0
- package/server/services/memory/ingestion_documents.js +123 -0
- package/server/services/memory/ingestion_support.js +171 -0
- package/server/services/memory/intelligence.js +181 -0
- package/server/services/memory/manager.js +476 -32
- package/server/services/messaging/automation.js +71 -134
- package/server/services/messaging/formatting_guides.js +26 -1
- package/server/services/messaging/inbound_queue.js +111 -0
- package/server/services/messaging/manager.js +25 -8
- package/server/services/messaging/typing_keepalive.js +110 -0
- package/server/services/streaming/android-stream.js +293 -40
- package/server/services/tasks/integration_runtime.js +4 -1
- package/server/services/tasks/runtime.js +415 -56
- package/server/services/tasks/task_repository.js +48 -6
- package/server/services/websocket.js +8 -2
- package/server/services/widgets/service.js +17 -1
- package/server/services/workspace/manager.js +116 -0
- package/server/utils/logger.js +44 -6
- package/server/utils/security.js +3 -0
- package/server/utils/version.js +29 -19
- package/server/services/memory/openhuman_uplift.test.js +0 -98
|
@@ -0,0 +1,632 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const path = require('path');
|
|
4
|
+
const express = require('express');
|
|
5
|
+
const { spawn } = require('child_process');
|
|
6
|
+
const { requireAdminAuth } = require('../middleware/adminAuth');
|
|
7
|
+
const { getVersionInfo } = require('../utils/version');
|
|
8
|
+
const {
|
|
9
|
+
readUpdateStatus,
|
|
10
|
+
writeUpdateStatusFile: writeUpdateStatus,
|
|
11
|
+
} = require('../utils/update_status');
|
|
12
|
+
const {
|
|
13
|
+
parseReleaseChannel,
|
|
14
|
+
writeReleaseChannelToEnvFile,
|
|
15
|
+
getReleaseChannelBranchPolicy,
|
|
16
|
+
getReleaseChannelNpmPolicy,
|
|
17
|
+
} = require('../../runtime/release_channel');
|
|
18
|
+
const { APP_DIR, ENV_FILE, upsertEnvValue } = require('../../runtime/paths');
|
|
19
|
+
const { isManagedDeployment } = require('../utils/deployment');
|
|
20
|
+
const rateLimit = require('express-rate-limit');
|
|
21
|
+
|
|
22
|
+
const router = express.Router();
|
|
23
|
+
const ADMIN_DIR = path.join(__dirname, '..', 'admin');
|
|
24
|
+
|
|
25
|
+
const fs = require('fs');
|
|
26
|
+
|
|
27
|
+
// Admin sessions last 30 days and roll on every request.
|
|
28
|
+
const ADMIN_SESSION_TTL = 30 * 24 * 60 * 60 * 1000;
|
|
29
|
+
|
|
30
|
+
// Rolling refresh: touch the session on every authenticated admin request
|
|
31
|
+
// so the 30-day window slides forward from the last activity.
|
|
32
|
+
router.use((req, res, next) => {
|
|
33
|
+
if (req.session?.isAdmin) {
|
|
34
|
+
req.session.cookie.maxAge = ADMIN_SESSION_TTL;
|
|
35
|
+
req.session.touch();
|
|
36
|
+
}
|
|
37
|
+
next();
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
const loginLimiter = rateLimit({
|
|
41
|
+
windowMs: 15 * 60 * 1000,
|
|
42
|
+
max: 20,
|
|
43
|
+
standardHeaders: true,
|
|
44
|
+
legacyHeaders: false,
|
|
45
|
+
skipSuccessfulRequests: true,
|
|
46
|
+
message: { error: 'Too many login attempts, try again later' },
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
const updateTriggerLimiter = rateLimit({
|
|
50
|
+
windowMs: 15 * 60 * 1000,
|
|
51
|
+
max: 5,
|
|
52
|
+
standardHeaders: true,
|
|
53
|
+
legacyHeaders: false,
|
|
54
|
+
message: { error: 'Too many update requests, try again later' },
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
// --- Auth ---
|
|
58
|
+
|
|
59
|
+
router.get('/login', (req, res) => {
|
|
60
|
+
if (req.session?.isAdmin) return res.redirect('/admin');
|
|
61
|
+
res.sendFile(path.join(ADMIN_DIR, 'login.html'));
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
router.post('/api/login', loginLimiter, express.json(), async (req, res) => {
|
|
65
|
+
const { username, password } = req.body || {};
|
|
66
|
+
const expectedUsername = process.env.ADMIN_USERNAME || 'admin';
|
|
67
|
+
const expectedPassword = process.env.ADMIN_PASSWORD || '';
|
|
68
|
+
if (!expectedPassword) {
|
|
69
|
+
return res.status(503).json({ error: 'Admin credentials not configured. Run `neoagent setup`.' });
|
|
70
|
+
}
|
|
71
|
+
if (username !== expectedUsername || password !== expectedPassword) {
|
|
72
|
+
return res.status(401).json({ error: 'Invalid credentials' });
|
|
73
|
+
}
|
|
74
|
+
// Check whether 2FA is enabled
|
|
75
|
+
const adminTwoFactor = require('../services/account/admin_two_factor');
|
|
76
|
+
const tfStatus = adminTwoFactor.getStatus();
|
|
77
|
+
if (tfStatus.enabled) {
|
|
78
|
+
// Park the session in a "password OK, waiting for TOTP" state
|
|
79
|
+
req.session.adminPendingTwoFactor = true;
|
|
80
|
+
delete req.session.isAdmin;
|
|
81
|
+
return req.session.save((err) => {
|
|
82
|
+
if (err) return res.status(500).json({ error: 'Session error' });
|
|
83
|
+
res.json({ ok: false, requiresTwoFactor: true });
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
req.session.isAdmin = true;
|
|
87
|
+
req.session.cookie.maxAge = ADMIN_SESSION_TTL;
|
|
88
|
+
req.session.save((err) => {
|
|
89
|
+
if (err) return res.status(500).json({ error: 'Session error' });
|
|
90
|
+
res.json({ ok: true });
|
|
91
|
+
});
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
// Second-factor verification (called after a successful password login when 2FA is on)
|
|
95
|
+
router.post('/api/2fa/verify', loginLimiter, express.json(), async (req, res) => {
|
|
96
|
+
if (!req.session?.adminPendingTwoFactor) {
|
|
97
|
+
return res.status(400).json({ error: 'No pending 2FA verification' });
|
|
98
|
+
}
|
|
99
|
+
const { code } = req.body || {};
|
|
100
|
+
try {
|
|
101
|
+
const adminTwoFactor = require('../services/account/admin_two_factor');
|
|
102
|
+
const valid = await adminTwoFactor.verifyCode(code);
|
|
103
|
+
if (!valid) return res.status(401).json({ error: 'Invalid code — try again' });
|
|
104
|
+
req.session.adminPendingTwoFactor = false;
|
|
105
|
+
req.session.isAdmin = true;
|
|
106
|
+
req.session.cookie.maxAge = ADMIN_SESSION_TTL;
|
|
107
|
+
req.session.save((err) => {
|
|
108
|
+
if (err) return res.status(500).json({ error: 'Session error' });
|
|
109
|
+
res.json({ ok: true });
|
|
110
|
+
});
|
|
111
|
+
} catch (err) {
|
|
112
|
+
res.status(500).json({ error: err.message });
|
|
113
|
+
}
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
router.post('/api/logout', (req, res) => {
|
|
117
|
+
req.session.destroy(() => res.json({ ok: true }));
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
// --- Protected API ---
|
|
121
|
+
|
|
122
|
+
router.get('/api/logs', requireAdminAuth, (req, res) => {
|
|
123
|
+
const logHistory = req.app.locals.logHistory || [];
|
|
124
|
+
res.json({ logs: logHistory });
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
router.get('/api/version', requireAdminAuth, (req, res) => {
|
|
128
|
+
const version = getVersionInfo();
|
|
129
|
+
const status = readUpdateStatus();
|
|
130
|
+
res.json({
|
|
131
|
+
version: version.version,
|
|
132
|
+
installedVersion: version.installedVersion,
|
|
133
|
+
packageVersion: version.packageVersion,
|
|
134
|
+
gitVersion: version.gitVersion,
|
|
135
|
+
gitSha: version.gitSha,
|
|
136
|
+
gitBranch: version.gitBranch,
|
|
137
|
+
releaseChannel: status.releaseChannel || version.releaseChannel,
|
|
138
|
+
deploymentMode: version.deploymentMode,
|
|
139
|
+
deploymentProfile: version.deploymentProfile,
|
|
140
|
+
allowSelfUpdate: version.allowSelfUpdate,
|
|
141
|
+
updateStatus: {
|
|
142
|
+
state: status.state,
|
|
143
|
+
progress: status.progress,
|
|
144
|
+
phase: status.phase,
|
|
145
|
+
message: status.message,
|
|
146
|
+
},
|
|
147
|
+
uptime: process.uptime(),
|
|
148
|
+
nodeVersion: process.version,
|
|
149
|
+
});
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
router.get('/api/health', requireAdminAuth, async (req, res) => {
|
|
153
|
+
const runtimeManager = req.app?.locals?.runtimeManager;
|
|
154
|
+
const desktopRegistry = req.app?.locals?.desktopCompanionRegistry;
|
|
155
|
+
const extensionRegistry = req.app?.locals?.browserExtensionRegistry;
|
|
156
|
+
const results = [];
|
|
157
|
+
|
|
158
|
+
results.push({ id: 'backend', label: 'Backend server', passed: true, detail: 'Running' });
|
|
159
|
+
|
|
160
|
+
const version = getVersionInfo();
|
|
161
|
+
results.push({
|
|
162
|
+
id: 'version',
|
|
163
|
+
label: 'Server version',
|
|
164
|
+
passed: true,
|
|
165
|
+
detail: version.version || version.packageVersion || 'Unknown',
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
try {
|
|
169
|
+
const db = require('../db/database');
|
|
170
|
+
db.prepare('SELECT 1').get();
|
|
171
|
+
results.push({ id: 'database', label: 'Database', passed: true, detail: 'SQLite connected' });
|
|
172
|
+
} catch (err) {
|
|
173
|
+
results.push({ id: 'database', label: 'Database', passed: false, detail: String(err?.message || err).slice(0, 120) });
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
const updateStatus = readUpdateStatus();
|
|
177
|
+
results.push({
|
|
178
|
+
id: 'update',
|
|
179
|
+
label: 'Update status',
|
|
180
|
+
passed: updateStatus.state !== 'failed',
|
|
181
|
+
detail: updateStatus.state === 'idle'
|
|
182
|
+
? 'No update running'
|
|
183
|
+
: `${updateStatus.state} — ${updateStatus.message || ''}`.trim(),
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
const { getRuntimeValidation } = require('../services/runtime/validation');
|
|
187
|
+
const runtimeValidation = getRuntimeValidation(runtimeManager);
|
|
188
|
+
const runtimeReady = Boolean(runtimeValidation?.ready);
|
|
189
|
+
results.push({
|
|
190
|
+
id: 'vm_runtime',
|
|
191
|
+
label: 'Cloud VM runtime',
|
|
192
|
+
passed: runtimeReady,
|
|
193
|
+
detail: runtimeReady ? 'Available' : String(runtimeValidation?.issues?.[0] || 'Not configured'),
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
if (desktopRegistry) {
|
|
197
|
+
try {
|
|
198
|
+
const connectedUsers = [];
|
|
199
|
+
for (const socket of (req.app?.locals?.io?.sockets?.sockets?.values?.() || [])) {
|
|
200
|
+
const uid = socket.request?.session?.userId;
|
|
201
|
+
if (uid && !connectedUsers.includes(uid)) connectedUsers.push(uid);
|
|
202
|
+
}
|
|
203
|
+
results.push({
|
|
204
|
+
id: 'desktop',
|
|
205
|
+
label: 'Desktop companion',
|
|
206
|
+
passed: true,
|
|
207
|
+
detail: 'Registry available',
|
|
208
|
+
});
|
|
209
|
+
} catch {
|
|
210
|
+
results.push({ id: 'desktop', label: 'Desktop companion', passed: false, detail: 'Registry error' });
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
if (extensionRegistry) {
|
|
215
|
+
results.push({ id: 'extension', label: 'Chrome extension registry', passed: true, detail: 'Available' });
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
const configuredProviders = [];
|
|
219
|
+
if (process.env.ANTHROPIC_API_KEY) configuredProviders.push('Anthropic');
|
|
220
|
+
if (process.env.OPENAI_API_KEY) configuredProviders.push('OpenAI');
|
|
221
|
+
if (process.env.XAI_API_KEY) configuredProviders.push('xAI');
|
|
222
|
+
if (process.env.GOOGLE_AI_KEY) configuredProviders.push('Google');
|
|
223
|
+
if (process.env.OPENROUTER_API_KEY) configuredProviders.push('OpenRouter');
|
|
224
|
+
results.push({
|
|
225
|
+
id: 'ai_providers',
|
|
226
|
+
label: 'AI providers',
|
|
227
|
+
passed: configuredProviders.length > 0,
|
|
228
|
+
detail: configuredProviders.length > 0
|
|
229
|
+
? configuredProviders.join(', ')
|
|
230
|
+
: 'No providers configured',
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
if (process.env.DEEPGRAM_API_KEY) {
|
|
234
|
+
results.push({ id: 'deepgram', label: 'Deepgram (voice)', passed: true, detail: 'API key configured' });
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
const allPassed = results.every((r) => r.passed);
|
|
238
|
+
res.json({ passed: allPassed, results });
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
router.get('/api/config', requireAdminAuth, (req, res) => {
|
|
242
|
+
const safe = [
|
|
243
|
+
'PORT', 'NODE_ENV', 'PUBLIC_URL', 'NEOAGENT_DEPLOYMENT_MODE',
|
|
244
|
+
'NEOAGENT_PROFILE', 'NEOAGENT_RELEASE_CHANNEL', 'ALLOWED_ORIGINS',
|
|
245
|
+
'SECURE_COOKIES', 'TRUST_PROXY', 'ADMIN_USERNAME',
|
|
246
|
+
];
|
|
247
|
+
const config = {};
|
|
248
|
+
for (const key of safe) {
|
|
249
|
+
config[key] = process.env[key] || '';
|
|
250
|
+
}
|
|
251
|
+
res.json({ config });
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
router.post('/api/update', requireAdminAuth, updateTriggerLimiter, (req, res) => {
|
|
255
|
+
if (isManagedDeployment()) {
|
|
256
|
+
return res.status(403).json({ error: 'Updates are managed by this deployment.' });
|
|
257
|
+
}
|
|
258
|
+
const status = readUpdateStatus();
|
|
259
|
+
if (status.state === 'running') {
|
|
260
|
+
return res.status(409).json({ error: 'An update is already running' });
|
|
261
|
+
}
|
|
262
|
+
console.log('[Admin] Triggering update-runner...');
|
|
263
|
+
const child = spawn(process.execPath, ['scripts/update-runner.js'], {
|
|
264
|
+
detached: true,
|
|
265
|
+
stdio: 'ignore',
|
|
266
|
+
cwd: APP_DIR,
|
|
267
|
+
});
|
|
268
|
+
writeUpdateStatus({
|
|
269
|
+
state: 'running',
|
|
270
|
+
progress: 1,
|
|
271
|
+
phase: 'starting',
|
|
272
|
+
message: 'Launching update job',
|
|
273
|
+
startedAt: new Date().toISOString(),
|
|
274
|
+
completedAt: null,
|
|
275
|
+
versionBefore: null,
|
|
276
|
+
versionAfter: null,
|
|
277
|
+
runnerPid: child.pid,
|
|
278
|
+
changelog: [],
|
|
279
|
+
logs: [],
|
|
280
|
+
});
|
|
281
|
+
child.once('error', (error) => {
|
|
282
|
+
writeUpdateStatus({
|
|
283
|
+
state: 'failed',
|
|
284
|
+
progress: 100,
|
|
285
|
+
phase: 'failed',
|
|
286
|
+
message: `Failed to launch update job: ${error.message}`,
|
|
287
|
+
completedAt: new Date().toISOString(),
|
|
288
|
+
runnerPid: null,
|
|
289
|
+
});
|
|
290
|
+
});
|
|
291
|
+
child.unref();
|
|
292
|
+
res.json({ ok: true, message: 'Update triggered', pid: child.pid });
|
|
293
|
+
});
|
|
294
|
+
|
|
295
|
+
router.put('/api/update/channel', requireAdminAuth, (req, res) => {
|
|
296
|
+
if (isManagedDeployment()) {
|
|
297
|
+
return res.status(403).json({ error: 'Release channel changes are managed by this deployment.' });
|
|
298
|
+
}
|
|
299
|
+
const requested = req.body?.channel;
|
|
300
|
+
const releaseChannel = parseReleaseChannel(requested);
|
|
301
|
+
if (!releaseChannel) {
|
|
302
|
+
return res.status(400).json({ error: 'Release channel must be "stable" or "beta".' });
|
|
303
|
+
}
|
|
304
|
+
writeReleaseChannelToEnvFile(releaseChannel);
|
|
305
|
+
process.env.NEOAGENT_RELEASE_CHANNEL = releaseChannel;
|
|
306
|
+
res.json({
|
|
307
|
+
ok: true,
|
|
308
|
+
releaseChannel,
|
|
309
|
+
targetBranch: getReleaseChannelBranchPolicy(releaseChannel),
|
|
310
|
+
npmDistTag: getReleaseChannelNpmPolicy(releaseChannel),
|
|
311
|
+
});
|
|
312
|
+
});
|
|
313
|
+
|
|
314
|
+
const settingsLimiter = rateLimit({
|
|
315
|
+
windowMs: 15 * 60 * 1000,
|
|
316
|
+
max: 20,
|
|
317
|
+
standardHeaders: true,
|
|
318
|
+
legacyHeaders: false,
|
|
319
|
+
message: { error: 'Too many settings changes, slow down' },
|
|
320
|
+
});
|
|
321
|
+
|
|
322
|
+
const sqlLimiter = rateLimit({
|
|
323
|
+
windowMs: 15 * 60 * 1000,
|
|
324
|
+
max: 60,
|
|
325
|
+
standardHeaders: true,
|
|
326
|
+
legacyHeaders: false,
|
|
327
|
+
message: { error: 'Too many SQL queries, slow down' },
|
|
328
|
+
});
|
|
329
|
+
|
|
330
|
+
// --- Access settings (signup toggle + API key) ---
|
|
331
|
+
|
|
332
|
+
router.get('/api/settings', requireAdminAuth, (req, res) => {
|
|
333
|
+
const apiKey = process.env.ADMIN_API_KEY || '';
|
|
334
|
+
const hint = apiKey.length > 8
|
|
335
|
+
? `${apiKey.slice(0, 4)}${'•'.repeat(4)}${apiKey.slice(-4)}`
|
|
336
|
+
: apiKey ? '•'.repeat(apiKey.length) : '';
|
|
337
|
+
const adminTwoFactor = require('../services/account/admin_two_factor');
|
|
338
|
+
const tfStatus = adminTwoFactor.getStatus();
|
|
339
|
+
res.json({
|
|
340
|
+
signupEnabled: process.env.NEOAGENT_ALLOW_SIGNUP !== 'false',
|
|
341
|
+
apiKeyConfigured: Boolean(apiKey),
|
|
342
|
+
apiKeyHint: hint,
|
|
343
|
+
twoFactor: tfStatus,
|
|
344
|
+
});
|
|
345
|
+
});
|
|
346
|
+
|
|
347
|
+
// --- 2FA management ---
|
|
348
|
+
|
|
349
|
+
router.get('/api/settings/2fa', requireAdminAuth, (req, res) => {
|
|
350
|
+
const adminTwoFactor = require('../services/account/admin_two_factor');
|
|
351
|
+
res.json(adminTwoFactor.getStatus());
|
|
352
|
+
});
|
|
353
|
+
|
|
354
|
+
router.post('/api/settings/2fa/setup', requireAdminAuth, settingsLimiter, async (req, res) => {
|
|
355
|
+
try {
|
|
356
|
+
const adminTwoFactor = require('../services/account/admin_two_factor');
|
|
357
|
+
const { otpauthUrl, manualKey } = adminTwoFactor.beginSetup();
|
|
358
|
+
const qrcode = require('qrcode');
|
|
359
|
+
const qrDataUrl = await qrcode.toDataURL(otpauthUrl, { width: 200, margin: 2 });
|
|
360
|
+
res.json({ qrDataUrl, manualKey });
|
|
361
|
+
} catch (err) {
|
|
362
|
+
res.status(err.statusCode || 500).json({ error: err.message });
|
|
363
|
+
}
|
|
364
|
+
});
|
|
365
|
+
|
|
366
|
+
router.post('/api/settings/2fa/enable', requireAdminAuth, settingsLimiter, express.json(), async (req, res) => {
|
|
367
|
+
try {
|
|
368
|
+
const adminTwoFactor = require('../services/account/admin_two_factor');
|
|
369
|
+
const { recoveryCodes } = await adminTwoFactor.enable(req.body?.code);
|
|
370
|
+
res.json({ ok: true, recoveryCodes });
|
|
371
|
+
} catch (err) {
|
|
372
|
+
res.status(err.statusCode || 500).json({ error: err.message });
|
|
373
|
+
}
|
|
374
|
+
});
|
|
375
|
+
|
|
376
|
+
router.delete('/api/settings/2fa', requireAdminAuth, settingsLimiter, express.json(), async (req, res) => {
|
|
377
|
+
try {
|
|
378
|
+
const adminTwoFactor = require('../services/account/admin_two_factor');
|
|
379
|
+
await adminTwoFactor.disable(req.body?.code);
|
|
380
|
+
res.json({ ok: true });
|
|
381
|
+
} catch (err) {
|
|
382
|
+
res.status(err.statusCode || 500).json({ error: err.message });
|
|
383
|
+
}
|
|
384
|
+
});
|
|
385
|
+
|
|
386
|
+
router.post('/api/settings/2fa/recovery-codes', requireAdminAuth, settingsLimiter, express.json(), async (req, res) => {
|
|
387
|
+
try {
|
|
388
|
+
const adminTwoFactor = require('../services/account/admin_two_factor');
|
|
389
|
+
const { recoveryCodes } = await adminTwoFactor.regenerateCodes(req.body?.code);
|
|
390
|
+
res.json({ ok: true, recoveryCodes });
|
|
391
|
+
} catch (err) {
|
|
392
|
+
res.status(err.statusCode || 500).json({ error: err.message });
|
|
393
|
+
}
|
|
394
|
+
});
|
|
395
|
+
|
|
396
|
+
router.put('/api/settings/signup', requireAdminAuth, settingsLimiter, express.json(), (req, res) => {
|
|
397
|
+
const enabled = req.body?.enabled !== false; // default true
|
|
398
|
+
const value = enabled ? 'true' : 'false';
|
|
399
|
+
upsertEnvValue(ENV_FILE, 'NEOAGENT_ALLOW_SIGNUP', value);
|
|
400
|
+
process.env.NEOAGENT_ALLOW_SIGNUP = value;
|
|
401
|
+
res.json({ ok: true, signupEnabled: enabled });
|
|
402
|
+
});
|
|
403
|
+
|
|
404
|
+
router.post('/api/settings/apikey/rotate', requireAdminAuth, settingsLimiter, (req, res) => {
|
|
405
|
+
const newKey = require('crypto').randomBytes(32).toString('hex');
|
|
406
|
+
upsertEnvValue(ENV_FILE, 'ADMIN_API_KEY', newKey);
|
|
407
|
+
process.env.ADMIN_API_KEY = newKey;
|
|
408
|
+
// Return the key once — it will not be shown again
|
|
409
|
+
res.json({ ok: true, apiKey: newKey });
|
|
410
|
+
});
|
|
411
|
+
|
|
412
|
+
router.delete('/api/settings/apikey', requireAdminAuth, settingsLimiter, (req, res) => {
|
|
413
|
+
upsertEnvValue(ENV_FILE, 'ADMIN_API_KEY', '');
|
|
414
|
+
delete process.env.ADMIN_API_KEY;
|
|
415
|
+
res.json({ ok: true });
|
|
416
|
+
});
|
|
417
|
+
|
|
418
|
+
// --- Analytics ---
|
|
419
|
+
|
|
420
|
+
router.get('/api/analytics', requireAdminAuth, (req, res) => {
|
|
421
|
+
const db = require('../db/database');
|
|
422
|
+
const now = new Date().toISOString();
|
|
423
|
+
const dayAgo = new Date(Date.now() - 86_400_000).toISOString();
|
|
424
|
+
const weekAgo = new Date(Date.now() - 7 * 86_400_000).toISOString();
|
|
425
|
+
try {
|
|
426
|
+
const stats = {
|
|
427
|
+
totalUsers: db.prepare('SELECT COUNT(*) AS n FROM users').get().n,
|
|
428
|
+
activeToday: db.prepare('SELECT COUNT(*) AS n FROM users WHERE last_login > ?').get(dayAgo).n,
|
|
429
|
+
newThisWeek: db.prepare('SELECT COUNT(*) AS n FROM users WHERE created_at > ?').get(weekAgo).n,
|
|
430
|
+
totalRuns: db.prepare('SELECT COUNT(*) AS n FROM agent_runs').get().n,
|
|
431
|
+
runsToday: db.prepare('SELECT COUNT(*) AS n FROM agent_runs WHERE created_at > ?').get(dayAgo).n,
|
|
432
|
+
totalTokens: db.prepare('SELECT COALESCE(SUM(total_tokens),0) AS n FROM agent_runs').get().n,
|
|
433
|
+
activeSessions:db.prepare('SELECT COUNT(*) AS n FROM user_sessions WHERE revoked_at IS NULL AND expires_at > ?').get(now).n,
|
|
434
|
+
totalStorage: db.prepare('SELECT COALESCE(SUM(byte_size),0) AS n FROM artifacts').get().n,
|
|
435
|
+
};
|
|
436
|
+
const topUsers = db.prepare(`
|
|
437
|
+
SELECT u.id, u.username, u.display_name,
|
|
438
|
+
COALESCE(r.runs, 0) AS runs,
|
|
439
|
+
COALESCE(r.tokens, 0) AS tokens,
|
|
440
|
+
COALESCE(a.storage, 0) AS storage
|
|
441
|
+
FROM users u
|
|
442
|
+
LEFT JOIN (
|
|
443
|
+
SELECT user_id,
|
|
444
|
+
COUNT(*) AS runs,
|
|
445
|
+
COALESCE(SUM(total_tokens),0) AS tokens
|
|
446
|
+
FROM agent_runs GROUP BY user_id
|
|
447
|
+
) r ON r.user_id = u.id
|
|
448
|
+
LEFT JOIN (
|
|
449
|
+
SELECT user_id, COALESCE(SUM(byte_size),0) AS storage
|
|
450
|
+
FROM artifacts GROUP BY user_id
|
|
451
|
+
) a ON a.user_id = u.id
|
|
452
|
+
ORDER BY runs DESC LIMIT 8
|
|
453
|
+
`).all();
|
|
454
|
+
const recentRuns = db.prepare(`
|
|
455
|
+
SELECT r.id, u.username, r.title, r.status, r.total_tokens,
|
|
456
|
+
r.created_at, r.completed_at
|
|
457
|
+
FROM agent_runs r
|
|
458
|
+
JOIN users u ON u.id = r.user_id
|
|
459
|
+
ORDER BY r.created_at DESC LIMIT 20
|
|
460
|
+
`).all();
|
|
461
|
+
res.json({ stats, topUsers, recentRuns });
|
|
462
|
+
} catch (err) {
|
|
463
|
+
res.status(500).json({ error: String(err.message || err) });
|
|
464
|
+
}
|
|
465
|
+
});
|
|
466
|
+
|
|
467
|
+
// --- User management ---
|
|
468
|
+
|
|
469
|
+
router.get('/api/users', requireAdminAuth, (req, res) => {
|
|
470
|
+
const db = require('../db/database');
|
|
471
|
+
const q = req.query.q ? `%${req.query.q}%` : null;
|
|
472
|
+
try {
|
|
473
|
+
const sql = `
|
|
474
|
+
SELECT u.id, u.username, u.display_name, u.email, u.email_verified_at,
|
|
475
|
+
u.created_at, u.last_login,
|
|
476
|
+
COALESCE(r.run_count, 0) AS run_count,
|
|
477
|
+
COALESCE(a.storage_bytes,0) AS storage_bytes
|
|
478
|
+
FROM users u
|
|
479
|
+
LEFT JOIN (
|
|
480
|
+
SELECT user_id, COUNT(*) AS run_count
|
|
481
|
+
FROM agent_runs GROUP BY user_id
|
|
482
|
+
) r ON r.user_id = u.id
|
|
483
|
+
LEFT JOIN (
|
|
484
|
+
SELECT user_id, COALESCE(SUM(byte_size),0) AS storage_bytes
|
|
485
|
+
FROM artifacts GROUP BY user_id
|
|
486
|
+
) a ON a.user_id = u.id
|
|
487
|
+
${q ? 'WHERE u.username LIKE ? OR u.email LIKE ?' : ''}
|
|
488
|
+
ORDER BY u.created_at DESC LIMIT 200`;
|
|
489
|
+
const users = q ? db.prepare(sql).all(q, q) : db.prepare(sql).all();
|
|
490
|
+
res.json({ users });
|
|
491
|
+
} catch (err) {
|
|
492
|
+
res.status(500).json({ error: String(err.message || err) });
|
|
493
|
+
}
|
|
494
|
+
});
|
|
495
|
+
|
|
496
|
+
router.delete('/api/users/:id', requireAdminAuth, (req, res) => {
|
|
497
|
+
const db = require('../db/database');
|
|
498
|
+
const { DATA_DIR } = require('../../runtime/paths');
|
|
499
|
+
const { id } = req.params;
|
|
500
|
+
if (!id) return res.status(400).json({ error: 'Missing user id' });
|
|
501
|
+
try {
|
|
502
|
+
// Collect artifact paths before deletion
|
|
503
|
+
const artifacts = db.prepare('SELECT storage_path FROM artifacts WHERE user_id = ?').all(id);
|
|
504
|
+
|
|
505
|
+
const erase = db.transaction((uid) => {
|
|
506
|
+
db.prepare('DELETE FROM conversation_messages WHERE conversation_id IN (SELECT id FROM conversations WHERE user_id = ?)').run(uid);
|
|
507
|
+
db.prepare('DELETE FROM conversations WHERE user_id = ?').run(uid);
|
|
508
|
+
db.prepare('DELETE FROM agent_steps WHERE run_id IN (SELECT id FROM agent_runs WHERE user_id = ?)').run(uid);
|
|
509
|
+
db.prepare('DELETE FROM agent_runs WHERE user_id = ?').run(uid);
|
|
510
|
+
db.prepare('DELETE FROM messages WHERE user_id = ?').run(uid);
|
|
511
|
+
db.prepare('DELETE FROM memories WHERE user_id = ?').run(uid);
|
|
512
|
+
db.prepare('DELETE FROM integration_connections WHERE user_id = ?').run(uid);
|
|
513
|
+
db.prepare('DELETE FROM platform_connections WHERE user_id = ?').run(uid);
|
|
514
|
+
db.prepare('DELETE FROM mcp_servers WHERE user_id = ?').run(uid);
|
|
515
|
+
db.prepare('DELETE FROM user_settings WHERE user_id = ?').run(uid);
|
|
516
|
+
db.prepare('DELETE FROM user_sessions WHERE user_id = ?').run(uid);
|
|
517
|
+
db.prepare('DELETE FROM desktop_companion_devices WHERE user_id = ?').run(uid);
|
|
518
|
+
db.prepare('DELETE FROM scheduled_tasks WHERE user_id = ?').run(uid);
|
|
519
|
+
db.prepare('DELETE FROM recording_sessions WHERE user_id = ?').run(uid);
|
|
520
|
+
db.prepare('DELETE FROM screen_history WHERE user_id = ?').run(uid);
|
|
521
|
+
db.prepare('DELETE FROM agents WHERE user_id = ?').run(uid);
|
|
522
|
+
db.prepare('DELETE FROM artifacts WHERE user_id = ?').run(uid);
|
|
523
|
+
db.prepare('DELETE FROM users WHERE id = ?').run(uid);
|
|
524
|
+
});
|
|
525
|
+
erase(id);
|
|
526
|
+
|
|
527
|
+
// Clean up artifact files on disk
|
|
528
|
+
for (const artifact of artifacts) {
|
|
529
|
+
try {
|
|
530
|
+
const abs = path.isAbsolute(artifact.storage_path)
|
|
531
|
+
? artifact.storage_path
|
|
532
|
+
: path.join(DATA_DIR, artifact.storage_path);
|
|
533
|
+
fs.rmSync(abs, { force: true });
|
|
534
|
+
} catch {}
|
|
535
|
+
}
|
|
536
|
+
try { fs.rmSync(path.join(DATA_DIR, 'artifacts', id), { recursive: true, force: true }); } catch {}
|
|
537
|
+
|
|
538
|
+
res.json({ ok: true });
|
|
539
|
+
} catch (err) {
|
|
540
|
+
res.status(500).json({ error: String(err.message || err) });
|
|
541
|
+
}
|
|
542
|
+
});
|
|
543
|
+
|
|
544
|
+
router.delete('/api/users/:id/sessions', requireAdminAuth, (req, res) => {
|
|
545
|
+
const db = require('../db/database');
|
|
546
|
+
const { id } = req.params;
|
|
547
|
+
try {
|
|
548
|
+
db.prepare('UPDATE user_sessions SET revoked_at = ? WHERE user_id = ?').run(new Date().toISOString(), id);
|
|
549
|
+
res.json({ ok: true });
|
|
550
|
+
} catch (err) {
|
|
551
|
+
res.status(500).json({ error: String(err.message || err) });
|
|
552
|
+
}
|
|
553
|
+
});
|
|
554
|
+
|
|
555
|
+
// --- SQL editor (read-only SELECT only) ---
|
|
556
|
+
|
|
557
|
+
const SQL_BLOCKED = /\b(INSERT|UPDATE|DELETE|DROP|CREATE|ALTER|ATTACH|DETACH|TRUNCATE|VACUUM|REINDEX|REPLACE|UPSERT|PRAGMA)\b/i;
|
|
558
|
+
|
|
559
|
+
router.post('/api/sql', requireAdminAuth, sqlLimiter, express.json(), (req, res) => {
|
|
560
|
+
const { query } = req.body || {};
|
|
561
|
+
if (!query || typeof query !== 'string') return res.status(400).json({ error: 'No query provided' });
|
|
562
|
+
const trimmed = query.trim();
|
|
563
|
+
if (!/^(SELECT|WITH)\b/i.test(trimmed)) return res.status(400).json({ error: 'Only SELECT (or WITH …) queries are allowed' });
|
|
564
|
+
if (SQL_BLOCKED.test(trimmed)) return res.status(400).json({ error: 'Query contains a blocked SQL keyword' });
|
|
565
|
+
try {
|
|
566
|
+
const db = require('../db/database');
|
|
567
|
+
const rows = db.prepare(trimmed).all();
|
|
568
|
+
const limited = rows.slice(0, 500);
|
|
569
|
+
const columns = limited.length ? Object.keys(limited[0]) : [];
|
|
570
|
+
res.json({ rows: limited, columns, truncated: rows.length > 500, total: rows.length });
|
|
571
|
+
} catch (err) {
|
|
572
|
+
res.status(400).json({ error: String(err.message || err) });
|
|
573
|
+
}
|
|
574
|
+
});
|
|
575
|
+
|
|
576
|
+
// --- Providers ---
|
|
577
|
+
|
|
578
|
+
const PROVIDERS = [
|
|
579
|
+
{ key: 'ANTHROPIC_API_KEY', label: 'Anthropic (Claude)', type: 'key' },
|
|
580
|
+
{ key: 'OPENAI_API_KEY', label: 'OpenAI', type: 'key' },
|
|
581
|
+
{ key: 'XAI_API_KEY', label: 'xAI (Grok)', type: 'key' },
|
|
582
|
+
{ key: 'GOOGLE_AI_KEY', label: 'Google (Gemini)', type: 'key' },
|
|
583
|
+
{ key: 'MINIMAX_API_KEY', label: 'MiniMax', type: 'key' },
|
|
584
|
+
{ key: 'NVIDIA_API_KEY', label: 'NVIDIA NIM', type: 'key' },
|
|
585
|
+
{ key: 'OPENROUTER_API_KEY', label: 'OpenRouter', type: 'key' },
|
|
586
|
+
{ key: 'BRAVE_SEARCH_API_KEY', label: 'Brave Search', type: 'key' },
|
|
587
|
+
{ key: 'DEEPGRAM_API_KEY', label: 'Deepgram (Voice)', type: 'key' },
|
|
588
|
+
{ key: 'OLLAMA_URL', label: 'Ollama (Local)', type: 'url' },
|
|
589
|
+
];
|
|
590
|
+
|
|
591
|
+
const ALLOWED_PROVIDER_KEYS = new Set(PROVIDERS.map((p) => p.key));
|
|
592
|
+
|
|
593
|
+
router.get('/api/providers', requireAdminAuth, (req, res) => {
|
|
594
|
+
const result = PROVIDERS.map(({ key, label, type }) => {
|
|
595
|
+
const value = process.env[key] || '';
|
|
596
|
+
let hint = '';
|
|
597
|
+
if (value) {
|
|
598
|
+
hint = type === 'url'
|
|
599
|
+
? value
|
|
600
|
+
: value.length > 8
|
|
601
|
+
? `${value.slice(0, 4)}${'•'.repeat(4)}${value.slice(-4)}`
|
|
602
|
+
: '•'.repeat(value.length);
|
|
603
|
+
}
|
|
604
|
+
return { key, label, type, configured: Boolean(value), hint };
|
|
605
|
+
});
|
|
606
|
+
res.json({ providers: result });
|
|
607
|
+
});
|
|
608
|
+
|
|
609
|
+
router.put('/api/providers', requireAdminAuth, express.json(), (req, res) => {
|
|
610
|
+
const { key, value } = req.body || {};
|
|
611
|
+
if (!ALLOWED_PROVIDER_KEYS.has(key)) {
|
|
612
|
+
return res.status(400).json({ error: 'Unknown provider key' });
|
|
613
|
+
}
|
|
614
|
+
const trimmed = String(value || '').trim().replace(/[\r\n]/g, '');
|
|
615
|
+
upsertEnvValue(ENV_FILE, key, trimmed);
|
|
616
|
+
if (trimmed) {
|
|
617
|
+
process.env[key] = trimmed;
|
|
618
|
+
} else {
|
|
619
|
+
delete process.env[key];
|
|
620
|
+
}
|
|
621
|
+
res.json({ ok: true });
|
|
622
|
+
});
|
|
623
|
+
|
|
624
|
+
// --- Static files ---
|
|
625
|
+
|
|
626
|
+
router.use(express.static(ADMIN_DIR));
|
|
627
|
+
|
|
628
|
+
router.get('*', requireAdminAuth, (req, res) => {
|
|
629
|
+
res.sendFile(path.join(ADMIN_DIR, 'index.html'));
|
|
630
|
+
});
|
|
631
|
+
|
|
632
|
+
module.exports = router;
|
|
@@ -41,6 +41,7 @@ router.put('/:id', (req, res) => {
|
|
|
41
41
|
try {
|
|
42
42
|
res.json(updateAgent(req.session.userId, req.params.id, req.body || {}));
|
|
43
43
|
} catch (err) {
|
|
44
|
+
if (err.message === 'Agent not found.') return res.status(404).json({ error: err.message });
|
|
44
45
|
res.status(400).json({ error: sanitizeError(err) });
|
|
45
46
|
}
|
|
46
47
|
});
|
|
@@ -49,6 +50,7 @@ router.post('/:id/default', (req, res) => {
|
|
|
49
50
|
try {
|
|
50
51
|
res.json(setDefaultAgent(req.session.userId, req.params.id));
|
|
51
52
|
} catch (err) {
|
|
53
|
+
if (err.message === 'Agent not found.') return res.status(404).json({ error: err.message });
|
|
52
54
|
res.status(400).json({ error: sanitizeError(err) });
|
|
53
55
|
}
|
|
54
56
|
});
|
|
@@ -57,6 +59,7 @@ router.delete('/:id', (req, res) => {
|
|
|
57
59
|
try {
|
|
58
60
|
res.json(archiveAgent(req.session.userId, req.params.id));
|
|
59
61
|
} catch (err) {
|
|
62
|
+
if (err.message === 'Agent not found.') return res.status(404).json({ error: err.message });
|
|
60
63
|
res.status(400).json({ error: sanitizeError(err) });
|
|
61
64
|
}
|
|
62
65
|
});
|
package/server/routes/agents.js
CHANGED
|
@@ -186,7 +186,7 @@ router.post('/:id/abort', (req, res) => {
|
|
|
186
186
|
const run = db.prepare('SELECT id FROM agent_runs WHERE id = ? AND user_id = ?').get(req.params.id, req.session.userId);
|
|
187
187
|
if (!run) return res.status(404).json({ error: 'Run not found' });
|
|
188
188
|
const engine = req.app.locals.agentEngine;
|
|
189
|
-
engine.abort(req.params.id);
|
|
189
|
+
engine.abort(req.params.id, { userId: req.session.userId });
|
|
190
190
|
res.json({ success: true });
|
|
191
191
|
} catch (err) {
|
|
192
192
|
res.status(500).json({ error: sanitizeError(err) });
|
package/server/routes/auth.js
CHANGED
|
@@ -129,7 +129,9 @@ function readAuthenticatedUser(req) {
|
|
|
129
129
|
if (!user) {
|
|
130
130
|
try {
|
|
131
131
|
req.session.destroy(() => {});
|
|
132
|
-
} catch (
|
|
132
|
+
} catch (destroyError) {
|
|
133
|
+
console.warn('Auth stale-session cleanup failed:', destroyError.message);
|
|
134
|
+
}
|
|
133
135
|
return null;
|
|
134
136
|
}
|
|
135
137
|
return user;
|