neoagent 1.0.0
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 +28 -0
- package/LICENSE +21 -0
- package/README.md +42 -0
- package/bin/neoagent.js +8 -0
- package/com.neoagent.plist +45 -0
- package/docs/configuration.md +45 -0
- package/docs/skills.md +45 -0
- package/lib/manager.js +459 -0
- package/package.json +61 -0
- package/server/db/database.js +239 -0
- package/server/index.js +442 -0
- package/server/middleware/auth.js +35 -0
- package/server/public/app.html +559 -0
- package/server/public/css/app.css +608 -0
- package/server/public/css/styles.css +472 -0
- package/server/public/favicon.svg +17 -0
- package/server/public/js/app.js +3283 -0
- package/server/public/login.html +313 -0
- package/server/routes/agents.js +125 -0
- package/server/routes/auth.js +105 -0
- package/server/routes/browser.js +116 -0
- package/server/routes/mcp.js +164 -0
- package/server/routes/memory.js +193 -0
- package/server/routes/messaging.js +153 -0
- package/server/routes/protocols.js +87 -0
- package/server/routes/scheduler.js +63 -0
- package/server/routes/settings.js +98 -0
- package/server/routes/skills.js +107 -0
- package/server/routes/store.js +1192 -0
- package/server/services/ai/compaction.js +82 -0
- package/server/services/ai/engine.js +1690 -0
- package/server/services/ai/models.js +46 -0
- package/server/services/ai/multiStep.js +112 -0
- package/server/services/ai/providers/anthropic.js +181 -0
- package/server/services/ai/providers/base.js +40 -0
- package/server/services/ai/providers/google.js +187 -0
- package/server/services/ai/providers/grok.js +121 -0
- package/server/services/ai/providers/ollama.js +162 -0
- package/server/services/ai/providers/openai.js +167 -0
- package/server/services/ai/toolRunner.js +218 -0
- package/server/services/browser/controller.js +320 -0
- package/server/services/cli/executor.js +204 -0
- package/server/services/mcp/client.js +260 -0
- package/server/services/memory/embeddings.js +126 -0
- package/server/services/memory/manager.js +431 -0
- package/server/services/messaging/base.js +23 -0
- package/server/services/messaging/discord.js +238 -0
- package/server/services/messaging/manager.js +328 -0
- package/server/services/messaging/telegram.js +243 -0
- package/server/services/messaging/telnyx.js +693 -0
- package/server/services/messaging/whatsapp.js +304 -0
- package/server/services/scheduler/cron.js +312 -0
- package/server/services/websocket.js +191 -0
- package/server/utils/security.js +71 -0
|
@@ -0,0 +1,313 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8">
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
6
|
+
<title>NeoAgent — Login</title>
|
|
7
|
+
<link rel="icon" type="image/svg+xml" href="/favicon.svg">
|
|
8
|
+
<link rel="preconnect" href="https://fonts.googleapis.com">
|
|
9
|
+
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
|
10
|
+
<link rel="stylesheet" href="/css/styles.css">
|
|
11
|
+
<style>
|
|
12
|
+
body {
|
|
13
|
+
display: flex;
|
|
14
|
+
align-items: center;
|
|
15
|
+
justify-content: center;
|
|
16
|
+
min-height: 100vh;
|
|
17
|
+
background: var(--bg-primary);
|
|
18
|
+
overflow: hidden;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
.login-bg {
|
|
22
|
+
position: fixed;
|
|
23
|
+
inset: 0;
|
|
24
|
+
overflow: hidden;
|
|
25
|
+
z-index: 0;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
.login-bg .orb {
|
|
29
|
+
position: absolute;
|
|
30
|
+
border-radius: 50%;
|
|
31
|
+
filter: blur(120px);
|
|
32
|
+
opacity: 0.15;
|
|
33
|
+
animation: float 20s ease-in-out infinite;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
.login-bg .orb-1 {
|
|
37
|
+
width: 500px; height: 500px;
|
|
38
|
+
background: var(--accent);
|
|
39
|
+
top: -100px; left: -100px;
|
|
40
|
+
animation-delay: 0s;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
.login-bg .orb-2 {
|
|
44
|
+
width: 400px; height: 400px;
|
|
45
|
+
background: #8b5cf6;
|
|
46
|
+
bottom: -80px; right: -80px;
|
|
47
|
+
animation-delay: -7s;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
.login-bg .orb-3 {
|
|
51
|
+
width: 300px; height: 300px;
|
|
52
|
+
background: #06b6d4;
|
|
53
|
+
top: 50%; left: 60%;
|
|
54
|
+
animation-delay: -14s;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
@keyframes float {
|
|
58
|
+
0%, 100% { transform: translate(0, 0) scale(1); }
|
|
59
|
+
33% { transform: translate(30px, -30px) scale(1.05); }
|
|
60
|
+
66% { transform: translate(-20px, 20px) scale(0.95); }
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
.login-card {
|
|
64
|
+
position: relative; z-index: 1;
|
|
65
|
+
width: 100%; max-width: 400px;
|
|
66
|
+
background: rgba(12,12,24,.92);
|
|
67
|
+
backdrop-filter: blur(24px) saturate(1.4);
|
|
68
|
+
-webkit-backdrop-filter: blur(24px) saturate(1.4);
|
|
69
|
+
border: 1px solid rgba(255,255,255,.1);
|
|
70
|
+
border-radius: 20px; padding: 40px;
|
|
71
|
+
box-shadow: 0 20px 60px rgba(0,0,0,.6);
|
|
72
|
+
animation: cardIn 400ms cubic-bezier(.16,1,.3,1) both;
|
|
73
|
+
}
|
|
74
|
+
@keyframes cardIn {
|
|
75
|
+
from { opacity: 0; transform: translateY(20px) scale(.97); }
|
|
76
|
+
to { opacity: 1; transform: translateY(0) scale(1); }
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
.login-logo {
|
|
80
|
+
text-align: center;
|
|
81
|
+
margin-bottom: 32px;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
.login-logo .logo-icon {
|
|
85
|
+
width: 52px; height: 52px;
|
|
86
|
+
background: linear-gradient(135deg, var(--accent), #8b5cf6);
|
|
87
|
+
border-radius: 14px;
|
|
88
|
+
display: inline-flex; align-items: center; justify-content: center;
|
|
89
|
+
margin-bottom: 16px;
|
|
90
|
+
box-shadow: 0 4px 24px rgba(99,102,241,.45);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
.login-logo .logo-icon svg {
|
|
94
|
+
width: 28px;
|
|
95
|
+
height: 28px;
|
|
96
|
+
stroke: white;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
.login-logo h1 {
|
|
100
|
+
font-size: 24px;
|
|
101
|
+
font-weight: 700;
|
|
102
|
+
margin-bottom: 4px;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
.login-logo p {
|
|
106
|
+
color: var(--text-secondary);
|
|
107
|
+
font-size: 14px;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
.login-tabs {
|
|
111
|
+
display: flex;
|
|
112
|
+
margin-bottom: 24px;
|
|
113
|
+
background: var(--bg-primary);
|
|
114
|
+
border-radius: var(--radius);
|
|
115
|
+
padding: 3px;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
.login-tab {
|
|
119
|
+
flex: 1;
|
|
120
|
+
text-align: center;
|
|
121
|
+
padding: 8px;
|
|
122
|
+
cursor: pointer;
|
|
123
|
+
border-radius: 6px;
|
|
124
|
+
font-weight: 500;
|
|
125
|
+
font-size: 13px;
|
|
126
|
+
color: var(--text-secondary);
|
|
127
|
+
transition: all var(--transition);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
.login-tab.active {
|
|
131
|
+
background: var(--accent);
|
|
132
|
+
color: white;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
.login-form { display: none; }
|
|
136
|
+
.login-form.active { display: block; }
|
|
137
|
+
|
|
138
|
+
.login-form .btn {
|
|
139
|
+
width: 100%;
|
|
140
|
+
justify-content: center;
|
|
141
|
+
padding: 12px;
|
|
142
|
+
margin-top: 8px;
|
|
143
|
+
font-size: 14px;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
.login-error {
|
|
147
|
+
background: rgba(239, 68, 68, 0.1);
|
|
148
|
+
border: 1px solid rgba(239, 68, 68, 0.3);
|
|
149
|
+
color: var(--error);
|
|
150
|
+
padding: 10px 14px;
|
|
151
|
+
border-radius: var(--radius);
|
|
152
|
+
font-size: 13px;
|
|
153
|
+
margin-bottom: 16px;
|
|
154
|
+
display: none;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
.login-error.show { display: block; }
|
|
158
|
+
</style>
|
|
159
|
+
</head>
|
|
160
|
+
<body>
|
|
161
|
+
<div class="login-bg">
|
|
162
|
+
<div class="orb orb-1"></div>
|
|
163
|
+
<div class="orb orb-2"></div>
|
|
164
|
+
<div class="orb orb-3"></div>
|
|
165
|
+
</div>
|
|
166
|
+
|
|
167
|
+
<div class="login-card">
|
|
168
|
+
<div class="login-logo">
|
|
169
|
+
<div class="logo-icon">
|
|
170
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke-linecap="round" stroke-linejoin="round">
|
|
171
|
+
<polygon points="12,2 2,7 12,12 22,7" fill="white" stroke="none"/>
|
|
172
|
+
<polyline points="2,17 12,22 22,17" fill="none" stroke="white" stroke-width="2"/>
|
|
173
|
+
<polyline points="2,12 12,17 22,12" fill="none" stroke="white" stroke-width="2"/>
|
|
174
|
+
</svg>
|
|
175
|
+
</div>
|
|
176
|
+
<h1>NeoAgent</h1>
|
|
177
|
+
<p>Your proactive AI agent</p>
|
|
178
|
+
</div>
|
|
179
|
+
|
|
180
|
+
<div class="login-tabs" id="tabBar" style="display:none">
|
|
181
|
+
<div class="login-tab active" data-tab="login">Sign In</div>
|
|
182
|
+
<div class="login-tab" data-tab="register" id="registerTab" style="display:none">Setup</div>
|
|
183
|
+
</div>
|
|
184
|
+
|
|
185
|
+
<div class="login-error" id="error"></div>
|
|
186
|
+
|
|
187
|
+
<form class="login-form active" id="loginForm" data-tab="login">
|
|
188
|
+
<div class="form-group">
|
|
189
|
+
<label class="form-label">Username</label>
|
|
190
|
+
<input type="text" class="input" name="username" autocomplete="username" required autofocus>
|
|
191
|
+
</div>
|
|
192
|
+
<div class="form-group">
|
|
193
|
+
<label class="form-label">Password</label>
|
|
194
|
+
<input type="password" class="input" name="password" autocomplete="current-password" required>
|
|
195
|
+
</div>
|
|
196
|
+
<button type="submit" class="btn btn-primary">Sign In</button>
|
|
197
|
+
</form>
|
|
198
|
+
|
|
199
|
+
<form class="login-form" id="registerForm" data-tab="register">
|
|
200
|
+
<div class="form-group">
|
|
201
|
+
<label class="form-label">Username</label>
|
|
202
|
+
<input type="text" class="input" name="username" autocomplete="username" required minlength="3">
|
|
203
|
+
</div>
|
|
204
|
+
<div class="form-group">
|
|
205
|
+
<label class="form-label">Password</label>
|
|
206
|
+
<input type="password" class="input" name="password" autocomplete="new-password" required minlength="8">
|
|
207
|
+
</div>
|
|
208
|
+
<div class="form-group">
|
|
209
|
+
<label class="form-label">Confirm Password</label>
|
|
210
|
+
<input type="password" class="input" name="confirmPassword" autocomplete="new-password" required>
|
|
211
|
+
</div>
|
|
212
|
+
<button type="submit" class="btn btn-primary">Create Account</button>
|
|
213
|
+
</form>
|
|
214
|
+
</div>
|
|
215
|
+
|
|
216
|
+
<script>
|
|
217
|
+
const tabs = document.querySelectorAll('.login-tab');
|
|
218
|
+
const forms = document.querySelectorAll('.login-form');
|
|
219
|
+
const errorEl = document.getElementById('error');
|
|
220
|
+
|
|
221
|
+
function switchTab(target) {
|
|
222
|
+
tabs.forEach(t => t.classList.toggle('active', t.dataset.tab === target));
|
|
223
|
+
forms.forEach(f => f.classList.toggle('active', f.dataset.tab === target));
|
|
224
|
+
errorEl.classList.remove('show');
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
tabs.forEach(tab => tab.addEventListener('click', () => switchTab(tab.dataset.tab)));
|
|
228
|
+
|
|
229
|
+
// Show tab bar + setup tab only if no user exists yet; otherwise hide the whole bar
|
|
230
|
+
fetch('/api/auth/status', { credentials: 'include' }).then(r => r.json()).then(data => {
|
|
231
|
+
if (!data.hasUser) {
|
|
232
|
+
document.getElementById('registerTab').style.display = '';
|
|
233
|
+
document.getElementById('tabBar').style.display = '';
|
|
234
|
+
switchTab('register');
|
|
235
|
+
}
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
function showError(msg) {
|
|
239
|
+
errorEl.textContent = msg;
|
|
240
|
+
errorEl.classList.add('show');
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
document.getElementById('loginForm').addEventListener('submit', async (e) => {
|
|
244
|
+
e.preventDefault();
|
|
245
|
+
const form = e.target;
|
|
246
|
+
const btn = form.querySelector('button');
|
|
247
|
+
btn.disabled = true;
|
|
248
|
+
btn.textContent = 'Signing in...';
|
|
249
|
+
errorEl.classList.remove('show');
|
|
250
|
+
|
|
251
|
+
try {
|
|
252
|
+
const res = await fetch('/api/auth/login', {
|
|
253
|
+
method: 'POST',
|
|
254
|
+
headers: { 'Content-Type': 'application/json' },
|
|
255
|
+
credentials: 'include',
|
|
256
|
+
body: JSON.stringify({
|
|
257
|
+
username: form.username.value,
|
|
258
|
+
password: form.password.value
|
|
259
|
+
})
|
|
260
|
+
});
|
|
261
|
+
const data = await res.json();
|
|
262
|
+
if (res.ok) {
|
|
263
|
+
window.location.href = data.redirect || '/app';
|
|
264
|
+
} else {
|
|
265
|
+
showError(data.error || 'Login failed');
|
|
266
|
+
}
|
|
267
|
+
} catch (err) {
|
|
268
|
+
showError('Connection error');
|
|
269
|
+
} finally {
|
|
270
|
+
btn.disabled = false;
|
|
271
|
+
btn.textContent = 'Sign In';
|
|
272
|
+
}
|
|
273
|
+
});
|
|
274
|
+
|
|
275
|
+
document.getElementById('registerForm').addEventListener('submit', async (e) => {
|
|
276
|
+
e.preventDefault();
|
|
277
|
+
const form = e.target;
|
|
278
|
+
const btn = form.querySelector('button');
|
|
279
|
+
|
|
280
|
+
if (form.password.value !== form.confirmPassword.value) {
|
|
281
|
+
return showError('Passwords do not match');
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
btn.disabled = true;
|
|
285
|
+
btn.textContent = 'Creating account...';
|
|
286
|
+
errorEl.classList.remove('show');
|
|
287
|
+
|
|
288
|
+
try {
|
|
289
|
+
const res = await fetch('/api/auth/register', {
|
|
290
|
+
method: 'POST',
|
|
291
|
+
headers: { 'Content-Type': 'application/json' },
|
|
292
|
+
credentials: 'include',
|
|
293
|
+
body: JSON.stringify({
|
|
294
|
+
username: form.username.value,
|
|
295
|
+
password: form.password.value
|
|
296
|
+
})
|
|
297
|
+
});
|
|
298
|
+
const data = await res.json();
|
|
299
|
+
if (res.ok) {
|
|
300
|
+
window.location.href = data.redirect || '/app';
|
|
301
|
+
} else {
|
|
302
|
+
showError(data.error || 'Setup failed');
|
|
303
|
+
}
|
|
304
|
+
} catch (err) {
|
|
305
|
+
showError('Connection error');
|
|
306
|
+
} finally {
|
|
307
|
+
btn.disabled = false;
|
|
308
|
+
btn.textContent = 'Create Account';
|
|
309
|
+
}
|
|
310
|
+
});
|
|
311
|
+
</script>
|
|
312
|
+
</body>
|
|
313
|
+
</html>
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
const express = require('express');
|
|
2
|
+
const router = express.Router();
|
|
3
|
+
const db = require('../db/database');
|
|
4
|
+
const { requireAuth } = require('../middleware/auth');
|
|
5
|
+
const { sanitizeError } = require('../utils/security');
|
|
6
|
+
|
|
7
|
+
router.use(requireAuth);
|
|
8
|
+
|
|
9
|
+
// List agent runs
|
|
10
|
+
router.get('/', (req, res) => {
|
|
11
|
+
const limit = Math.min(parseInt(req.query.limit) || 50, 200);
|
|
12
|
+
const offset = parseInt(req.query.offset) || 0;
|
|
13
|
+
const runs = db.prepare('SELECT * FROM agent_runs WHERE user_id = ? ORDER BY created_at DESC LIMIT ? OFFSET ?')
|
|
14
|
+
.all(req.session.userId, limit, offset);
|
|
15
|
+
const total = db.prepare('SELECT COUNT(*) as count FROM agent_runs WHERE user_id = ?').get(req.session.userId).count;
|
|
16
|
+
res.json({ runs, total, limit, offset });
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
// Chat history (web + social messages merged)
|
|
20
|
+
router.get('/chat-history', (req, res) => {
|
|
21
|
+
const limit = Math.min(parseInt(req.query.limit) || 100, 500);
|
|
22
|
+
const userId = req.session.userId;
|
|
23
|
+
|
|
24
|
+
const webMsgs = db.prepare(`
|
|
25
|
+
SELECT id, role, content, 'web' AS platform, NULL AS sender_name, created_at, agent_run_id AS run_id
|
|
26
|
+
FROM conversation_history WHERE user_id = ? ORDER BY created_at DESC LIMIT ?
|
|
27
|
+
`).all(userId, limit);
|
|
28
|
+
|
|
29
|
+
const socialMsgs = db.prepare(`
|
|
30
|
+
SELECT id, role, content, platform,
|
|
31
|
+
json_extract(metadata, '$.senderName') AS sender_name, created_at, run_id
|
|
32
|
+
FROM messages WHERE user_id = ? AND platform != 'web'
|
|
33
|
+
ORDER BY created_at DESC LIMIT ?
|
|
34
|
+
`).all(userId, limit);
|
|
35
|
+
|
|
36
|
+
// Normalize SQL datetime ('YYYY-MM-DD HH:MM:SS', treated as local) and ISO-Z strings to ms
|
|
37
|
+
const toMs = (s) => {
|
|
38
|
+
if (!s) return 0;
|
|
39
|
+
const normalized = s.includes('T') ? s : s.replace(' ', 'T') + 'Z';
|
|
40
|
+
return new Date(normalized).getTime();
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
const all = [...webMsgs, ...socialMsgs]
|
|
44
|
+
.sort((a, b) => toMs(a.created_at) - toMs(b.created_at))
|
|
45
|
+
.slice(-limit);
|
|
46
|
+
|
|
47
|
+
res.json({ messages: all });
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
// Create new agent run
|
|
51
|
+
router.post('/', async (req, res) => {
|
|
52
|
+
try {
|
|
53
|
+
const { task, options } = req.body;
|
|
54
|
+
if (!task || typeof task !== 'string') return res.status(400).json({ error: 'Task must be a non-empty string' });
|
|
55
|
+
if (task.length > 50000) return res.status(400).json({ error: 'Task exceeds maximum length of 50,000 characters' });
|
|
56
|
+
|
|
57
|
+
const engine = req.app.locals.agentEngine;
|
|
58
|
+
const result = await engine.run(req.session.userId, task, options || {});
|
|
59
|
+
res.json(result);
|
|
60
|
+
} catch (err) {
|
|
61
|
+
res.status(500).json({ error: sanitizeError(err) });
|
|
62
|
+
}
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
// Get specific run
|
|
66
|
+
router.get('/:id', (req, res) => {
|
|
67
|
+
const run = db.prepare('SELECT * FROM agent_runs WHERE id = ? AND user_id = ?').get(req.params.id, req.session.userId);
|
|
68
|
+
if (!run) return res.status(404).json({ error: 'Run not found' });
|
|
69
|
+
|
|
70
|
+
const steps = db.prepare('SELECT * FROM agent_steps WHERE run_id = ? ORDER BY step_index ASC').all(run.id);
|
|
71
|
+
const history = db.prepare('SELECT * FROM conversation_history WHERE agent_run_id = ? ORDER BY created_at ASC').all(run.id);
|
|
72
|
+
|
|
73
|
+
res.json({ run, steps, history });
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
// Get detailed steps for a run (for activity history replay)
|
|
77
|
+
router.get('/:id/steps', (req, res) => {
|
|
78
|
+
const run = db.prepare('SELECT * FROM agent_runs WHERE id = ? AND user_id = ?').get(req.params.id, req.session.userId);
|
|
79
|
+
if (!run) return res.status(404).json({ error: 'Run not found' });
|
|
80
|
+
|
|
81
|
+
const steps = db.prepare('SELECT * FROM agent_steps WHERE run_id = ? ORDER BY step_index ASC').all(run.id);
|
|
82
|
+
const response = db.prepare(
|
|
83
|
+
`SELECT content FROM conversation_history WHERE user_id = ? AND agent_run_id = ? AND role = 'assistant' ORDER BY created_at DESC LIMIT 1`
|
|
84
|
+
).get(req.session.userId, run.id);
|
|
85
|
+
|
|
86
|
+
res.json({ run, steps, response: response?.content || null });
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
// Abort a run
|
|
90
|
+
router.post('/:id/abort', (req, res) => {
|
|
91
|
+
try {
|
|
92
|
+
const engine = req.app.locals.agentEngine;
|
|
93
|
+
engine.abort(req.params.id);
|
|
94
|
+
res.json({ success: true });
|
|
95
|
+
} catch (err) {
|
|
96
|
+
res.status(500).json({ error: sanitizeError(err) });
|
|
97
|
+
}
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
// Delete a run
|
|
101
|
+
router.delete('/:id', (req, res) => {
|
|
102
|
+
const run = db.prepare('SELECT id FROM agent_runs WHERE id = ? AND user_id = ?').get(req.params.id, req.session.userId);
|
|
103
|
+
if (!run) return res.status(404).json({ error: 'Run not found' });
|
|
104
|
+
|
|
105
|
+
db.prepare('DELETE FROM agent_steps WHERE run_id = ?').run(run.id);
|
|
106
|
+
db.prepare('DELETE FROM conversation_history WHERE agent_run_id = ?').run(run.id);
|
|
107
|
+
db.prepare('DELETE FROM agent_runs WHERE id = ?').run(run.id);
|
|
108
|
+
res.json({ success: true });
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
// Multi-step task
|
|
112
|
+
router.post('/multi-step', async (req, res) => {
|
|
113
|
+
try {
|
|
114
|
+
const { task, steps, options } = req.body;
|
|
115
|
+
if (!task) return res.status(400).json({ error: 'Task is required' });
|
|
116
|
+
|
|
117
|
+
const multiStep = req.app.locals.multiStep;
|
|
118
|
+
const result = await multiStep.create(req.session.userId, task, steps || [], options || {});
|
|
119
|
+
res.json(result);
|
|
120
|
+
} catch (err) {
|
|
121
|
+
res.status(500).json({ error: sanitizeError(err) });
|
|
122
|
+
}
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
module.exports = router;
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
const express = require('express');
|
|
2
|
+
const router = express.Router();
|
|
3
|
+
const bcrypt = require('bcrypt');
|
|
4
|
+
const rateLimit = require('express-rate-limit');
|
|
5
|
+
const db = require('../db/database');
|
|
6
|
+
const { requireNoAuth } = require('../middleware/auth');
|
|
7
|
+
|
|
8
|
+
const authLimiter = rateLimit({
|
|
9
|
+
windowMs: 15 * 60 * 1000,
|
|
10
|
+
max: 20,
|
|
11
|
+
message: { error: 'Too many attempts, try again later' },
|
|
12
|
+
standardHeaders: true,
|
|
13
|
+
legacyHeaders: false
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
router.get('/api/auth/status', (req, res) => {
|
|
17
|
+
const count = db.prepare('SELECT COUNT(*) as count FROM users').get();
|
|
18
|
+
res.json({ hasUser: count.count > 0 });
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
router.post('/api/auth/register', authLimiter, async (req, res) => {
|
|
22
|
+
try {
|
|
23
|
+
const userCount = db.prepare('SELECT COUNT(*) as count FROM users').get();
|
|
24
|
+
if (userCount.count > 0) {
|
|
25
|
+
return res.status(403).json({ error: 'Registration is closed' });
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const { username, password } = req.body;
|
|
29
|
+
if (!username || !password) {
|
|
30
|
+
return res.status(400).json({ error: 'Username and password required' });
|
|
31
|
+
}
|
|
32
|
+
if (username.length < 3 || password.length < 8) {
|
|
33
|
+
return res.status(400).json({ error: 'Username min 3 chars, password min 8' });
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const hash = await bcrypt.hash(password, 12);
|
|
37
|
+
const result = db.prepare('INSERT INTO users (username, password) VALUES (?, ?)').run(username, hash);
|
|
38
|
+
|
|
39
|
+
req.session.regenerate((err) => {
|
|
40
|
+
if (err) return res.status(500).json({ error: 'Session error' });
|
|
41
|
+
req.session.userId = result.lastInsertRowid;
|
|
42
|
+
req.session.username = username;
|
|
43
|
+
req.session.save((err) => {
|
|
44
|
+
if (err) return res.status(500).json({ error: 'Session save error' });
|
|
45
|
+
res.json({ success: true, redirect: '/app', user: { id: result.lastInsertRowid, username } });
|
|
46
|
+
});
|
|
47
|
+
});
|
|
48
|
+
} catch (err) {
|
|
49
|
+
console.error('Register error:', err);
|
|
50
|
+
res.status(500).json({ error: 'Registration failed' });
|
|
51
|
+
}
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
router.post('/api/auth/login', authLimiter, async (req, res) => {
|
|
55
|
+
try {
|
|
56
|
+
const { username, password } = req.body;
|
|
57
|
+
if (!username || !password) {
|
|
58
|
+
return res.status(400).json({ error: 'Username and password required' });
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const user = db.prepare('SELECT * FROM users WHERE username = ?').get(username);
|
|
62
|
+
if (!user) {
|
|
63
|
+
return res.status(401).json({ error: 'Invalid credentials' });
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const match = await bcrypt.compare(password, user.password);
|
|
67
|
+
if (!match) {
|
|
68
|
+
return res.status(401).json({ error: 'Invalid credentials' });
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
db.prepare('UPDATE users SET last_login = datetime(\'now\') WHERE id = ?').run(user.id);
|
|
72
|
+
|
|
73
|
+
req.session.regenerate((err) => {
|
|
74
|
+
if (err) return res.status(500).json({ error: 'Session error' });
|
|
75
|
+
req.session.userId = user.id;
|
|
76
|
+
req.session.username = user.username;
|
|
77
|
+
req.session.save((err) => {
|
|
78
|
+
if (err) return res.status(500).json({ error: 'Session save error' });
|
|
79
|
+
res.json({ success: true, redirect: '/app', user: { id: user.id, username: user.username } });
|
|
80
|
+
});
|
|
81
|
+
});
|
|
82
|
+
} catch (err) {
|
|
83
|
+
console.error('Login error:', err);
|
|
84
|
+
res.status(500).json({ error: 'Login failed' });
|
|
85
|
+
}
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
router.post('/api/auth/logout', (req, res) => {
|
|
89
|
+
req.session.destroy((err) => {
|
|
90
|
+
if (err) return res.status(500).json({ error: 'Logout failed' });
|
|
91
|
+
res.clearCookie('neoagent.sid');
|
|
92
|
+
res.json({ success: true });
|
|
93
|
+
});
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
router.get('/api/auth/me', (req, res) => {
|
|
97
|
+
if (!req.session || !req.session.userId) {
|
|
98
|
+
return res.status(401).json({ error: 'Not authenticated' });
|
|
99
|
+
}
|
|
100
|
+
const user = db.prepare('SELECT id, username, email, created_at, last_login FROM users WHERE id = ?').get(req.session.userId);
|
|
101
|
+
if (!user) return res.status(401).json({ error: 'User not found' });
|
|
102
|
+
res.json({ user });
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
module.exports = router;
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
const express = require('express');
|
|
2
|
+
const router = express.Router();
|
|
3
|
+
const { requireAuth } = require('../middleware/auth');
|
|
4
|
+
const { sanitizeError } = require('../utils/security');
|
|
5
|
+
|
|
6
|
+
router.use(requireAuth);
|
|
7
|
+
|
|
8
|
+
// Get browser status
|
|
9
|
+
router.get('/status', (req, res) => {
|
|
10
|
+
const bc = req.app.locals.browserController;
|
|
11
|
+
res.json({
|
|
12
|
+
launched: bc.isLaunched(),
|
|
13
|
+
pages: bc.getPageCount(),
|
|
14
|
+
headless: bc.headless
|
|
15
|
+
});
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
// Launch browser
|
|
19
|
+
router.post('/launch', async (req, res) => {
|
|
20
|
+
try {
|
|
21
|
+
const bc = req.app.locals.browserController;
|
|
22
|
+
await bc.launch(req.body || {});
|
|
23
|
+
res.json({ success: true });
|
|
24
|
+
} catch (err) {
|
|
25
|
+
res.status(500).json({ error: sanitizeError(err) });
|
|
26
|
+
}
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
// Navigate to URL
|
|
30
|
+
router.post('/navigate', async (req, res) => {
|
|
31
|
+
try {
|
|
32
|
+
const { url, waitFor } = req.body;
|
|
33
|
+
if (!url) return res.status(400).json({ error: 'url required' });
|
|
34
|
+
|
|
35
|
+
const bc = req.app.locals.browserController;
|
|
36
|
+
const result = await bc.navigate(url, { waitUntil: waitFor || 'domcontentloaded' });
|
|
37
|
+
res.json(result);
|
|
38
|
+
} catch (err) {
|
|
39
|
+
res.status(500).json({ error: sanitizeError(err) });
|
|
40
|
+
}
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
// Take screenshot
|
|
44
|
+
router.post('/screenshot', async (req, res) => {
|
|
45
|
+
try {
|
|
46
|
+
const bc = req.app.locals.browserController;
|
|
47
|
+
const result = await bc.screenshot(req.body || {});
|
|
48
|
+
res.json(result);
|
|
49
|
+
} catch (err) {
|
|
50
|
+
res.status(500).json({ error: sanitizeError(err) });
|
|
51
|
+
}
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
// Click element
|
|
55
|
+
router.post('/click', async (req, res) => {
|
|
56
|
+
try {
|
|
57
|
+
const { selector, text } = req.body;
|
|
58
|
+
const bc = req.app.locals.browserController;
|
|
59
|
+
const result = await bc.click(selector, { text });
|
|
60
|
+
res.json(result);
|
|
61
|
+
} catch (err) {
|
|
62
|
+
res.status(500).json({ error: sanitizeError(err) });
|
|
63
|
+
}
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
// Fill form field
|
|
67
|
+
router.post('/fill', async (req, res) => {
|
|
68
|
+
try {
|
|
69
|
+
const { selector, value } = req.body;
|
|
70
|
+
if (!selector || value === undefined) return res.status(400).json({ error: 'selector and value required' });
|
|
71
|
+
|
|
72
|
+
const bc = req.app.locals.browserController;
|
|
73
|
+
const result = await bc.fill(selector, value);
|
|
74
|
+
res.json(result);
|
|
75
|
+
} catch (err) {
|
|
76
|
+
res.status(500).json({ error: sanitizeError(err) });
|
|
77
|
+
}
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
// Extract content
|
|
81
|
+
router.post('/extract', async (req, res) => {
|
|
82
|
+
try {
|
|
83
|
+
const bc = req.app.locals.browserController;
|
|
84
|
+
const result = await bc.extractContent(req.body || {});
|
|
85
|
+
res.json(result);
|
|
86
|
+
} catch (err) {
|
|
87
|
+
res.status(500).json({ error: sanitizeError(err) });
|
|
88
|
+
}
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
// Execute JavaScript
|
|
92
|
+
router.post('/execute', async (req, res) => {
|
|
93
|
+
try {
|
|
94
|
+
const { code } = req.body;
|
|
95
|
+
if (!code) return res.status(400).json({ error: 'code required' });
|
|
96
|
+
|
|
97
|
+
const bc = req.app.locals.browserController;
|
|
98
|
+
const result = await bc.executeJS(code);
|
|
99
|
+
res.json({ result });
|
|
100
|
+
} catch (err) {
|
|
101
|
+
res.status(500).json({ error: sanitizeError(err) });
|
|
102
|
+
}
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
// Close browser
|
|
106
|
+
router.post('/close', async (req, res) => {
|
|
107
|
+
try {
|
|
108
|
+
const bc = req.app.locals.browserController;
|
|
109
|
+
await bc.closeBrowser();
|
|
110
|
+
res.json({ success: true });
|
|
111
|
+
} catch (err) {
|
|
112
|
+
res.status(500).json({ error: sanitizeError(err) });
|
|
113
|
+
}
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
module.exports = router;
|