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,98 @@
|
|
|
1
|
+
const express = require('express');
|
|
2
|
+
const router = express.Router();
|
|
3
|
+
const db = require('../db/database');
|
|
4
|
+
const { requireAuth } = require('../middleware/auth');
|
|
5
|
+
|
|
6
|
+
router.use(requireAuth);
|
|
7
|
+
|
|
8
|
+
// Get supported models metadata
|
|
9
|
+
router.get('/meta/models', (req, res) => {
|
|
10
|
+
const { SUPPORTED_MODELS } = require('../services/ai/models');
|
|
11
|
+
res.json({ models: SUPPORTED_MODELS });
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
// Get all settings
|
|
15
|
+
router.get('/', (req, res) => {
|
|
16
|
+
const rows = db.prepare('SELECT key, value FROM user_settings WHERE user_id = ?').all(req.session.userId);
|
|
17
|
+
const settings = {};
|
|
18
|
+
for (const row of rows) {
|
|
19
|
+
try {
|
|
20
|
+
settings[row.key] = JSON.parse(row.value);
|
|
21
|
+
} catch (e) {
|
|
22
|
+
if (typeof row.value === 'string' && (row.value.trim().startsWith('{') || row.value.trim().startsWith('['))) {
|
|
23
|
+
console.warn(`[Settings] Failed to parse '${row.key}' as JSON, treating as raw string. Error:`, e.message);
|
|
24
|
+
}
|
|
25
|
+
settings[row.key] = row.value;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
res.json(settings);
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
// Update settings (batch)
|
|
32
|
+
router.put('/', (req, res) => {
|
|
33
|
+
const userId = req.session.userId;
|
|
34
|
+
const upsert = db.prepare('INSERT INTO user_settings (user_id, key, value) VALUES (?, ?, ?) ON CONFLICT(user_id, key) DO UPDATE SET value = excluded.value');
|
|
35
|
+
|
|
36
|
+
const tx = db.transaction((entries) => {
|
|
37
|
+
for (const [key, value] of entries) {
|
|
38
|
+
const v = typeof value === 'string' ? value : JSON.stringify(value);
|
|
39
|
+
upsert.run(userId, key, v);
|
|
40
|
+
}
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
tx(Object.entries(req.body));
|
|
44
|
+
|
|
45
|
+
// Apply headless toggle immediately without restarting
|
|
46
|
+
if ('headless_browser' in req.body) {
|
|
47
|
+
const bc = req.app.locals.browserController;
|
|
48
|
+
if (bc) bc.setHeadless(req.body.headless_browser).catch(() => { });
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
res.json({ success: true });
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
// Get single setting
|
|
55
|
+
router.get('/:key', (req, res) => {
|
|
56
|
+
const row = db.prepare('SELECT value FROM user_settings WHERE user_id = ? AND key = ?').get(req.session.userId, req.params.key);
|
|
57
|
+
if (!row) return res.json({ value: null });
|
|
58
|
+
try {
|
|
59
|
+
res.json({ value: JSON.parse(row.value) });
|
|
60
|
+
} catch (e) {
|
|
61
|
+
if (typeof row.value === 'string' && (row.value.trim().startsWith('{') || row.value.trim().startsWith('['))) {
|
|
62
|
+
console.warn(`[Settings] Failed to parse '${req.params.key}' as JSON, returning as raw string. Error:`, e.message);
|
|
63
|
+
}
|
|
64
|
+
res.json({ value: row.value });
|
|
65
|
+
}
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
// Set single setting
|
|
69
|
+
router.put('/:key', (req, res) => {
|
|
70
|
+
const v = typeof req.body.value === 'string' ? req.body.value : JSON.stringify(req.body.value);
|
|
71
|
+
db.prepare('INSERT INTO user_settings (user_id, key, value) VALUES (?, ?, ?) ON CONFLICT(user_id, key) DO UPDATE SET value = excluded.value')
|
|
72
|
+
.run(req.session.userId, req.params.key, v);
|
|
73
|
+
res.json({ success: true });
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
// Delete setting
|
|
77
|
+
router.delete('/:key', (req, res) => {
|
|
78
|
+
db.prepare('DELETE FROM user_settings WHERE user_id = ? AND key = ?').run(req.session.userId, req.params.key);
|
|
79
|
+
res.json({ success: true });
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
// Trigger auto-update script
|
|
83
|
+
router.post('/update', (req, res) => {
|
|
84
|
+
const { spawn } = require('child_process');
|
|
85
|
+
console.log('[Settings] Triggering neoagent update...');
|
|
86
|
+
|
|
87
|
+
// Spawn the update command in detached mode so it survives the node process exiting
|
|
88
|
+
const child = spawn(process.execPath, ['bin/neoagent.js', 'update'], {
|
|
89
|
+
detached: true,
|
|
90
|
+
stdio: 'ignore',
|
|
91
|
+
cwd: process.cwd()
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
child.unref();
|
|
95
|
+
res.json({ success: true, message: 'Update triggered' });
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
module.exports = router;
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
const express = require('express');
|
|
2
|
+
const router = express.Router();
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
const { requireAuth } = require('../middleware/auth');
|
|
6
|
+
|
|
7
|
+
const SKILLS_DIR = path.join(__dirname, '../../agent-data/skills');
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Resolve a client-supplied filename to an absolute path inside SKILLS_DIR.
|
|
11
|
+
* Returns null if the resolved path escapes the directory (path traversal attempt).
|
|
12
|
+
*/
|
|
13
|
+
function safeSkillPath(filename) {
|
|
14
|
+
if (!filename || typeof filename !== 'string') return null;
|
|
15
|
+
// Strip any directory components – only the basename is allowed
|
|
16
|
+
const base = path.basename(filename);
|
|
17
|
+
if (!base || base === '.' || base === '..') return null;
|
|
18
|
+
const resolved = path.resolve(SKILLS_DIR, base);
|
|
19
|
+
// Ensure the resolved path stays inside SKILLS_DIR
|
|
20
|
+
if (!resolved.startsWith(SKILLS_DIR + path.sep) && resolved !== SKILLS_DIR) return null;
|
|
21
|
+
return resolved;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
router.use(requireAuth);
|
|
25
|
+
|
|
26
|
+
// List all skills
|
|
27
|
+
router.get('/', (req, res) => {
|
|
28
|
+
if (!fs.existsSync(SKILLS_DIR)) fs.mkdirSync(SKILLS_DIR, { recursive: true });
|
|
29
|
+
|
|
30
|
+
const files = fs.readdirSync(SKILLS_DIR).filter(f => f.endsWith('.md'));
|
|
31
|
+
const skills = files.map(f => {
|
|
32
|
+
const content = fs.readFileSync(path.join(SKILLS_DIR, f), 'utf-8');
|
|
33
|
+
const meta = parseSkillMeta(content);
|
|
34
|
+
return {
|
|
35
|
+
filename: f,
|
|
36
|
+
name: meta.name || f.replace('.md', ''),
|
|
37
|
+
description: meta.description || '',
|
|
38
|
+
trigger: meta.trigger || '',
|
|
39
|
+
enabled: meta.enabled !== false,
|
|
40
|
+
category: meta.category || 'general'
|
|
41
|
+
};
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
res.json(skills);
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
// Get a specific skill
|
|
48
|
+
router.get('/:filename', (req, res) => {
|
|
49
|
+
const fp = safeSkillPath(req.params.filename);
|
|
50
|
+
if (!fp || !fs.existsSync(fp)) return res.status(404).json({ error: 'Skill not found' });
|
|
51
|
+
|
|
52
|
+
const content = fs.readFileSync(fp, 'utf-8');
|
|
53
|
+
res.json({ filename: req.params.filename, content, meta: parseSkillMeta(content) });
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
// Create a new skill
|
|
57
|
+
router.post('/', (req, res) => {
|
|
58
|
+
const { filename, content } = req.body;
|
|
59
|
+
if (!filename || !content) return res.status(400).json({ error: 'filename and content required' });
|
|
60
|
+
|
|
61
|
+
const baseName = filename.replace(/\.md$/i, '').replace(/[^a-zA-Z0-9_.-]/g, '');
|
|
62
|
+
const safeName = baseName + '.md';
|
|
63
|
+
const fp = path.join(SKILLS_DIR, safeName);
|
|
64
|
+
|
|
65
|
+
if (!fs.existsSync(SKILLS_DIR)) fs.mkdirSync(SKILLS_DIR, { recursive: true });
|
|
66
|
+
|
|
67
|
+
fs.writeFileSync(fp, content, 'utf-8');
|
|
68
|
+
res.status(201).json({ filename: safeName, meta: parseSkillMeta(content) });
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
// Update a skill
|
|
72
|
+
router.put('/:filename', (req, res) => {
|
|
73
|
+
const fp = safeSkillPath(req.params.filename);
|
|
74
|
+
if (!fp || !fs.existsSync(fp)) return res.status(404).json({ error: 'Skill not found' });
|
|
75
|
+
|
|
76
|
+
fs.writeFileSync(fp, req.body.content, 'utf-8');
|
|
77
|
+
res.json({ filename: path.basename(fp), meta: parseSkillMeta(req.body.content) });
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
// Delete a skill
|
|
81
|
+
router.delete('/:filename', (req, res) => {
|
|
82
|
+
const fp = safeSkillPath(req.params.filename);
|
|
83
|
+
if (!fp || !fs.existsSync(fp)) return res.status(404).json({ error: 'Skill not found' });
|
|
84
|
+
|
|
85
|
+
fs.unlinkSync(fp);
|
|
86
|
+
res.json({ success: true });
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
function parseSkillMeta(content) {
|
|
90
|
+
const meta = {};
|
|
91
|
+
const match = content.match(/^---\n([\s\S]*?)\n---/);
|
|
92
|
+
if (!match) return meta;
|
|
93
|
+
|
|
94
|
+
const lines = match[1].split('\n');
|
|
95
|
+
for (const line of lines) {
|
|
96
|
+
const colon = line.indexOf(':');
|
|
97
|
+
if (colon === -1) continue;
|
|
98
|
+
const key = line.slice(0, colon).trim();
|
|
99
|
+
let val = line.slice(colon + 1).trim();
|
|
100
|
+
if (val === 'true') val = true;
|
|
101
|
+
else if (val === 'false') val = false;
|
|
102
|
+
meta[key] = val;
|
|
103
|
+
}
|
|
104
|
+
return meta;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
module.exports = router;
|