neoagent 2.4.2-beta.4 → 2.4.2-beta.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (62) hide show
  1. package/.env.example +0 -9
  2. package/README.md +48 -18
  3. package/docs/configuration.md +0 -10
  4. package/flutter_app/lib/main.dart +0 -1
  5. package/flutter_app/lib/main_controller.dart +0 -160
  6. package/flutter_app/lib/main_models.dart +28 -0
  7. package/flutter_app/lib/main_operations.dart +17 -1
  8. package/flutter_app/lib/main_runtime.dart +0 -22
  9. package/flutter_app/lib/main_settings.dart +9 -0
  10. package/flutter_app/lib/main_shared.dart +0 -161
  11. package/flutter_app/lib/src/backend_client.dart +8 -0
  12. package/flutter_app/macos/Flutter/GeneratedPluginRegistrant.swift +0 -2
  13. package/flutter_app/pubspec.lock +0 -8
  14. package/flutter_app/pubspec.yaml +0 -1
  15. package/package.json +1 -1
  16. package/server/admin/admin.css +23 -0
  17. package/server/admin/admin.js +17 -19
  18. package/server/admin/analytics.js +254 -48
  19. package/server/admin/index.html +9 -2
  20. package/server/db/database.js +112 -0
  21. package/server/http/routes.js +1 -0
  22. package/server/public/.last_build_id +1 -1
  23. package/server/public/assets/AssetManifest.bin +1 -1
  24. package/server/public/assets/AssetManifest.bin.json +1 -1
  25. package/server/public/assets/NOTICES +0 -183
  26. package/server/public/assets/fonts/MaterialIcons-Regular.otf +0 -0
  27. package/server/public/flutter_bootstrap.js +2 -2
  28. package/server/public/main.dart.js +77723 -78469
  29. package/server/routes/admin.js +73 -24
  30. package/server/routes/mcp.js +9 -0
  31. package/server/routes/memory.js +35 -2
  32. package/server/routes/runtime.js +5 -2
  33. package/server/routes/settings.js +34 -1
  34. package/server/routes/skills.js +42 -0
  35. package/server/routes/task_webhooks.js +65 -0
  36. package/server/services/ai/engine.js +525 -24
  37. package/server/services/ai/learning.js +56 -8
  38. package/server/services/ai/models.js +4 -4
  39. package/server/services/ai/providers/anthropic.js +40 -8
  40. package/server/services/ai/providers/google.js +10 -0
  41. package/server/services/ai/providers/openai.js +3 -15
  42. package/server/services/ai/providers/openaiCompatible.js +11 -0
  43. package/server/services/ai/repetitionGuard.js +60 -0
  44. package/server/services/ai/systemPrompt.js +52 -16
  45. package/server/services/ai/taskAnalysis.js +15 -6
  46. package/server/services/ai/toolEvidence.js +3 -1
  47. package/server/services/ai/toolRunner.js +43 -1
  48. package/server/services/ai/toolSelector.js +92 -46
  49. package/server/services/ai/tools.js +89 -9
  50. package/server/services/ai/usage.js +114 -0
  51. package/server/services/manager.js +34 -0
  52. package/server/services/memory/manager.js +170 -4
  53. package/server/services/security/capability_audit.js +107 -0
  54. package/server/services/tasks/adapters/index.js +1 -0
  55. package/server/services/tasks/adapters/webhook.js +14 -0
  56. package/server/services/tasks/runtime.js +1 -1
  57. package/server/services/tasks/webhooks.js +146 -0
  58. package/server/services/workspace/code_navigation.js +194 -0
  59. package/server/services/workspace/structured_data.js +93 -0
  60. package/server/utils/cloud-security.js +24 -2
  61. package/flutter_app/lib/src/analytics_service.dart +0 -294
  62. package/server/config/analytics.js +0 -30
