neoagent 2.5.2-beta.9 → 3.0.1-beta.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/README.md +5 -1
- package/docs/integrations.md +11 -7
- package/flutter_app/lib/main_controller.dart +34 -0
- package/flutter_app/lib/main_security.dart +19 -0
- package/lib/schema_migrations.js +224 -0
- package/package.json +2 -2
- package/server/admin/admin.css +29 -0
- package/server/admin/admin.js +81 -1
- package/server/admin/index.html +97 -9
- package/server/db/database.js +57 -1
- package/server/public/.last_build_id +1 -1
- package/server/public/flutter_bootstrap.js +1 -1
- package/server/public/main.dart.js +32788 -32752
- package/server/routes/security.js +19 -0
- package/server/routes/settings.js +1 -0
- package/server/routes/skills.js +9 -5
- package/server/services/ai/deliverables/artifact_helpers.js +92 -9
- package/server/services/ai/deliverables/selector.js +17 -0
- package/server/services/ai/hooks.js +3 -2
- package/server/services/ai/learning.js +8 -3
- package/server/services/ai/loop/agent_engine_core.js +67 -0
- package/server/services/ai/loop/blank_recovery.js +37 -0
- package/server/services/ai/loop/conversation_loop.js +391 -252
- package/server/services/ai/loop/error_recovery.js +38 -0
- package/server/services/ai/loop/messaging_delivery.js +52 -6
- package/server/services/ai/loop/progress_classification.js +178 -0
- package/server/services/ai/loop/progress_monitor.js +6 -5
- package/server/services/ai/loop/tool_dispatch.js +1 -0
- package/server/services/ai/loopPolicy.js +15 -6
- package/server/services/ai/messagingFallback.js +23 -1
- package/server/services/ai/preModelCompaction.js +31 -0
- package/server/services/ai/repetitionGuard.js +47 -2
- package/server/services/ai/settings.js +8 -0
- package/server/services/ai/systemPrompt.js +24 -0
- package/server/services/ai/taskAnalysis.js +10 -0
- package/server/services/ai/toolEvidence.js +39 -3
- package/server/services/ai/toolResult.js +29 -0
- package/server/services/ai/toolRunner.js +262 -55
- package/server/services/ai/toolSelector.js +20 -3
- package/server/services/ai/tools.js +201 -31
- package/server/services/cli/shell_worker_pool.js +87 -5
- package/server/services/integrations/github/common.js +2 -2
- package/server/services/integrations/github/repos.js +123 -55
- package/server/services/manager.js +16 -0
- package/server/services/memory/manager.js +39 -24
- package/server/services/runtime/docker-vm-manager.js +21 -1
- package/server/services/runtime/manager.js +6 -2
- package/server/services/security/approval_gate_service.js +108 -5
- package/server/services/security/tool_categories.js +113 -4
- package/server/services/security/tool_security_hook.js +7 -2
- package/server/services/skills/runtime.js +2 -0
- package/server/services/workspace/manager.js +56 -0
package/README.md
CHANGED
|
@@ -19,7 +19,11 @@
|
|
|
19
19
|
</p>
|
|
20
20
|
|
|
21
21
|
<p align="center">
|
|
22
|
-
<
|
|
22
|
+
<a href="https://www.producthunt.com/products/neoagent-2?embed=true&utm_source=badge-featured&utm_medium=badge&utm_campaign=badge-neoagent-2" target="_blank" rel="noopener noreferrer"><img alt="NeoAgent - The next-gen self-hosted AI agent that works beyond chat | Product Hunt" width="250" height="54" src="https://api.producthunt.com/widgets/embed-image/v1/featured.svg?post_id=1171573&theme=light&t=1781447653039"></a>
|
|
23
|
+
</p>
|
|
24
|
+
|
|
25
|
+
<p align="center">
|
|
26
|
+
<img src="demo.gif" alt="NeoAgent demo" width="100%">
|
|
23
27
|
</p>
|
|
24
28
|
|
|
25
29
|
## 🚀 Install
|
package/docs/integrations.md
CHANGED
|
@@ -36,15 +36,19 @@ or delete data.
|
|
|
36
36
|
|
|
37
37
|
### Setup
|
|
38
38
|
|
|
39
|
-
Google, Microsoft, GitHub, Notion, Slack, Figma, and Spotify require OAuth
|
|
40
|
-
application credentials on the server. The default callback is:
|
|
41
39
|
|
|
42
|
-
|
|
43
|
-
PUBLIC_URL/api/integrations/oauth/callback
|
|
44
|
-
|
|
40
|
+
Google, Microsoft, GitHub, Notion, Slack, Figma, and Spotify require OAuth application credentials on the server. The default callback is:
|
|
41
|
+
`PUBLIC_URL/api/integrations/oauth/callback`
|
|
42
|
+
|
|
43
|
+
**GitHub OAuth Configuration:**
|
|
44
|
+
1. Navigate to your GitHub Developer Settings and create a new **OAuth App**.
|
|
45
|
+
2. Set the Authorization callback URL to: `PUBLIC_URL/api/integrations/oauth/callback`.
|
|
46
|
+
3. Generate a **Client ID** and **Client Secret**.
|
|
47
|
+
4. Add these to your server environment configuration:
|
|
48
|
+
- `GITHUB_CLIENT_ID=your_id_here`
|
|
49
|
+
- `GITHUB_CLIENT_SECRET=your_secret_here`
|
|
45
50
|
|
|
46
|
-
Home Assistant and Trello can be configured for a user from the application.
|
|
47
|
-
See [Configuration](configuration.md#official-integrations).
|
|
51
|
+
Home Assistant and Trello can be configured for a user from the application. See Configuration.
|
|
48
52
|
|
|
49
53
|
## Messaging channels
|
|
50
54
|
|
|
@@ -367,6 +367,15 @@ class NeoAgentController extends ChangeNotifier {
|
|
|
367
367
|
notifyListeners();
|
|
368
368
|
}
|
|
369
369
|
|
|
370
|
+
void clearPendingApprovalForRun(String runId) {
|
|
371
|
+
if (pendingApproval?.runId != runId) return;
|
|
372
|
+
_SecurityNotificationService.cancelApprovalNotification(
|
|
373
|
+
pendingApproval?.approvalId ?? '',
|
|
374
|
+
);
|
|
375
|
+
pendingApproval = null;
|
|
376
|
+
notifyListeners();
|
|
377
|
+
}
|
|
378
|
+
|
|
370
379
|
bool get desktopFloatingToolbarPopupRequested =>
|
|
371
380
|
_desktopFloatingToolbarPopupRequested;
|
|
372
381
|
|
|
@@ -7349,6 +7358,7 @@ class NeoAgentController extends ChangeNotifier {
|
|
|
7349
7358
|
socket.on('run:stopped', (dynamic data) {
|
|
7350
7359
|
final payload = _jsonMap(data);
|
|
7351
7360
|
final runId = payload['runId']?.toString() ?? '';
|
|
7361
|
+
clearPendingApprovalForRun(runId);
|
|
7352
7362
|
if (_voiceRunIds.remove(runId)) {
|
|
7353
7363
|
return;
|
|
7354
7364
|
}
|
|
@@ -7369,6 +7379,30 @@ class NeoAgentController extends ChangeNotifier {
|
|
|
7369
7379
|
unawaited(refreshRunsOnly());
|
|
7370
7380
|
notifyListeners();
|
|
7371
7381
|
});
|
|
7382
|
+
socket.on('run:interrupted', (dynamic data) {
|
|
7383
|
+
final payload = _jsonMap(data);
|
|
7384
|
+
final runId = payload['runId']?.toString() ?? '';
|
|
7385
|
+
clearPendingApprovalForRun(runId);
|
|
7386
|
+
if (_voiceRunIds.remove(runId)) {
|
|
7387
|
+
return;
|
|
7388
|
+
}
|
|
7389
|
+
if (_backgroundRunIds.remove(runId)) {
|
|
7390
|
+
unawaited(refreshRunsOnly());
|
|
7391
|
+
unawaited(refreshMemory());
|
|
7392
|
+
notifyListeners();
|
|
7393
|
+
return;
|
|
7394
|
+
}
|
|
7395
|
+
streamingAssistant = '';
|
|
7396
|
+
isSendingMessage = false;
|
|
7397
|
+
if (activeRun?.runId == runId) {
|
|
7398
|
+
activeRun = activeRun!.copyWith(
|
|
7399
|
+
phase: 'Interrupted',
|
|
7400
|
+
pendingSteeringCount: 0,
|
|
7401
|
+
);
|
|
7402
|
+
}
|
|
7403
|
+
unawaited(refreshRunsOnly());
|
|
7404
|
+
notifyListeners();
|
|
7405
|
+
});
|
|
7372
7406
|
socket.on('run:error', (dynamic data) {
|
|
7373
7407
|
final payload = _jsonMap(data);
|
|
7374
7408
|
final runId = payload['runId']?.toString();
|
|
@@ -103,6 +103,13 @@ const _kCategoryInfo = <String, _CategoryInfo>{
|
|
|
103
103
|
color: Color(0xFFFB8C00),
|
|
104
104
|
riskLevel: 'medium',
|
|
105
105
|
),
|
|
106
|
+
'external': _CategoryInfo(
|
|
107
|
+
label: 'External & MCP Tools',
|
|
108
|
+
subtitle: 'Tools not built into NeoAgent, including connected MCP servers and custom tool providers.',
|
|
109
|
+
icon: Icons.hub_rounded,
|
|
110
|
+
color: Color(0xFF6D4C41),
|
|
111
|
+
riskLevel: 'high',
|
|
112
|
+
),
|
|
106
113
|
};
|
|
107
114
|
|
|
108
115
|
_CategoryInfo _categoryInfo(String category) {
|
|
@@ -732,6 +739,18 @@ class _ToolApprovalSheetState extends State<ToolApprovalSheet>
|
|
|
732
739
|
toolArgs: widget.request.toolArgs,
|
|
733
740
|
);
|
|
734
741
|
widget.controller.clearPendingApproval();
|
|
742
|
+
} on BackendException catch (error) {
|
|
743
|
+
if (error.statusCode == 410) {
|
|
744
|
+
widget.controller.clearPendingApproval();
|
|
745
|
+
if (mounted) {
|
|
746
|
+
ScaffoldMessenger.of(context).showSnackBar(
|
|
747
|
+
SnackBar(
|
|
748
|
+
content: Text(error.message),
|
|
749
|
+
backgroundColor: _warning,
|
|
750
|
+
),
|
|
751
|
+
);
|
|
752
|
+
}
|
|
753
|
+
}
|
|
735
754
|
} catch (_) {
|
|
736
755
|
// timeout will fire server-side; safe to dismiss
|
|
737
756
|
}
|
package/lib/schema_migrations.js
CHANGED
|
@@ -191,6 +191,187 @@ function migrateMemoryEmbeddingMetadata(db) {
|
|
|
191
191
|
}
|
|
192
192
|
}
|
|
193
193
|
|
|
194
|
+
function migrateMemoryExactHashUniqueness(db) {
|
|
195
|
+
const columns = new Set(
|
|
196
|
+
db.prepare('PRAGMA table_info(memories)').all().map((column) => column.name),
|
|
197
|
+
);
|
|
198
|
+
if (!columns.has('memory_hash')) return;
|
|
199
|
+
|
|
200
|
+
const groups = db.prepare(
|
|
201
|
+
`SELECT
|
|
202
|
+
user_id,
|
|
203
|
+
COALESCE(agent_id, '') AS agent_key,
|
|
204
|
+
memory_hash,
|
|
205
|
+
COALESCE(scope_type, 'agent') AS scope_type_key,
|
|
206
|
+
COALESCE(scope_id, '') AS scope_id_key,
|
|
207
|
+
COUNT(*) AS duplicate_count
|
|
208
|
+
FROM memories
|
|
209
|
+
WHERE archived = 0
|
|
210
|
+
AND memory_hash IS NOT NULL
|
|
211
|
+
AND trim(memory_hash) <> ''
|
|
212
|
+
GROUP BY
|
|
213
|
+
user_id,
|
|
214
|
+
COALESCE(agent_id, ''),
|
|
215
|
+
memory_hash,
|
|
216
|
+
COALESCE(scope_type, 'agent'),
|
|
217
|
+
COALESCE(scope_id, '')
|
|
218
|
+
HAVING COUNT(*) > 1`
|
|
219
|
+
).all();
|
|
220
|
+
|
|
221
|
+
const selectGroupRows = db.prepare(
|
|
222
|
+
`SELECT id, importance, confidence, access_count
|
|
223
|
+
FROM memories
|
|
224
|
+
WHERE user_id = ?
|
|
225
|
+
AND COALESCE(agent_id, '') = ?
|
|
226
|
+
AND memory_hash = ?
|
|
227
|
+
AND COALESCE(scope_type, 'agent') = ?
|
|
228
|
+
AND COALESCE(scope_id, '') = ?
|
|
229
|
+
AND archived = 0
|
|
230
|
+
ORDER BY updated_at DESC, created_at DESC, id DESC`
|
|
231
|
+
);
|
|
232
|
+
|
|
233
|
+
const archiveDuplicates = db.transaction(() => {
|
|
234
|
+
for (const group of groups) {
|
|
235
|
+
const rows = selectGroupRows.all(
|
|
236
|
+
group.user_id,
|
|
237
|
+
group.agent_key,
|
|
238
|
+
group.memory_hash,
|
|
239
|
+
group.scope_type_key,
|
|
240
|
+
group.scope_id_key,
|
|
241
|
+
);
|
|
242
|
+
if (rows.length < 2) continue;
|
|
243
|
+
|
|
244
|
+
const winner = rows[0];
|
|
245
|
+
const mergedImportance = Math.max(...rows.map((row) => Number(row.importance || 0)));
|
|
246
|
+
const mergedConfidence = Math.max(...rows.map((row) => Number(row.confidence || 0)));
|
|
247
|
+
const mergedAccessCount = rows.reduce(
|
|
248
|
+
(sum, row) => sum + Math.max(0, Number(row.access_count || 0)),
|
|
249
|
+
0,
|
|
250
|
+
);
|
|
251
|
+
|
|
252
|
+
db.prepare(
|
|
253
|
+
`UPDATE memories
|
|
254
|
+
SET importance = MAX(importance, ?),
|
|
255
|
+
confidence = MAX(confidence, ?),
|
|
256
|
+
access_count = ?,
|
|
257
|
+
updated_at = datetime('now')
|
|
258
|
+
WHERE id = ?`
|
|
259
|
+
).run(
|
|
260
|
+
mergedImportance,
|
|
261
|
+
Math.min(1, mergedConfidence),
|
|
262
|
+
mergedAccessCount,
|
|
263
|
+
winner.id,
|
|
264
|
+
);
|
|
265
|
+
|
|
266
|
+
const duplicateIds = rows.slice(1).map((row) => row.id);
|
|
267
|
+
if (!duplicateIds.length) continue;
|
|
268
|
+
const placeholders = duplicateIds.map(() => '?').join(', ');
|
|
269
|
+
db.prepare(
|
|
270
|
+
`UPDATE memories
|
|
271
|
+
SET archived = 1,
|
|
272
|
+
updated_at = datetime('now')
|
|
273
|
+
WHERE id IN (${placeholders})`
|
|
274
|
+
).run(...duplicateIds);
|
|
275
|
+
}
|
|
276
|
+
});
|
|
277
|
+
archiveDuplicates();
|
|
278
|
+
|
|
279
|
+
db.exec(`
|
|
280
|
+
CREATE UNIQUE INDEX IF NOT EXISTS idx_memories_hash_unique
|
|
281
|
+
ON memories(
|
|
282
|
+
user_id,
|
|
283
|
+
COALESCE(agent_id, ''),
|
|
284
|
+
memory_hash,
|
|
285
|
+
COALESCE(scope_type, 'agent'),
|
|
286
|
+
COALESCE(scope_id, '')
|
|
287
|
+
)
|
|
288
|
+
WHERE archived = 0 AND memory_hash IS NOT NULL
|
|
289
|
+
`);
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
function migrateSkillOwnership(db) {
|
|
293
|
+
const tableInfo = db.prepare("PRAGMA table_info(skills)").all();
|
|
294
|
+
if (!tableInfo.length) return;
|
|
295
|
+
|
|
296
|
+
const skillsSql = db.prepare(
|
|
297
|
+
"SELECT sql FROM sqlite_master WHERE type='table' AND name='skills'",
|
|
298
|
+
).get()?.sql || '';
|
|
299
|
+
const hasUserIdColumn = tableInfo.some((column) => column.name === 'user_id');
|
|
300
|
+
const hasLegacyNameUniqueConstraint = /\bname\s+TEXT\s+UNIQUE\b/i.test(skillsSql);
|
|
301
|
+
|
|
302
|
+
if (!hasUserIdColumn || hasLegacyNameUniqueConstraint) {
|
|
303
|
+
db.exec(`
|
|
304
|
+
DROP TABLE IF EXISTS skills_v2;
|
|
305
|
+
CREATE TABLE IF NOT EXISTS skills_v2 (
|
|
306
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
307
|
+
user_id INTEGER NOT NULL DEFAULT 0,
|
|
308
|
+
name TEXT NOT NULL,
|
|
309
|
+
description TEXT,
|
|
310
|
+
file_path TEXT NOT NULL,
|
|
311
|
+
metadata TEXT DEFAULT '{}',
|
|
312
|
+
enabled INTEGER DEFAULT 1,
|
|
313
|
+
auto_created INTEGER DEFAULT 0,
|
|
314
|
+
created_at TEXT DEFAULT (datetime('now')),
|
|
315
|
+
updated_at TEXT DEFAULT (datetime('now'))
|
|
316
|
+
);
|
|
317
|
+
`);
|
|
318
|
+
|
|
319
|
+
if (hasUserIdColumn) {
|
|
320
|
+
db.exec(`
|
|
321
|
+
INSERT INTO skills_v2 (
|
|
322
|
+
id, user_id, name, description, file_path, metadata,
|
|
323
|
+
enabled, auto_created, created_at, updated_at
|
|
324
|
+
)
|
|
325
|
+
SELECT
|
|
326
|
+
id,
|
|
327
|
+
COALESCE(user_id, 0),
|
|
328
|
+
name,
|
|
329
|
+
description,
|
|
330
|
+
file_path,
|
|
331
|
+
COALESCE(metadata, '{}'),
|
|
332
|
+
COALESCE(enabled, 1),
|
|
333
|
+
COALESCE(auto_created, 0),
|
|
334
|
+
created_at,
|
|
335
|
+
updated_at
|
|
336
|
+
FROM skills
|
|
337
|
+
`);
|
|
338
|
+
} else {
|
|
339
|
+
db.exec(`
|
|
340
|
+
INSERT INTO skills_v2 (
|
|
341
|
+
id, user_id, name, description, file_path, metadata,
|
|
342
|
+
enabled, auto_created, created_at, updated_at
|
|
343
|
+
)
|
|
344
|
+
SELECT
|
|
345
|
+
id,
|
|
346
|
+
0,
|
|
347
|
+
name,
|
|
348
|
+
description,
|
|
349
|
+
file_path,
|
|
350
|
+
COALESCE(metadata, '{}'),
|
|
351
|
+
COALESCE(enabled, 1),
|
|
352
|
+
COALESCE(auto_created, 0),
|
|
353
|
+
created_at,
|
|
354
|
+
updated_at
|
|
355
|
+
FROM skills
|
|
356
|
+
`);
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
db.exec(`
|
|
360
|
+
DROP TABLE skills;
|
|
361
|
+
ALTER TABLE skills_v2 RENAME TO skills;
|
|
362
|
+
`);
|
|
363
|
+
} else {
|
|
364
|
+
db.exec('UPDATE skills SET user_id = 0 WHERE user_id IS NULL');
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
db.exec(`
|
|
368
|
+
CREATE UNIQUE INDEX IF NOT EXISTS idx_skills_user_name
|
|
369
|
+
ON skills(user_id, name);
|
|
370
|
+
CREATE INDEX IF NOT EXISTS idx_skills_user_enabled
|
|
371
|
+
ON skills(user_id, enabled, name);
|
|
372
|
+
`);
|
|
373
|
+
}
|
|
374
|
+
|
|
194
375
|
function migrateToolPermissions(db) {
|
|
195
376
|
db.exec(`
|
|
196
377
|
CREATE TABLE IF NOT EXISTS tool_policies (
|
|
@@ -217,6 +398,43 @@ function migrateToolPermissions(db) {
|
|
|
217
398
|
`);
|
|
218
399
|
}
|
|
219
400
|
|
|
401
|
+
function migrateApprovalPersistence(db) {
|
|
402
|
+
db.exec(`
|
|
403
|
+
CREATE TABLE IF NOT EXISTS pending_approvals (
|
|
404
|
+
id TEXT PRIMARY KEY,
|
|
405
|
+
user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
|
406
|
+
run_id TEXT REFERENCES agent_runs(id) ON DELETE CASCADE,
|
|
407
|
+
tool_name TEXT NOT NULL,
|
|
408
|
+
tool_args_json TEXT,
|
|
409
|
+
category TEXT,
|
|
410
|
+
status TEXT NOT NULL CHECK(status IN ('pending','approved','denied','timeout','expired')),
|
|
411
|
+
scope TEXT CHECK(scope IN ('once','session','always')),
|
|
412
|
+
expires_at TEXT NOT NULL,
|
|
413
|
+
decided_at TEXT,
|
|
414
|
+
created_at TEXT DEFAULT (datetime('now')),
|
|
415
|
+
updated_at TEXT DEFAULT (datetime('now'))
|
|
416
|
+
);
|
|
417
|
+
|
|
418
|
+
CREATE INDEX IF NOT EXISTS idx_pending_approvals_user
|
|
419
|
+
ON pending_approvals(user_id, status, created_at DESC);
|
|
420
|
+
CREATE INDEX IF NOT EXISTS idx_pending_approvals_run
|
|
421
|
+
ON pending_approvals(run_id, status, expires_at);
|
|
422
|
+
|
|
423
|
+
CREATE TABLE IF NOT EXISTS approval_session_grants (
|
|
424
|
+
user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
|
425
|
+
run_id TEXT NOT NULL,
|
|
426
|
+
tool_name TEXT NOT NULL,
|
|
427
|
+
expires_at TEXT NOT NULL,
|
|
428
|
+
created_at TEXT DEFAULT (datetime('now')),
|
|
429
|
+
updated_at TEXT DEFAULT (datetime('now')),
|
|
430
|
+
PRIMARY KEY (user_id, run_id, tool_name)
|
|
431
|
+
);
|
|
432
|
+
|
|
433
|
+
CREATE INDEX IF NOT EXISTS idx_approval_session_grants_expiry
|
|
434
|
+
ON approval_session_grants(expires_at);
|
|
435
|
+
`);
|
|
436
|
+
}
|
|
437
|
+
|
|
220
438
|
function migrateToolPoliciesAllowAlways(db) {
|
|
221
439
|
// SQLite doesn't support ALTER COLUMN — recreate the table to add 'allow_always'
|
|
222
440
|
// to the CHECK constraint on existing installations.
|
|
@@ -246,7 +464,10 @@ function runSchemaMigrations(db) {
|
|
|
246
464
|
migrateMemoryRelations(db);
|
|
247
465
|
migrateMemoryRetrievalEvents(db);
|
|
248
466
|
migrateMemoryEmbeddingMetadata(db);
|
|
467
|
+
migrateMemoryExactHashUniqueness(db);
|
|
468
|
+
migrateSkillOwnership(db);
|
|
249
469
|
migrateToolPermissions(db);
|
|
470
|
+
migrateApprovalPersistence(db);
|
|
250
471
|
migrateToolPoliciesAllowAlways(db);
|
|
251
472
|
}
|
|
252
473
|
|
|
@@ -256,7 +477,10 @@ module.exports = {
|
|
|
256
477
|
migrateMemoryRelations,
|
|
257
478
|
migrateMemoryRetrievalEvents,
|
|
258
479
|
migrateMemoryEmbeddingMetadata,
|
|
480
|
+
migrateMemoryExactHashUniqueness,
|
|
481
|
+
migrateSkillOwnership,
|
|
259
482
|
migrateToolPermissions,
|
|
483
|
+
migrateApprovalPersistence,
|
|
260
484
|
migrateToolPoliciesAllowAlways,
|
|
261
485
|
runSchemaMigrations,
|
|
262
486
|
};
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "neoagent",
|
|
3
|
-
"version": "
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "3.0.1-beta.0",
|
|
4
|
+
"description": "Self-hosted AI agent for long-running tasks, automation, messaging, device control, and local memory",
|
|
5
5
|
"license": "AGPL-3.0-only",
|
|
6
6
|
"main": "server/index.js",
|
|
7
7
|
"engines": {
|
package/server/admin/admin.css
CHANGED
|
@@ -36,6 +36,35 @@
|
|
|
36
36
|
|
|
37
37
|
--font-sans: 'Geist', 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
|
|
38
38
|
--font-mono: 'Geist Mono', 'JetBrains Mono', 'Fira Code', 'Menlo', monospace;
|
|
39
|
+
|
|
40
|
+
--row-hover: rgba(224, 240, 224, 0.04);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/* ── Light theme override ───────────────────────────────────────────────── */
|
|
44
|
+
[data-theme="light"] {
|
|
45
|
+
--bg: #f3f6f1;
|
|
46
|
+
--bg-sidebar: #ffffff;
|
|
47
|
+
--bg-card: #ffffff;
|
|
48
|
+
--bg-secondary: #eaeee6;
|
|
49
|
+
--bg-input: #ffffff;
|
|
50
|
+
--border: rgba(20, 40, 24, 0.12);
|
|
51
|
+
--border-light: rgba(20, 40, 24, 0.20);
|
|
52
|
+
|
|
53
|
+
--accent: #b8861f;
|
|
54
|
+
--accent-hover: #9a6f14;
|
|
55
|
+
--accent-alt: #3f8a47;
|
|
56
|
+
--accent-muted: rgba(184, 134, 31, 0.14);
|
|
57
|
+
|
|
58
|
+
--text: #1a211b;
|
|
59
|
+
--text-secondary:#4a544a;
|
|
60
|
+
--text-muted: #717b6f;
|
|
61
|
+
|
|
62
|
+
--success: #2f8a3c;
|
|
63
|
+
--warning: #b07a18;
|
|
64
|
+
--danger: #c0492f;
|
|
65
|
+
--info: #2f7d72;
|
|
66
|
+
|
|
67
|
+
--row-hover: rgba(20, 40, 24, 0.05);
|
|
39
68
|
}
|
|
40
69
|
|
|
41
70
|
/* ── Base ────────────────────────────────────────────────────────────────── */
|
package/server/admin/admin.js
CHANGED
|
@@ -16,10 +16,31 @@ function showPage(page, btn) {
|
|
|
16
16
|
if (btn) btn.classList.add('active');
|
|
17
17
|
currentPage = page;
|
|
18
18
|
|
|
19
|
-
const loaders = { overview: loadHealth, logs: loadLogs, updates: loadVersion, config: loadConfig, providers: loadProviders, models: loadModels, analytics: loadAnalytics, users: loadUsers, sql: loadSql, access: loadAccess };
|
|
19
|
+
const loaders = { overview: loadHealth, logs: loadLogs, issues: loadIssues, updates: loadVersion, config: loadConfig, providers: loadProviders, models: loadModels, analytics: loadAnalytics, users: loadUsers, sql: loadSql, access: loadAccess };
|
|
20
20
|
loaders[page]?.();
|
|
21
21
|
}
|
|
22
22
|
|
|
23
|
+
// ── Theme ──────────────────────────────────────────────────────────────────
|
|
24
|
+
|
|
25
|
+
function applyTheme(theme) {
|
|
26
|
+
const isLight = theme === 'light';
|
|
27
|
+
document.documentElement.setAttribute('data-theme', isLight ? 'light' : 'dark');
|
|
28
|
+
const label = document.getElementById('theme-toggle-label');
|
|
29
|
+
if (label) label.textContent = isLight ? 'Dark mode' : 'Light mode';
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function toggleTheme() {
|
|
33
|
+
const next = document.documentElement.getAttribute('data-theme') === 'light' ? 'dark' : 'light';
|
|
34
|
+
try { localStorage.setItem('admin-theme', next); } catch {}
|
|
35
|
+
applyTheme(next);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function initTheme() {
|
|
39
|
+
let stored = 'dark';
|
|
40
|
+
try { stored = localStorage.getItem('admin-theme') || 'dark'; } catch {}
|
|
41
|
+
applyTheme(stored);
|
|
42
|
+
}
|
|
43
|
+
|
|
23
44
|
async function signOut() {
|
|
24
45
|
await fetch('/admin/api/logout', { method: 'POST' }).catch(() => {});
|
|
25
46
|
window.location.replace('/admin/login');
|
|
@@ -130,6 +151,62 @@ function copyLogs() {
|
|
|
130
151
|
navigator.clipboard.writeText(text).catch(() => {});
|
|
131
152
|
}
|
|
132
153
|
|
|
154
|
+
// ── Issues ─────────────────────────────────────────────────────────────────
|
|
155
|
+
|
|
156
|
+
// Distil the raw log stream down to critical, actionable issues: error-level
|
|
157
|
+
// entries grouped by message so a repeated failure shows up once with a count.
|
|
158
|
+
function groupIssues(logs) {
|
|
159
|
+
const groups = new Map();
|
|
160
|
+
for (const entry of logs) {
|
|
161
|
+
if ((entry.type || 'log') !== 'error') continue;
|
|
162
|
+
const message = String(entry.message || '').trim();
|
|
163
|
+
if (!message) continue;
|
|
164
|
+
const existing = groups.get(message);
|
|
165
|
+
if (existing) {
|
|
166
|
+
existing.count += 1;
|
|
167
|
+
if (entry.timestamp && entry.timestamp > existing.last) existing.last = entry.timestamp;
|
|
168
|
+
} else {
|
|
169
|
+
groups.set(message, { message, count: 1, last: entry.timestamp || '' });
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
return Array.from(groups.values()).sort((a, b) => String(b.last).localeCompare(String(a.last)));
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
function renderIssues(issues) {
|
|
176
|
+
const table = document.getElementById('issue-table');
|
|
177
|
+
const count = document.getElementById('issue-count');
|
|
178
|
+
const badge = document.getElementById('issues-badge');
|
|
179
|
+
if (count) count.textContent = `${issues.length} issue${issues.length === 1 ? '' : 's'}`;
|
|
180
|
+
if (badge) {
|
|
181
|
+
if (issues.length) { badge.hidden = false; badge.textContent = String(issues.length); }
|
|
182
|
+
else { badge.hidden = true; }
|
|
183
|
+
}
|
|
184
|
+
if (!table) return;
|
|
185
|
+
if (!issues.length) { table.innerHTML = '<div class="empty">No critical issues 🎉</div>'; return; }
|
|
186
|
+
table.innerHTML = issues.map((i) => `
|
|
187
|
+
<div class="issue-row">
|
|
188
|
+
<div class="issue-msg">${esc(i.message)}</div>
|
|
189
|
+
<div class="issue-meta">
|
|
190
|
+
${i.count > 1 ? `<span class="issue-count">×${i.count}</span><br>` : ''}
|
|
191
|
+
${esc(fmtTime(i.last))}
|
|
192
|
+
</div>
|
|
193
|
+
</div>`).join('');
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
async function loadIssues() {
|
|
197
|
+
try {
|
|
198
|
+
const data = await api('/admin/api/logs').then((r) => r.json());
|
|
199
|
+
localLogs = data.logs || localLogs;
|
|
200
|
+
renderIssues(groupIssues(localLogs));
|
|
201
|
+
setTs('issues-ts', 'Updated');
|
|
202
|
+
} catch (err) {
|
|
203
|
+
if (err.message !== 'unauthorized') {
|
|
204
|
+
const table = document.getElementById('issue-table');
|
|
205
|
+
if (table) table.innerHTML = '<div class="empty">Failed to load issues</div>';
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
133
210
|
// ── Updates ────────────────────────────────────────────────────────────────
|
|
134
211
|
|
|
135
212
|
async function loadVersion() {
|
|
@@ -626,12 +703,14 @@ async function saveEnabledModels(btn) {
|
|
|
626
703
|
function startPolling() {
|
|
627
704
|
setInterval(() => { if (currentPage === 'overview') loadHealth(); }, 30_000);
|
|
628
705
|
setInterval(() => { if (currentPage === 'logs' && !logsCleared) loadLogs(); }, 5_000);
|
|
706
|
+
setInterval(() => { if (currentPage === 'issues') loadIssues(); }, 10_000);
|
|
629
707
|
setInterval(() => { if (currentPage === 'updates') loadVersion(); }, 10_000);
|
|
630
708
|
}
|
|
631
709
|
|
|
632
710
|
// ── Init ───────────────────────────────────────────────────────────────────
|
|
633
711
|
|
|
634
712
|
(async function init() {
|
|
713
|
+
initTheme();
|
|
635
714
|
try {
|
|
636
715
|
const res = await fetch('/admin/api/version');
|
|
637
716
|
if (res.status === 401) { window.location.replace('/admin/login'); return; }
|
|
@@ -639,5 +718,6 @@ function startPolling() {
|
|
|
639
718
|
// server unreachable — still render; API calls will fail gracefully
|
|
640
719
|
}
|
|
641
720
|
loadHealth();
|
|
721
|
+
loadIssues();
|
|
642
722
|
startPolling();
|
|
643
723
|
}());
|