neoagent 2.4.2-beta.5 → 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 (40) hide show
  1. package/README.md +48 -18
  2. package/flutter_app/lib/main_models.dart +28 -0
  3. package/flutter_app/lib/main_settings.dart +9 -0
  4. package/flutter_app/lib/src/backend_client.dart +8 -0
  5. package/package.json +1 -1
  6. package/server/db/database.js +112 -0
  7. package/server/http/routes.js +1 -0
  8. package/server/public/.last_build_id +1 -1
  9. package/server/public/flutter_bootstrap.js +2 -2
  10. package/server/public/main.dart.js +10069 -10049
  11. package/server/routes/mcp.js +9 -0
  12. package/server/routes/memory.js +35 -2
  13. package/server/routes/runtime.js +7 -1
  14. package/server/routes/settings.js +34 -1
  15. package/server/routes/skills.js +42 -0
  16. package/server/routes/task_webhooks.js +65 -0
  17. package/server/services/ai/engine.js +525 -24
  18. package/server/services/ai/learning.js +56 -8
  19. package/server/services/ai/providers/anthropic.js +40 -8
  20. package/server/services/ai/providers/google.js +10 -0
  21. package/server/services/ai/providers/openai.js +3 -15
  22. package/server/services/ai/providers/openaiCompatible.js +11 -0
  23. package/server/services/ai/repetitionGuard.js +60 -0
  24. package/server/services/ai/systemPrompt.js +52 -16
  25. package/server/services/ai/taskAnalysis.js +15 -6
  26. package/server/services/ai/toolEvidence.js +3 -1
  27. package/server/services/ai/toolRunner.js +43 -1
  28. package/server/services/ai/toolSelector.js +92 -46
  29. package/server/services/ai/tools.js +89 -9
  30. package/server/services/ai/usage.js +114 -0
  31. package/server/services/manager.js +34 -0
  32. package/server/services/memory/manager.js +170 -4
  33. package/server/services/security/capability_audit.js +107 -0
  34. package/server/services/tasks/adapters/index.js +1 -0
  35. package/server/services/tasks/adapters/webhook.js +14 -0
  36. package/server/services/tasks/runtime.js +1 -1
  37. package/server/services/tasks/webhooks.js +146 -0
  38. package/server/services/workspace/code_navigation.js +194 -0
  39. package/server/services/workspace/structured_data.js +93 -0
  40. package/server/utils/cloud-security.js +24 -2
