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.
- package/README.md +48 -18
- package/flutter_app/lib/main_models.dart +28 -0
- package/flutter_app/lib/main_settings.dart +9 -0
- package/flutter_app/lib/src/backend_client.dart +8 -0
- package/package.json +1 -1
- package/server/db/database.js +112 -0
- package/server/http/routes.js +1 -0
- package/server/public/.last_build_id +1 -1
- package/server/public/flutter_bootstrap.js +2 -2
- package/server/public/main.dart.js +10069 -10049
- package/server/routes/mcp.js +9 -0
- package/server/routes/memory.js +35 -2
- package/server/routes/runtime.js +7 -1
- package/server/routes/settings.js +34 -1
- package/server/routes/skills.js +42 -0
- package/server/routes/task_webhooks.js +65 -0
- package/server/services/ai/engine.js +525 -24
- package/server/services/ai/learning.js +56 -8
- package/server/services/ai/providers/anthropic.js +40 -8
- package/server/services/ai/providers/google.js +10 -0
- package/server/services/ai/providers/openai.js +3 -15
- package/server/services/ai/providers/openaiCompatible.js +11 -0
- package/server/services/ai/repetitionGuard.js +60 -0
- package/server/services/ai/systemPrompt.js +52 -16
- package/server/services/ai/taskAnalysis.js +15 -6
- package/server/services/ai/toolEvidence.js +3 -1
- package/server/services/ai/toolRunner.js +43 -1
- package/server/services/ai/toolSelector.js +92 -46
- package/server/services/ai/tools.js +89 -9
- package/server/services/ai/usage.js +114 -0
- package/server/services/manager.js +34 -0
- package/server/services/memory/manager.js +170 -4
- package/server/services/security/capability_audit.js +107 -0
- package/server/services/tasks/adapters/index.js +1 -0
- package/server/services/tasks/adapters/webhook.js +14 -0
- package/server/services/tasks/runtime.js +1 -1
- package/server/services/tasks/webhooks.js +146 -0
- package/server/services/workspace/code_navigation.js +194 -0
- package/server/services/workspace/structured_data.js +93 -0
- 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
|
-
|
|
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
|
|
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;
|