@@ -416,20 +416,69 @@ router.delete('/api/settings/apikey', requireAdminAuth, settingsLimiter, (req, r
416
416
 
417
417
  router.get('/api/analytics', requireAdminAuth, (req, res) => {
418
418
  const db = require('../db/database');
419
+ const range = Math.min(Math.max(parseInt(req.query.range) || 30, 1), 365);
419
420
  const now = new Date().toISOString();
420
421
  const dayAgo = new Date(Date.now() - 86_400_000).toISOString();
421
422
  const weekAgo = new Date(Date.now() - 7 * 86_400_000).toISOString();
423
+ const rangeAgo = new Date(Date.now() - range * 86_400_000).toISOString();
422
424
  try {
425
+ const totalRunsRow = db.prepare('SELECT COUNT(*) AS n, COALESCE(SUM(total_tokens),0) AS t FROM agent_runs').get();
426
+ const successRow = db.prepare("SELECT COUNT(*) AS n FROM agent_runs WHERE status = 'completed'").get();
423
427
  const stats = {
424
- totalUsers: db.prepare('SELECT COUNT(*) AS n FROM users').get().n,
425
- activeToday: db.prepare('SELECT COUNT(*) AS n FROM users WHERE last_login > ?').get(dayAgo).n,
426
- newThisWeek: db.prepare('SELECT COUNT(*) AS n FROM users WHERE created_at > ?').get(weekAgo).n,
427
- totalRuns: db.prepare('SELECT COUNT(*) AS n FROM agent_runs').get().n,
428
- runsToday: db.prepare('SELECT COUNT(*) AS n FROM agent_runs WHERE created_at > ?').get(dayAgo).n,
429
- totalTokens: db.prepare('SELECT COALESCE(SUM(total_tokens),0) AS n FROM agent_runs').get().n,
430
- activeSessions:db.prepare('SELECT COUNT(*) AS n FROM user_sessions WHERE revoked_at IS NULL AND expires_at > ?').get(now).n,
431
- totalStorage: db.prepare('SELECT COALESCE(SUM(byte_size),0) AS n FROM artifacts').get().n,
428
+ totalUsers: db.prepare('SELECT COUNT(*) AS n FROM users').get().n,
429
+ activeToday: db.prepare('SELECT COUNT(*) AS n FROM users WHERE last_login > ?').get(dayAgo).n,
430
+ newThisWeek: db.prepare('SELECT COUNT(*) AS n FROM users WHERE created_at > ?').get(weekAgo).n,
431
+ totalRuns: totalRunsRow.n,
432
+ runsToday: db.prepare('SELECT COUNT(*) AS n FROM agent_runs WHERE created_at > ?').get(dayAgo).n,
433
+ runsThisWeek: db.prepare('SELECT COUNT(*) AS n FROM agent_runs WHERE created_at > ?').get(weekAgo).n,
434
+ totalTokens: totalRunsRow.t,
435
+ tokensToday: db.prepare("SELECT COALESCE(SUM(total_tokens),0) AS n FROM agent_runs WHERE created_at > ?").get(dayAgo).n,
436
+ avgTokensPerRun: totalRunsRow.n > 0 ? Math.round(totalRunsRow.t / totalRunsRow.n) : 0,
437
+ successRate: totalRunsRow.n > 0 ? Math.round((successRow.n / totalRunsRow.n) * 100) : 0,
438
+ activeSessions: db.prepare('SELECT COUNT(*) AS n FROM user_sessions WHERE revoked_at IS NULL AND expires_at > ?').get(now).n,
439
+ totalStorage: db.prepare('SELECT COALESCE(SUM(byte_size),0) AS n FROM artifacts').get().n,
432
440
  };
441
+
442
+ // Time-series: runs + tokens per day for selected range
443
+ const runsByDay = db.prepare(`
444
+ SELECT date(created_at) AS date,
445
+ COUNT(*) AS runs,
446
+ COALESCE(SUM(total_tokens), 0) AS tokens
447
+ FROM agent_runs
448
+ WHERE created_at > ?
449
+ GROUP BY date(created_at)
450
+ ORDER BY date
451
+ `).all(rangeAgo);
452
+
453
+ // New users per day for selected range
454
+ const usersByDay = db.prepare(`
455
+ SELECT date(created_at) AS date, COUNT(*) AS newUsers
456
+ FROM users
457
+ WHERE created_at > ?
458
+ GROUP BY date(created_at)
459
+ ORDER BY date
460
+ `).all(rangeAgo);
461
+
462
+ // Model breakdown (top 10 by runs)
463
+ const modelBreakdown = db.prepare(`
464
+ SELECT COALESCE(model, 'unknown') AS model,
465
+ COUNT(*) AS runs,
466
+ COALESCE(SUM(total_tokens), 0) AS tokens
467
+ FROM agent_runs
468
+ WHERE created_at > ?
469
+ GROUP BY model
470
+ ORDER BY runs DESC
471
+ LIMIT 10
472
+ `).all(rangeAgo);
473
+
474
+ // Run status breakdown
475
+ const statusBreakdown = db.prepare(`
476
+ SELECT status, COUNT(*) AS count
477
+ FROM agent_runs
478
+ GROUP BY status
479
+ ORDER BY count DESC
480
+ `).all();
481
+
433
482
  const topUsers = db.prepare(`
434
483
  SELECT u.id, u.username, u.display_name,
435
484
  COALESCE(r.runs, 0) AS runs,
@@ -437,25 +486,25 @@ router.get('/api/analytics', requireAdminAuth, (req, res) => {
437
486
  COALESCE(a.storage, 0) AS storage
438
487
  FROM users u
439
488
  LEFT JOIN (
440
- SELECT user_id,
441
- COUNT(*) AS runs,
442
- COALESCE(SUM(total_tokens),0) AS tokens
489
+ SELECT user_id, COUNT(*) AS runs, COALESCE(SUM(total_tokens),0) AS tokens
443
490
  FROM agent_runs GROUP BY user_id
444
491
  ) r ON r.user_id = u.id
445
492
  LEFT JOIN (
446
493
  SELECT user_id, COALESCE(SUM(byte_size),0) AS storage
447
494
  FROM artifacts GROUP BY user_id
448
495
  ) a ON a.user_id = u.id
449
- ORDER BY runs DESC LIMIT 8
496
+ ORDER BY tokens DESC LIMIT 10
450
497
  `).all();
498
+
451
499
  const recentRuns = db.prepare(`
452
- SELECT r.id, u.username, r.title, r.status, r.total_tokens,
500
+ SELECT r.id, u.username, r.title, r.status, r.model, r.total_tokens,
453
501
  r.created_at, r.completed_at
454
502
  FROM agent_runs r
455
503
  JOIN users u ON u.id = r.user_id
456
- ORDER BY r.created_at DESC LIMIT 20
504
+ ORDER BY r.created_at DESC LIMIT 25
457
505
  `).all();
458
- res.json({ stats, topUsers, recentRuns });
506
+
507
+ res.json({ stats, runsByDay, usersByDay, modelBreakdown, statusBreakdown, topUsers, recentRuns });
459
508
  } catch (err) {
460
509
  res.status(500).json({ error: String(err.message || err) });
461
510
  }
@@ -645,20 +694,20 @@ router.get('/api/models', requireAdminAuth, async (req, res) => {
645
694
  const { getSupportedModels } = require('../services/ai/models');
646
695
  try {
647
696
  const models = await getSupportedModels(null, null);
648
- const enabledModelsStr = process.env.NEOAGENT_ENABLED_MODELS || '';
649
- const enabledModelsList = enabledModelsStr ? enabledModelsStr.split(',').map(s => s.trim()).filter(s => s.length > 0) : [];
650
- res.json({ models, enabledModels: enabledModelsList });
697
+ const disabledStr = process.env.NEOAGENT_DISABLED_MODELS || '';
698
+ const disabledModels = disabledStr ? disabledStr.split(',').map(s => s.trim()).filter(Boolean) : [];
699
+ res.json({ models, disabledModels });
651
700
  } catch (err) {
652
701
  res.status(500).json({ error: String(err.message || err) });
653
702
  }
654
703
  });
655
704
 
656
- router.put('/api/models/enabled', requireAdminAuth, express.json(), (req, res) => {
657
- const { enabledModels } = req.body || {};
658
- if (!Array.isArray(enabledModels)) return res.status(400).json({ error: 'enabledModels must be an array' });
659
- const value = enabledModels.join(',');
660
- upsertEnvValue(ENV_FILE, 'NEOAGENT_ENABLED_MODELS', value);
661
- process.env.NEOAGENT_ENABLED_MODELS = value;
705
+ router.put('/api/models/config', requireAdminAuth, express.json(), (req, res) => {
706
+ const { disabledModels } = req.body || {};
707
+ if (!Array.isArray(disabledModels)) return res.status(400).json({ error: 'disabledModels must be an array' });
708
+ const value = disabledModels.join(',');
709
+ upsertEnvValue(ENV_FILE, 'NEOAGENT_DISABLED_MODELS', value);
710
+ process.env.NEOAGENT_DISABLED_MODELS = value;
662
711
  res.json({ ok: true });
663
712
  });
664
713
 
@@ -20,6 +20,15 @@ function getTrustedPostMessageOrigin(req) {
20
20
 
21
21
  router.use(requireAuth);
22
22
 
23
+ router.get('/audit', (req, res) => {
24
+ try {
25
+ const agentId = resolveAgentId(req.session.userId, getAgentIdFromRequest(req));
26
+ res.json(req.app.locals.capabilityAuditService.auditMcp(req.session.userId, { agentId }));
27
+ } catch (err) {
28
+ res.status(500).json({ error: sanitizeError(err) });
29
+ }
30
+ });
31
+
23
32
  // List configured MCP servers
24
33
  router.get('/', (req, res) => {
25
34
  const agentId = resolveAgentId(req.session.userId, getAgentIdFromRequest(req));
@@ -226,6 +226,39 @@ router.get('/entities', (req, res) => {
226
226
  }
227
227
  });
228
228
 
229
+ router.get('/facts', (req, res) => {
230
+ const mm = req.app.locals.memoryManager;
231
+ const userId = req.session.userId;
232
+ const agentId = resolveAgentId(userId, getAgentIdFromRequest(req));
233
+ try {
234
+ res.json(mm.listFacts(userId, {
235
+ agentId,
236
+ status: req.query.status || '',
237
+ subject: req.query.subject || '',
238
+ predicate: req.query.predicate || '',
239
+ validAt: req.query.validAt || '',
240
+ knownAt: req.query.knownAt || '',
241
+ limit: req.query.limit,
242
+ }));
243
+ } catch (err) {
244
+ res.status(500).json({ error: sanitizeError(err) });
245
+ }
246
+ });
247
+
248
+ router.post('/facts/reconcile', (req, res) => {
249
+ const mm = req.app.locals.memoryManager;
250
+ const userId = req.session.userId;
251
+ const agentId = resolveAgentId(userId, getAgentIdFromRequest(req));
252
+ try {
253
+ res.json(mm.reconcileFacts(userId, {
254
+ agentId,
255
+ minimumConfidence: req.body?.minimumConfidence,
256
+ }));
257
+ } catch (err) {
258
+ res.status(500).json({ error: sanitizeError(err) });
259
+ }
260
+ });
261
+
229
262
  router.post('/knowledge-views/materialize', (req, res) => {
230
263
  const mm = req.app.locals.memoryManager;
231
264
  const userId = req.session.userId;
@@ -424,7 +457,7 @@ router.post('/transfer-import', transferImportLimiter, async (req, res) => {
424
457
  if (applyCoreMemory && parsed.coreEntries) {
425
458
  for (const [key, value] of Object.entries(parsed.coreEntries)) {
426
459
  if (!key || key === 'active_context') continue;
427
- mm.updateCore(userId, key, value, { agentId });
460
+ mm.updateCore(userId, key, value, { agentId, confirmed: true });
428
461
  coreUpdatedCount += 1;
429
462
  }
430
463
  }
@@ -456,7 +489,7 @@ router.put('/core/:key', (req, res) => {
456
489
  if (req.params.key === 'active_context') {
457
490
  return res.status(400).json({ error: 'active_context is no longer a supported core memory key' });
458
491
  }
459
- mm.updateCore(userId, req.params.key, value, { agentId });
492
+ mm.updateCore(userId, req.params.key, value, { agentId, confirmed: true });
460
493
  res.json({ success: true });
461
494
  });
462
495
 
@@ -1,13 +1,16 @@
1
1
  'use strict';
2
2
 
3
3
  const express = require('express');
4
- const { getAnalyticsConfig } = require('../config/analytics');
5
4
 
6
5
  const router = express.Router();
7
6
 
8
7
  router.get('/config', (req, res) => {
9
8
  res.json({
10
- analytics: getAnalyticsConfig(),
9
+ analytics: {
10
+ enabled: false,
11
+ provider: null,
12
+ requiresConsent: true,
13
+ },
11
14
  });
12
15
  });
13
16
 
@@ -410,6 +410,23 @@ router.get('/token-usage/summary', (req, res) => {
410
410
  return acc;
411
411
  }, { count: 0, system: 0, tools: 0, history: 0, recall: 0, replay: 0 });
412
412
 
413
+ const normalizedUsage = db.prepare(`
414
+ SELECT
415
+ COALESCE(SUM(input_tokens), 0) AS inputTokens,
416
+ COALESCE(SUM(output_tokens), 0) AS outputTokens,
417
+ COALESCE(SUM(reasoning_tokens), 0) AS reasoningTokens,
418
+ COALESCE(SUM(cached_read_tokens), 0) AS cachedReadTokens,
419
+ COALESCE(SUM(cache_write_tokens), 0) AS cacheWriteTokens,
420
+ COALESCE(SUM(total_tokens), 0) AS totalTokens,
421
+ SUM(estimated_cost_usd) AS estimatedCostUsd,
422
+ COUNT(estimated_cost_usd) AS pricedCallCount,
423
+ COUNT(*) AS callCount
424
+ FROM agent_model_usage
425
+ WHERE user_id = ? AND agent_id = ?
426
+ `).get(userId, agentId);
427
+ const cacheEligibleTokens = Number(normalizedUsage?.inputTokens || 0)
428
+ + Number(normalizedUsage?.cachedReadTokens || 0);
429
+
413
430
  res.json({
414
431
  totals: {
415
432
  totalTokens: Number(totals?.totalTokens || 0),
@@ -429,7 +446,23 @@ router.get('/token-usage/summary', (req, res) => {
429
446
  toolReplayTokens: Math.round(metricTotals.replay / metricTotals.count)
430
447
  },
431
448
  latest: parsedMetrics[0] || null
432
- }
449
+ },
450
+ modelUsage: {
451
+ inputTokens: Number(normalizedUsage?.inputTokens || 0),
452
+ outputTokens: Number(normalizedUsage?.outputTokens || 0),
453
+ reasoningTokens: Number(normalizedUsage?.reasoningTokens || 0),
454
+ cachedReadTokens: Number(normalizedUsage?.cachedReadTokens || 0),
455
+ cacheWriteTokens: Number(normalizedUsage?.cacheWriteTokens || 0),
456
+ totalTokens: Number(normalizedUsage?.totalTokens || 0),
457
+ estimatedCostUsd: Number(normalizedUsage?.pricedCallCount || 0) > 0
458
+ ? Number(normalizedUsage.estimatedCostUsd)
459
+ : null,
460
+ pricedCallCount: Number(normalizedUsage?.pricedCallCount || 0),
461
+ callCount: Number(normalizedUsage?.callCount || 0),
462
+ cacheHitRatio: cacheEligibleTokens > 0
463
+ ? Number(normalizedUsage?.cachedReadTokens || 0) / cacheEligibleTokens
464
+ : 0,
465
+ },
433
466
  });
434
467
  });
435
468
 
@@ -2,6 +2,7 @@ const express = require('express');
2
2
  const router = express.Router();
3
3
  const { requireAuth } = require('../middleware/auth');
4
4
  const { sanitizeError } = require('../utils/security');
5
+ const db = require('../db/database');
5
6
  const {
6
7
  getSkillRunner,
7
8
  serializeInstalledSkill,
@@ -62,6 +63,47 @@ router.get('/', async (req, res) => {
62
63
  }
63
64
  });
64
65
 
66
+ router.get('/metrics/summary', (req, res) => {
67
+ try {
68
+ const rows = db.prepare(
69
+ `SELECT skill_name, invocation_count, success_count, failure_count,
70
+ correction_count, total_tokens, last_used_at
71
+ FROM skill_metrics WHERE user_id = ?
72
+ ORDER BY invocation_count DESC, skill_name ASC`
73
+ ).all(req.session.userId);
74
+ res.json(rows.map((row) => {
75
+ const invocations = Number(row.invocation_count || 0);
76
+ const successes = Number(row.success_count || 0);
77
+ const failures = Number(row.failure_count || 0);
78
+ let recommendation = 'keep';
79
+ if (invocations >= 5 && failures / invocations >= 0.4) recommendation = 'improve_or_disable';
80
+ else if (invocations >= 10 && successes === 0) recommendation = 'disable_or_delete';
81
+ else if (invocations === 0) recommendation = 'review_unused';
82
+ return {
83
+ skillName: row.skill_name,
84
+ invocationCount: invocations,
85
+ successCount: successes,
86
+ failureCount: failures,
87
+ correctionCount: Number(row.correction_count || 0),
88
+ totalTokens: Number(row.total_tokens || 0),
89
+ successRate: invocations > 0 ? successes / invocations : null,
90
+ lastUsedAt: row.last_used_at,
91
+ recommendation,
92
+ };
93
+ }));
94
+ } catch (err) {
95
+ res.status(500).json({ error: sanitizeError(err) });
96
+ }
97
+ });
98
+
99
+ router.get('/audit/summary', (req, res) => {
100
+ try {
101
+ res.json(req.app.locals.capabilityAuditService.auditSkills());
102
+ } catch (err) {
103
+ res.status(500).json({ error: sanitizeError(err) });
104
+ }
105
+ });
106
+
65
107
  router.get('/:name', async (req, res) => {
66
108
  try {
67
109
  const runner = await getSkillRunner(req.app);
@@ -0,0 +1,65 @@
1
+ 'use strict';
2
+
3
+ const express = require('express');
4
+ const { requireAuth } = require('../middleware/auth');
5
+ const { sanitizeError } = require('../utils/security');
6
+
7
+ const router = express.Router();
8
+
9
+ function parseTaskId(value) {
10
+ const taskId = Number.parseInt(value, 10);
11
+ if (!Number.isInteger(taskId) || taskId <= 0) throw new Error('Invalid task id.');
12
+ return taskId;
13
+ }
14
+
15
+ router.post('/:taskId/deliver', async (req, res) => {
16
+ try {
17
+ const result = await req.app.locals.taskWebhookService.deliver(
18
+ parseTaskId(req.params.taskId),
19
+ req.headers,
20
+ req.rawBody || JSON.stringify(req.body || {}),
21
+ req.body || {},
22
+ );
23
+ res.status(202).json(result);
24
+ } catch (err) {
25
+ res.status(err.status || 400).json({ error: sanitizeError(err) });
26
+ }
27
+ });
28
+
29
+ router.use(requireAuth);
30
+
31
+ router.get('/:taskId', (req, res) => {
32
+ try {
33
+ res.json(req.app.locals.taskWebhookService.getConfiguration(
34
+ req.session.userId,
35
+ parseTaskId(req.params.taskId),
36
+ ));
37
+ } catch (err) {
38
+ res.status(err.status || 400).json({ error: sanitizeError(err) });
39
+ }
40
+ });
41
+
42
+ router.post('/:taskId/rotate', (req, res) => {
43
+ try {
44
+ res.json(req.app.locals.taskWebhookService.rotateSecret(
45
+ req.session.userId,
46
+ parseTaskId(req.params.taskId),
47
+ ));
48
+ } catch (err) {
49
+ res.status(err.status || 400).json({ error: sanitizeError(err) });
50
+ }
51
+ });
52
+
53
+ router.get('/:taskId/deliveries', (req, res) => {
54
+ try {
55
+ res.json(req.app.locals.taskWebhookService.listDeliveries(
56
+ req.session.userId,
57
+ parseTaskId(req.params.taskId),
58
+ req.query?.limit,
59
+ ));
60
+ } catch (err) {
61
+ res.status(err.status || 400).json({ error: sanitizeError(err) });
62
+ }
63
+ });
64
+
65
+ module.exports = router;