@@ -0,0 +1,194 @@
1
+ 'use strict';
2
+
3
+ const fs = require('fs');
4
+ const path = require('path');
5
+ const crypto = require('crypto');
6
+ const { spawnSync } = require('child_process');
7
+ const { parseSync } = require('oxc-parser');
8
+ const db = require('../../db/database');
9
+ const {
10
+ getEmbedding,
11
+ cosineSimilarity,
12
+ serializeEmbedding,
13
+ deserializeEmbedding,
14
+ keywordSimilarity,
15
+ } = require('../memory/embeddings');
16
+
17
+ const CODE_EXTENSIONS = new Set(['.js', '.cjs', '.mjs', '.jsx', '.ts', '.tsx']);
18
+
19
+ function lineAtOffset(content, offset) {
20
+ return content.slice(0, Math.max(0, offset)).split('\n').length;
21
+ }
22
+
23
+ function declarationInfo(node) {
24
+ const target = node?.type === 'ExportNamedDeclaration' || node?.type === 'ExportDefaultDeclaration'
25
+ ? node.declaration
26
+ : node;
27
+ if (!target) return null;
28
+ if (['FunctionDeclaration', 'ClassDeclaration', 'TSEnumDeclaration', 'TSInterfaceDeclaration', 'TSTypeAliasDeclaration'].includes(target.type)) {
29
+ return { node: target, name: target.id?.name || 'default', type: target.type };
30
+ }
31
+ if (target.type === 'VariableDeclaration') {
32
+ const first = target.declarations?.[0];
33
+ if (first?.id?.name) return { node: target, name: first.id.name, type: target.type };
34
+ }
35
+ return null;
36
+ }
37
+
38
+ class CodeNavigationService {
39
+ constructor(options = {}) {
40
+ this.workspaceManager = options.workspaceManager;
41
+ }
42
+
43
+ lexical(userId, options = {}) {
44
+ const root = this.workspaceManager.resolvePath(userId, options.path || '.', 'path');
45
+ const query = String(options.query || '').trim();
46
+ if (!query) throw new Error('query is required.');
47
+ const args = ['--json', '--line-number', '--max-count', '100'];
48
+ if (options.fixed !== false) args.push('--fixed-strings');
49
+ if (options.include) args.push('--glob', String(options.include));
50
+ args.push(query, root);
51
+ const result = spawnSync('rg', args, { encoding: 'utf8', maxBuffer: 8 * 1024 * 1024 });
52
+ if (result.error?.code === 'ENOENT') {
53
+ return this.workspaceManager.searchFiles(userId, options);
54
+ }
55
+ const matches = String(result.stdout || '').split('\n').filter(Boolean).flatMap((line) => {
56
+ try {
57
+ const event = JSON.parse(line);
58
+ if (event.type !== 'match') return [];
59
+ const data = event.data;
60
+ return [{
61
+ path: path.relative(root, data.path.text).split(path.sep).join('/'),
62
+ line: data.line_number,
63
+ excerpt: String(data.lines.text || '').trim().slice(0, 1000),
64
+ submatches: data.submatches.map((item) => ({ start: item.start, end: item.end })),
65
+ }];
66
+ } catch {
67
+ return [];
68
+ }
69
+ });
70
+ return { mode: 'lexical', count: matches.length, results: matches };
71
+ }
72
+
73
+ extractStructure(userId, options = {}) {
74
+ const absolute = this.workspaceManager.resolvePath(userId, options.path || '', 'path');
75
+ const content = fs.readFileSync(absolute, 'utf8');
76
+ if (!CODE_EXTENSIONS.has(path.extname(absolute).toLowerCase())) {
77
+ throw new Error('Structural parsing currently supports JavaScript and TypeScript files.');
78
+ }
79
+ const parsed = parseSync(absolute, content);
80
+ const symbols = [];
81
+ for (const entry of parsed.program?.body || []) {
82
+ const info = declarationInfo(entry);
83
+ if (!info) continue;
84
+ const startLine = lineAtOffset(content, info.node.start);
85
+ const endLine = lineAtOffset(content, info.node.end);
86
+ symbols.push({
87
+ name: info.name,
88
+ type: info.type,
89
+ startLine,
90
+ endLine,
91
+ excerpt: content.slice(info.node.start, Math.min(info.node.end, info.node.start + 1600)),
92
+ });
93
+ }
94
+ return {
95
+ mode: 'structure',
96
+ path: absolute,
97
+ parseErrors: (parsed.errors || []).map((error) => String(error.message || error)).slice(0, 20),
98
+ symbols,
99
+ };
100
+ }
101
+
102
+ async indexWorkspace(userId, options = {}) {
103
+ const root = this.workspaceManager.resolvePath(userId, options.path || '.', 'path');
104
+ const files = [];
105
+ const walk = (dir) => {
106
+ if (files.length >= 500) return;
107
+ for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
108
+ if (entry.name === 'node_modules' || entry.name === '.git' || entry.name.startsWith('.neoagent-')) continue;
109
+ const fullPath = path.join(dir, entry.name);
110
+ if (entry.isDirectory()) walk(fullPath);
111
+ else if (entry.isFile() && CODE_EXTENSIONS.has(path.extname(entry.name).toLowerCase())) files.push(fullPath);
112
+ }
113
+ };
114
+ walk(root);
115
+ let indexed = 0;
116
+ for (const file of files) {
117
+ if (fs.statSync(file).size > 512 * 1024) continue;
118
+ const structure = this.extractStructure(userId, { path: file });
119
+ const relative = path.relative(this.workspaceManager._ensureWorkspaceRootSync(userId), file).split(path.sep).join('/');
120
+ for (const symbol of structure.symbols) {
121
+ const hash = crypto.createHash('sha256').update(symbol.excerpt).digest('hex');
122
+ const existing = db.prepare(
123
+ `SELECT id, content_hash FROM workspace_code_index
124
+ WHERE user_id = ? AND path = ? AND symbol = ? AND start_line = ?`
125
+ ).get(userId, relative, symbol.name, symbol.startLine);
126
+ if (existing?.content_hash === hash) continue;
127
+ const embedding = await getEmbedding(`${symbol.name}\n${symbol.excerpt}`);
128
+ db.prepare(
129
+ `INSERT INTO workspace_code_index (
130
+ user_id, path, symbol, symbol_type, start_line, end_line, content, content_hash, embedding
131
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
132
+ ON CONFLICT(user_id, path, symbol, start_line) DO UPDATE SET
133
+ symbol_type = excluded.symbol_type,
134
+ end_line = excluded.end_line,
135
+ content = excluded.content,
136
+ content_hash = excluded.content_hash,
137
+ embedding = excluded.embedding,
138
+ updated_at = datetime('now')`
139
+ ).run(
140
+ userId,
141
+ relative,
142
+ symbol.name,
143
+ symbol.type,
144
+ symbol.startLine,
145
+ symbol.endLine,
146
+ symbol.excerpt,
147
+ hash,
148
+ embedding ? serializeEmbedding(embedding) : null,
149
+ );
150
+ indexed += 1;
151
+ }
152
+ }
153
+ return { filesScanned: files.length, symbolsIndexed: indexed };
154
+ }
155
+
156
+ async semantic(userId, options = {}) {
157
+ const query = String(options.query || '').trim();
158
+ if (!query) throw new Error('query is required.');
159
+ const indexStatus = await this.indexWorkspace(userId, options);
160
+ const queryEmbedding = await getEmbedding(query);
161
+ const rows = db.prepare(
162
+ `SELECT path, symbol, symbol_type, start_line, end_line, content, embedding
163
+ FROM workspace_code_index WHERE user_id = ?`
164
+ ).all(userId);
165
+ const limit = Math.min(Math.max(Number(options.limit || 12), 1), 50);
166
+ const results = rows.map((row) => {
167
+ const vector = deserializeEmbedding(row.embedding);
168
+ const score = queryEmbedding && vector
169
+ ? cosineSimilarity(queryEmbedding, vector)
170
+ : keywordSimilarity(query, `${row.symbol || ''} ${row.content}`);
171
+ return {
172
+ path: row.path,
173
+ symbol: row.symbol,
174
+ symbolType: row.symbol_type,
175
+ startLine: row.start_line,
176
+ endLine: row.end_line,
177
+ score,
178
+ excerpt: row.content.slice(0, 1600),
179
+ };
180
+ }).filter((item) => item.score > 0).sort((a, b) => b.score - a.score).slice(0, limit);
181
+ return { mode: 'semantic', indexStatus, count: results.length, results };
182
+ }
183
+
184
+ navigate(userId, options = {}) {
185
+ const mode = String(options.mode || 'lexical');
186
+ if (mode === 'structure') return this.extractStructure(userId, options);
187
+ if (mode === 'semantic') return this.semantic(userId, options);
188
+ return this.lexical(userId, options);
189
+ }
190
+ }
191
+
192
+ module.exports = {
193
+ CodeNavigationService,
194
+ };
@@ -0,0 +1,93 @@
1
+ 'use strict';
2
+
3
+ const fs = require('fs');
4
+ const path = require('path');
5
+ const Database = require('better-sqlite3');
6
+
7
+ function parseDelimited(text, delimiter) {
8
+ const rows = [];
9
+ let row = [];
10
+ let value = '';
11
+ let quoted = false;
12
+ for (let index = 0; index < text.length; index += 1) {
13
+ const char = text[index];
14
+ if (char === '"') {
15
+ if (quoted && text[index + 1] === '"') {
16
+ value += '"';
17
+ index += 1;
18
+ } else {
19
+ quoted = !quoted;
20
+ }
21
+ } else if (char === delimiter && !quoted) {
22
+ row.push(value);
23
+ value = '';
24
+ } else if ((char === '\n' || char === '\r') && !quoted) {
25
+ if (char === '\r' && text[index + 1] === '\n') index += 1;
26
+ row.push(value);
27
+ if (row.some((cell) => cell !== '')) rows.push(row);
28
+ row = [];
29
+ value = '';
30
+ } else {
31
+ value += char;
32
+ }
33
+ }
34
+ row.push(value);
35
+ if (row.some((cell) => cell !== '')) rows.push(row);
36
+ const headers = rows.shift() || [];
37
+ return rows.map((cells) => Object.fromEntries(headers.map((header, index) => [header, cells[index] ?? ''])));
38
+ }
39
+
40
+ class StructuredDataService {
41
+ constructor(options = {}) {
42
+ this.workspaceManager = options.workspaceManager;
43
+ }
44
+
45
+ query(userId, options = {}) {
46
+ const filePath = this.workspaceManager.resolvePath(userId, options.path || '', 'path');
47
+ const extension = path.extname(filePath).toLowerCase();
48
+ const limit = Math.min(Math.max(Number(options.limit || 100), 1), 1000);
49
+ if (extension === '.sqlite' || extension === '.db' || extension === '.sqlite3') {
50
+ const sql = String(options.sql || '').trim();
51
+ if (!sql) throw new Error('sql is required for SQLite data.');
52
+ const database = new Database(filePath, { readonly: true, fileMustExist: true });
53
+ try {
54
+ const statement = database.prepare(sql);
55
+ if (!statement.reader || !statement.readonly) throw new Error('Only read-only queries are allowed.');
56
+ return {
57
+ format: 'sqlite',
58
+ columns: statement.columns().map((column) => column.name),
59
+ rows: statement.all(options.parameters || {}).slice(0, limit),
60
+ };
61
+ } finally {
62
+ database.close();
63
+ }
64
+ }
65
+ let rows;
66
+ if (extension === '.json') {
67
+ const parsed = JSON.parse(fs.readFileSync(filePath, 'utf8'));
68
+ rows = Array.isArray(parsed) ? parsed : [parsed];
69
+ } else if (extension === '.csv' || extension === '.tsv') {
70
+ rows = parseDelimited(fs.readFileSync(filePath, 'utf8'), extension === '.tsv' ? '\t' : ',');
71
+ } else {
72
+ throw new Error('Supported structured-data formats are CSV, TSV, JSON, and SQLite.');
73
+ }
74
+ const selectedColumns = Array.isArray(options.columns) ? options.columns.map(String) : null;
75
+ const filtered = rows.filter((row) => {
76
+ if (!options.equals || typeof options.equals !== 'object') return true;
77
+ return Object.entries(options.equals).every(([key, value]) => row?.[key] === value);
78
+ }).slice(0, limit).map((row) => {
79
+ if (!selectedColumns) return row;
80
+ return Object.fromEntries(selectedColumns.map((column) => [column, row?.[column]]));
81
+ });
82
+ return {
83
+ format: extension.slice(1),
84
+ columns: filtered[0] && typeof filtered[0] === 'object' ? Object.keys(filtered[0]) : [],
85
+ rows: filtered,
86
+ };
87
+ }
88
+ }
89
+
90
+ module.exports = {
91
+ StructuredDataService,
92
+ parseDelimited,
93
+ };
@@ -28,14 +28,36 @@ const PRIVATE_IPV4 = [
28
28
 
29
29
  function isPrivateHost(hostname) {
30
30
  if (!hostname) return false;
31
- const h = hostname.toLowerCase().replace(/^\[|\]$/g, '');
31
+ let h = hostname.toLowerCase().replace(/^\[|\]$/g, '');
32
32
 
33
33
  if (h === 'localhost' || h === 'localhost.localdomain') return true;
34
34
  if (h.endsWith('.local') || h.endsWith('.internal') || h.endsWith('.localhost')) return true;
35
35
 
36
- // IPv6 loopback and link-local
36
+ // Unwrap IPv4-mapped/compatible IPv6 (e.g. ::ffff:127.0.0.1 or ::ffff:7f00:1)
37
+ // so the embedded IPv4 address is checked against the private ranges below.
38
+ const mapped = h.match(/^::(?:ffff:)?(?:0:)?([0-9a-f.:]+)$/);
39
+ if (mapped) {
40
+ const tail = mapped[1];
41
+ if (tail.includes('.')) {
42
+ // Dotted IPv4 form, e.g. ::ffff:127.0.0.1
43
+ h = tail;
44
+ } else {
45
+ // Hex form, e.g. ::ffff:7f00:1 -> reconstruct dotted IPv4.
46
+ const groups = tail.split(':');
47
+ if (groups.length === 2) {
48
+ const hi = parseInt(groups[0], 16);
49
+ const lo = parseInt(groups[1], 16);
50
+ if (Number.isFinite(hi) && Number.isFinite(lo) && hi <= 0xffff && lo <= 0xffff) {
51
+ h = `${(hi >> 8) & 0xff}.${hi & 0xff}.${(lo >> 8) & 0xff}.${lo & 0xff}`;
52
+ }
53
+ }
54
+ }
55
+ }
56
+
57
+ // IPv6 loopback, link-local and unique-local
37
58
  if (h === '::1' || h === '::') return true;
38
59
  if (h.startsWith('fe80:')) return true;
60
+ if (/^f[cd][0-9a-f]*:/.test(h)) return true; // fc00::/7 unique-local
39
61
 
40
62
  for (const pattern of PRIVATE_IPV4) {
41
63
  if (pattern.test(h)) return true;