morpheus-cli 0.4.15 → 0.5.1
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 +293 -1115
- package/dist/channels/telegram.js +379 -74
- package/dist/cli/commands/doctor.js +34 -0
- package/dist/cli/commands/init.js +128 -0
- package/dist/cli/commands/restart.js +32 -14
- package/dist/cli/commands/start.js +28 -12
- package/dist/config/manager.js +82 -0
- package/dist/config/mcp-manager.js +19 -1
- package/dist/config/schemas.js +9 -0
- package/dist/devkit/tools/network.js +1 -1
- package/dist/http/api.js +399 -10
- package/dist/runtime/apoc.js +25 -17
- package/dist/runtime/memory/sati/repository.js +30 -2
- package/dist/runtime/memory/sati/service.js +46 -15
- package/dist/runtime/memory/sati/system-prompts.js +71 -29
- package/dist/runtime/memory/session-embedding-worker.js +3 -3
- package/dist/runtime/memory/sqlite.js +24 -0
- package/dist/runtime/memory/trinity-db.js +203 -0
- package/dist/runtime/neo.js +124 -0
- package/dist/runtime/oracle.js +252 -205
- package/dist/runtime/providers/factory.js +1 -12
- package/dist/runtime/session-embedding-scheduler.js +1 -1
- package/dist/runtime/tasks/context.js +53 -0
- package/dist/runtime/tasks/dispatcher.js +91 -0
- package/dist/runtime/tasks/notifier.js +68 -0
- package/dist/runtime/tasks/repository.js +370 -0
- package/dist/runtime/tasks/types.js +1 -0
- package/dist/runtime/tasks/worker.js +99 -0
- package/dist/runtime/tools/__tests__/tools.test.js +1 -3
- package/dist/runtime/tools/apoc-tool.js +61 -8
- package/dist/runtime/tools/delegation-guard.js +29 -0
- package/dist/runtime/tools/factory.js +1 -1
- package/dist/runtime/tools/index.js +2 -3
- package/dist/runtime/tools/morpheus-tools.js +742 -0
- package/dist/runtime/tools/neo-tool.js +109 -0
- package/dist/runtime/tools/trinity-tool.js +98 -0
- package/dist/runtime/trinity-connector.js +611 -0
- package/dist/runtime/trinity-crypto.js +52 -0
- package/dist/runtime/trinity.js +246 -0
- package/dist/runtime/webhooks/dispatcher.js +10 -19
- package/dist/types/config.js +10 -0
- package/dist/ui/assets/index-DP2V4kRd.js +112 -0
- package/dist/ui/assets/index-mglRG5Zw.css +1 -0
- package/dist/ui/index.html +2 -2
- package/dist/ui/sw.js +1 -1
- package/package.json +6 -1
- package/dist/runtime/tools/analytics-tools.js +0 -139
- package/dist/runtime/tools/config-tools.js +0 -64
- package/dist/runtime/tools/diagnostic-tools.js +0 -153
- package/dist/ui/assets/index-LemKVRjC.js +0 -112
- package/dist/ui/assets/index-TCQ7VNYO.css +0 -1
|
@@ -60,7 +60,6 @@ export class SatiService {
|
|
|
60
60
|
const agent = await ProviderFactory.create(satiConfig, []);
|
|
61
61
|
// Get existing memories for context (Simulated "Working Memory" or full list if small)
|
|
62
62
|
const allMemories = this.repository.getAllMemories();
|
|
63
|
-
const existingSummaries = allMemories.slice(0, 50).map(m => m.summary);
|
|
64
63
|
// Map conversation to strict types and sanitize
|
|
65
64
|
const recentConversation = conversation.map(c => ({
|
|
66
65
|
role: (c.role === 'human' ? 'user' : c.role),
|
|
@@ -68,7 +67,12 @@ export class SatiService {
|
|
|
68
67
|
}));
|
|
69
68
|
const inputPayload = {
|
|
70
69
|
recent_conversation: recentConversation,
|
|
71
|
-
|
|
70
|
+
existing_memories: allMemories.map(m => ({
|
|
71
|
+
id: m.id,
|
|
72
|
+
category: m.category,
|
|
73
|
+
importance: m.importance,
|
|
74
|
+
summary: m.summary
|
|
75
|
+
}))
|
|
72
76
|
};
|
|
73
77
|
const messages = [
|
|
74
78
|
new SystemMessage(SATI_EVALUATION_PROMPT),
|
|
@@ -114,7 +118,7 @@ export class SatiService {
|
|
|
114
118
|
}
|
|
115
119
|
// Safe JSON parsing (handle markdown blocks if LLM wraps output)
|
|
116
120
|
content = content.replace(/```json/g, '').replace(/```/g, '').trim();
|
|
117
|
-
let result = [];
|
|
121
|
+
let result = { inclusions: [], edits: [], deletions: [] };
|
|
118
122
|
try {
|
|
119
123
|
result = JSON.parse(content);
|
|
120
124
|
}
|
|
@@ -122,8 +126,10 @@ export class SatiService {
|
|
|
122
126
|
console.warn('[SatiService] Failed to parse JSON response:', content);
|
|
123
127
|
return;
|
|
124
128
|
}
|
|
125
|
-
|
|
126
|
-
|
|
129
|
+
const embeddingService = await EmbeddingService.getInstance();
|
|
130
|
+
// Process inclusions (new memories)
|
|
131
|
+
for (const item of (result.inclusions ?? [])) {
|
|
132
|
+
if (item.summary && item.category && item.importance) {
|
|
127
133
|
display.log(`Persisting new memory: [${item.category.toUpperCase()}] ${item.summary}`, { source: 'Sati' });
|
|
128
134
|
try {
|
|
129
135
|
const savedMemory = await this.repository.save({
|
|
@@ -134,22 +140,14 @@ export class SatiService {
|
|
|
134
140
|
hash: this.generateHash(item.summary),
|
|
135
141
|
source: 'conversation'
|
|
136
142
|
});
|
|
137
|
-
|
|
138
|
-
const embeddingService = await EmbeddingService.getInstance();
|
|
139
|
-
const textForEmbedding = [
|
|
140
|
-
savedMemory.summary,
|
|
141
|
-
savedMemory.details ?? ''
|
|
142
|
-
].join(' ');
|
|
143
|
+
const textForEmbedding = [savedMemory.summary, savedMemory.details ?? ''].join(' ');
|
|
143
144
|
const embedding = await embeddingService.generate(textForEmbedding);
|
|
144
145
|
display.log(`Generated embedding for memory ID ${savedMemory.id}`, { source: 'Sati', level: 'debug' });
|
|
145
|
-
// 🔥 SALVAR EMBEDDING NO SQLITE_VEC
|
|
146
146
|
this.repository.upsertEmbedding(savedMemory.id, embedding);
|
|
147
|
-
// Quiet success - logging handled by repository/middleware if needed, or verbose debug
|
|
148
147
|
}
|
|
149
148
|
catch (saveError) {
|
|
150
149
|
if (saveError.message && saveError.message.includes('UNIQUE constraint failed')) {
|
|
151
|
-
// Duplicate detected by DB (
|
|
152
|
-
// This is expected given T012 logic
|
|
150
|
+
// Duplicate detected by DB (hash collision) — expected
|
|
153
151
|
}
|
|
154
152
|
else {
|
|
155
153
|
throw saveError;
|
|
@@ -157,6 +155,39 @@ export class SatiService {
|
|
|
157
155
|
}
|
|
158
156
|
}
|
|
159
157
|
}
|
|
158
|
+
// Process edits (update existing memories)
|
|
159
|
+
for (const edit of (result.edits ?? [])) {
|
|
160
|
+
if (!edit.id)
|
|
161
|
+
continue;
|
|
162
|
+
const updated = this.repository.update(edit.id, {
|
|
163
|
+
importance: edit.importance,
|
|
164
|
+
summary: edit.summary,
|
|
165
|
+
details: edit.details,
|
|
166
|
+
});
|
|
167
|
+
if (updated) {
|
|
168
|
+
display.log(`Updated memory ${edit.id}: ${edit.reason ?? ''}`, { source: 'Sati' });
|
|
169
|
+
if (edit.summary || edit.details) {
|
|
170
|
+
const text = [updated.summary, updated.details ?? ''].join(' ');
|
|
171
|
+
const embedding = await embeddingService.generate(text);
|
|
172
|
+
this.repository.upsertEmbedding(updated.id, embedding);
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
else {
|
|
176
|
+
display.log(`Edit skipped — memory not found: ${edit.id}`, { source: 'Sati', level: 'warning' });
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
// Process deletions (archive memories)
|
|
180
|
+
for (const deletion of (result.deletions ?? [])) {
|
|
181
|
+
if (!deletion.id)
|
|
182
|
+
continue;
|
|
183
|
+
const archived = this.repository.archiveMemory(deletion.id);
|
|
184
|
+
if (archived) {
|
|
185
|
+
display.log(`Archived memory ${deletion.id}: ${deletion.reason ?? ''}`, { source: 'Sati' });
|
|
186
|
+
}
|
|
187
|
+
else {
|
|
188
|
+
display.log(`Deletion skipped — memory not found: ${deletion.id}`, { source: 'Sati', level: 'warning' });
|
|
189
|
+
}
|
|
190
|
+
}
|
|
160
191
|
}
|
|
161
192
|
catch (error) {
|
|
162
193
|
console.error('[SatiService] Evaluation failed:', error);
|
|
@@ -1,13 +1,27 @@
|
|
|
1
1
|
export const SATI_EVALUATION_PROMPT = `You are **Sati**, an autonomous background memory manager for an AI assistant.
|
|
2
|
-
Your goal is to analyze the conversation
|
|
2
|
+
Your goal is to analyze the conversation and decide what changes to make to the long-term memory store.
|
|
3
|
+
You have full CRUD power: you can create new memories, edit existing ones, or delete memories that are no longer valid.
|
|
3
4
|
|
|
4
5
|
### INPUT DATA
|
|
5
|
-
You will receive:
|
|
6
|
-
1.
|
|
7
|
-
2.
|
|
6
|
+
You will receive a JSON object with:
|
|
7
|
+
1. \`recent_conversation\` — list of recent messages (user and assistant).
|
|
8
|
+
2. \`existing_memories\` — full objects for all current memories (with IDs), so you can reference them for edits or deletions.
|
|
9
|
+
|
|
10
|
+
Example input:
|
|
11
|
+
\`\`\`json
|
|
12
|
+
{
|
|
13
|
+
"recent_conversation": [
|
|
14
|
+
{ "role": "user", "content": "..." },
|
|
15
|
+
{ "role": "assistant", "content": "..." }
|
|
16
|
+
],
|
|
17
|
+
"existing_memories": [
|
|
18
|
+
{ "id": "uuid", "category": "preference", "importance": "medium", "summary": "..." }
|
|
19
|
+
]
|
|
20
|
+
}
|
|
21
|
+
\`\`\`
|
|
8
22
|
|
|
9
23
|
### MEMORY CATEGORIES
|
|
10
|
-
Classify
|
|
24
|
+
Classify memories into one of these types:
|
|
11
25
|
- **preference**: User preferences (e.g., "I like dark mode", "Use TypeScript").
|
|
12
26
|
- **project**: Details about the user's projects, architecture, or tech stack.
|
|
13
27
|
- **identity**: Facts about the user's identity, role, or background.
|
|
@@ -22,32 +36,60 @@ Classify any new memory into one of these types:
|
|
|
22
36
|
- **professional_profile**: Job title, industry, skills.
|
|
23
37
|
|
|
24
38
|
### CRITICAL RULES
|
|
25
|
-
0. **
|
|
26
|
-
1. **NO SECRETS**: NEVER store API keys, passwords, credit cards, or private tokens.
|
|
27
|
-
2. **NO
|
|
28
|
-
3. **NO
|
|
29
|
-
4. **IMPORTANCE**: Assign 'low', 'medium', or 'high'
|
|
39
|
+
0. **SUMMARIES IN ENGLISH AND NATIVE LANGUAGE**: Always write summaries in English. If the original content is in another language, also include a summary in that language (e.g., "Prefers TypeScript | Prefere TypeScript").
|
|
40
|
+
1. **NO SECRETS**: NEVER store API keys, passwords, credit cards, or private tokens.
|
|
41
|
+
2. **NO CHIT-CHAT**: Do not store trivial conversation like "Hello", "Thanks", "How are you?".
|
|
42
|
+
3. **NO PERSONAL FINANCIAL**: Avoid storing sensitive financial information (income, debts, expenses).
|
|
43
|
+
4. **IMPORTANCE**: Assign 'low', 'medium', or 'high'. Prefer 'medium' or 'high' — only use 'low' for minor context.
|
|
44
|
+
5. **OBEY THE USER**: If the user explicitly states something should be remembered, store it with at least 'medium' importance.
|
|
45
|
+
|
|
46
|
+
### WHEN TO INCLUDE (create new memory)
|
|
47
|
+
- The information is **genuinely new** and not covered by any entry in \`existing_memories\`.
|
|
48
|
+
- Do NOT create a new entry if an existing memory already captures the same fact.
|
|
30
49
|
|
|
31
|
-
###
|
|
32
|
-
|
|
50
|
+
### WHEN TO EDIT an existing memory
|
|
51
|
+
- The user **confirmed or reinforced** an existing preference/fact → elevate importance (e.g., medium → high).
|
|
52
|
+
- The existing memory is **slightly incorrect or outdated** → correct the summary.
|
|
53
|
+
- The user provided **additional detail** about something already stored → update details field.
|
|
54
|
+
- Use the **exact \`id\`** from \`existing_memories\`. Only include fields that actually change.
|
|
55
|
+
- **Never duplicate** — if the info hasn't changed, do nothing.
|
|
56
|
+
|
|
57
|
+
### WHEN TO DELETE an existing memory
|
|
58
|
+
- The user **explicitly denied** a stored fact ("No, I don't prefer X", "That's wrong").
|
|
59
|
+
- A memory was **proven false** during this conversation.
|
|
60
|
+
- The user **explicitly asked to forget** something specific.
|
|
61
|
+
- Use the **exact \`id\`** from \`existing_memories\`.
|
|
33
62
|
|
|
34
63
|
### OUTPUT FORMAT
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
64
|
+
Respond with a valid JSON object matching this structure. All three arrays are required (use empty arrays if nothing applies):
|
|
65
|
+
|
|
66
|
+
\`\`\`json
|
|
67
|
+
{
|
|
68
|
+
"inclusions": [
|
|
69
|
+
{
|
|
70
|
+
"category": "preference",
|
|
71
|
+
"importance": "high",
|
|
72
|
+
"summary": "Concise factual statement | Summary in native language",
|
|
73
|
+
"reason": "Why this is being stored"
|
|
74
|
+
}
|
|
75
|
+
],
|
|
76
|
+
"edits": [
|
|
77
|
+
{
|
|
78
|
+
"id": "uuid-of-existing-memory",
|
|
79
|
+
"importance": "high",
|
|
80
|
+
"summary": "Updated summary if changed",
|
|
81
|
+
"details": "Updated details if changed",
|
|
82
|
+
"reason": "Why this is being edited"
|
|
83
|
+
}
|
|
84
|
+
],
|
|
85
|
+
"deletions": [
|
|
86
|
+
{
|
|
87
|
+
"id": "uuid-of-existing-memory",
|
|
88
|
+
"reason": "Why this memory is being deleted"
|
|
89
|
+
}
|
|
90
|
+
]
|
|
91
|
+
}
|
|
92
|
+
\`\`\`
|
|
52
93
|
|
|
94
|
+
Empty arrays are perfectly valid. Only include fields in edits that actually need to change.
|
|
53
95
|
`;
|
|
@@ -10,7 +10,7 @@ const EMBEDDING_DIM = 384;
|
|
|
10
10
|
const BATCH_LIMIT = 5;
|
|
11
11
|
export async function runSessionEmbeddingWorker() {
|
|
12
12
|
const display = DisplayManager.getInstance();
|
|
13
|
-
display.log('🚀 Iniciando worker de embeddings de sessões...', { source: 'SessionEmbeddingWorker' });
|
|
13
|
+
// display.log('🚀 Iniciando worker de embeddings de sessões...', { source: 'SessionEmbeddingWorker' });
|
|
14
14
|
const shortDb = new Database(SHORT_DB_PATH);
|
|
15
15
|
const satiDb = new Database(SATI_DB_PATH);
|
|
16
16
|
shortDb.pragma('journal_mode = WAL');
|
|
@@ -27,7 +27,7 @@ export async function runSessionEmbeddingWorker() {
|
|
|
27
27
|
LIMIT ?
|
|
28
28
|
`).all(BATCH_LIMIT);
|
|
29
29
|
if (sessions.length === 0) {
|
|
30
|
-
display.log('✅ Nenhuma sessão pendente.', { level: 'debug', source: 'SessionEmbeddingWorker' });
|
|
30
|
+
// display.log('✅ Nenhuma sessão pendente.', { level: 'debug', source: 'SessionEmbeddingWorker' });
|
|
31
31
|
break;
|
|
32
32
|
}
|
|
33
33
|
for (const session of sessions) {
|
|
@@ -90,5 +90,5 @@ export async function runSessionEmbeddingWorker() {
|
|
|
90
90
|
}
|
|
91
91
|
}
|
|
92
92
|
}
|
|
93
|
-
display.log('🏁 Worker finalizado.', { source: 'SessionEmbeddingWorker' });
|
|
93
|
+
// display.log('🏁 Worker finalizado.', { source: 'SessionEmbeddingWorker' });
|
|
94
94
|
}
|
|
@@ -290,6 +290,30 @@ export class SQLiteChatMessageHistory extends BaseListChatMessageHistory {
|
|
|
290
290
|
throw new Error(`Failed to retrieve messages: ${error}`);
|
|
291
291
|
}
|
|
292
292
|
}
|
|
293
|
+
/**
|
|
294
|
+
* Retrieves raw stored messages for one or more session IDs.
|
|
295
|
+
* Useful when the caller needs metadata like session_id and created_at.
|
|
296
|
+
*/
|
|
297
|
+
async getRawMessagesBySessionIds(sessionIds, limit = this.limit) {
|
|
298
|
+
if (sessionIds.length === 0) {
|
|
299
|
+
return [];
|
|
300
|
+
}
|
|
301
|
+
try {
|
|
302
|
+
const placeholders = sessionIds.map(() => '?').join(', ');
|
|
303
|
+
const stmt = this.db.prepare(`SELECT id, session_id, type, content, created_at, input_tokens, output_tokens, total_tokens, cache_read_tokens, provider, model
|
|
304
|
+
FROM messages
|
|
305
|
+
WHERE session_id IN (${placeholders})
|
|
306
|
+
ORDER BY id DESC
|
|
307
|
+
LIMIT ?`);
|
|
308
|
+
return stmt.all(...sessionIds, limit);
|
|
309
|
+
}
|
|
310
|
+
catch (error) {
|
|
311
|
+
if (error instanceof Error && error.message.includes('SQLITE_BUSY')) {
|
|
312
|
+
throw new Error(`Database is locked. Please try again. Original error: ${error.message}`);
|
|
313
|
+
}
|
|
314
|
+
throw new Error(`Failed to retrieve raw messages: ${error}`);
|
|
315
|
+
}
|
|
316
|
+
}
|
|
293
317
|
/**
|
|
294
318
|
* Adds a message to the database.
|
|
295
319
|
* @param message The message to add
|
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
import Database from 'better-sqlite3';
|
|
2
|
+
import fs from 'fs-extra';
|
|
3
|
+
import path from 'path';
|
|
4
|
+
import { homedir } from 'os';
|
|
5
|
+
import { encrypt, decrypt, canEncrypt } from '../trinity-crypto.js';
|
|
6
|
+
function safeDecrypt(value) {
|
|
7
|
+
if (!value)
|
|
8
|
+
return null;
|
|
9
|
+
try {
|
|
10
|
+
return decrypt(value);
|
|
11
|
+
}
|
|
12
|
+
catch {
|
|
13
|
+
return null;
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
function rowToRecord(row) {
|
|
17
|
+
return {
|
|
18
|
+
id: row.id,
|
|
19
|
+
name: row.name,
|
|
20
|
+
type: row.type,
|
|
21
|
+
host: row.host,
|
|
22
|
+
port: row.port,
|
|
23
|
+
database_name: row.database_name,
|
|
24
|
+
username: row.username,
|
|
25
|
+
password: safeDecrypt(row.password_encrypted),
|
|
26
|
+
connection_string: safeDecrypt(row.connection_string_encrypted),
|
|
27
|
+
schema_json: row.schema_json,
|
|
28
|
+
schema_updated_at: row.schema_updated_at,
|
|
29
|
+
created_at: row.created_at,
|
|
30
|
+
updated_at: row.updated_at,
|
|
31
|
+
allow_read: row.allow_read === 1,
|
|
32
|
+
allow_insert: row.allow_insert === 1,
|
|
33
|
+
allow_update: row.allow_update === 1,
|
|
34
|
+
allow_delete: row.allow_delete === 1,
|
|
35
|
+
allow_ddl: row.allow_ddl === 1,
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
export class DatabaseRegistry {
|
|
39
|
+
static instance = null;
|
|
40
|
+
db;
|
|
41
|
+
constructor() {
|
|
42
|
+
const dbPath = path.join(homedir(), '.morpheus', 'memory', 'trinity.db');
|
|
43
|
+
fs.ensureDirSync(path.dirname(dbPath));
|
|
44
|
+
this.db = new Database(dbPath, { timeout: 5000 });
|
|
45
|
+
this.db.pragma('journal_mode = WAL');
|
|
46
|
+
this.db.pragma('foreign_keys = ON');
|
|
47
|
+
this.ensureTable();
|
|
48
|
+
this.ensureMigrations();
|
|
49
|
+
}
|
|
50
|
+
static getInstance() {
|
|
51
|
+
if (!DatabaseRegistry.instance) {
|
|
52
|
+
DatabaseRegistry.instance = new DatabaseRegistry();
|
|
53
|
+
}
|
|
54
|
+
return DatabaseRegistry.instance;
|
|
55
|
+
}
|
|
56
|
+
ensureTable() {
|
|
57
|
+
this.db.exec(`
|
|
58
|
+
CREATE TABLE IF NOT EXISTS databases (
|
|
59
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
60
|
+
name TEXT NOT NULL UNIQUE,
|
|
61
|
+
type TEXT NOT NULL,
|
|
62
|
+
host TEXT,
|
|
63
|
+
port INTEGER,
|
|
64
|
+
database_name TEXT,
|
|
65
|
+
username TEXT,
|
|
66
|
+
password_encrypted TEXT,
|
|
67
|
+
connection_string_encrypted TEXT,
|
|
68
|
+
schema_json TEXT,
|
|
69
|
+
schema_updated_at INTEGER,
|
|
70
|
+
created_at INTEGER NOT NULL,
|
|
71
|
+
updated_at INTEGER NOT NULL,
|
|
72
|
+
allow_read INTEGER NOT NULL DEFAULT 1,
|
|
73
|
+
allow_insert INTEGER NOT NULL DEFAULT 0,
|
|
74
|
+
allow_update INTEGER NOT NULL DEFAULT 0,
|
|
75
|
+
allow_delete INTEGER NOT NULL DEFAULT 0,
|
|
76
|
+
allow_ddl INTEGER NOT NULL DEFAULT 0
|
|
77
|
+
)
|
|
78
|
+
`);
|
|
79
|
+
}
|
|
80
|
+
/** Add new columns to existing databases table (migration) */
|
|
81
|
+
ensureMigrations() {
|
|
82
|
+
const existingCols = this.db.pragma('table_info(databases)').map((c) => c.name);
|
|
83
|
+
const addIfMissing = (col, def) => {
|
|
84
|
+
if (!existingCols.includes(col)) {
|
|
85
|
+
this.db.exec(`ALTER TABLE databases ADD COLUMN ${col} ${def}`);
|
|
86
|
+
}
|
|
87
|
+
};
|
|
88
|
+
addIfMissing('allow_read', 'INTEGER NOT NULL DEFAULT 1');
|
|
89
|
+
addIfMissing('allow_insert', 'INTEGER NOT NULL DEFAULT 0');
|
|
90
|
+
addIfMissing('allow_update', 'INTEGER NOT NULL DEFAULT 0');
|
|
91
|
+
addIfMissing('allow_delete', 'INTEGER NOT NULL DEFAULT 0');
|
|
92
|
+
addIfMissing('allow_ddl', 'INTEGER NOT NULL DEFAULT 0');
|
|
93
|
+
}
|
|
94
|
+
listDatabases() {
|
|
95
|
+
const rows = this.db.prepare('SELECT * FROM databases ORDER BY name ASC').all();
|
|
96
|
+
return rows.map(rowToRecord);
|
|
97
|
+
}
|
|
98
|
+
getDatabase(id) {
|
|
99
|
+
const row = this.db.prepare('SELECT * FROM databases WHERE id = ?').get(id);
|
|
100
|
+
return row ? rowToRecord(row) : null;
|
|
101
|
+
}
|
|
102
|
+
getDatabaseByName(name) {
|
|
103
|
+
const row = this.db.prepare('SELECT * FROM databases WHERE name = ?').get(name);
|
|
104
|
+
return row ? rowToRecord(row) : null;
|
|
105
|
+
}
|
|
106
|
+
createDatabase(data) {
|
|
107
|
+
if ((data.password || data.connection_string) && !canEncrypt()) {
|
|
108
|
+
throw new Error('MORPHEUS_SECRET must be set to store database credentials. ' +
|
|
109
|
+
'Add MORPHEUS_SECRET to your environment before saving credentials.');
|
|
110
|
+
}
|
|
111
|
+
const now = Date.now();
|
|
112
|
+
const stmt = this.db.prepare(`
|
|
113
|
+
INSERT INTO databases (
|
|
114
|
+
name, type, host, port, database_name, username,
|
|
115
|
+
password_encrypted, connection_string_encrypted,
|
|
116
|
+
schema_json, schema_updated_at,
|
|
117
|
+
allow_read, allow_insert, allow_update, allow_delete, allow_ddl,
|
|
118
|
+
created_at, updated_at
|
|
119
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, NULL, NULL, ?, ?, ?, ?, ?, ?, ?)
|
|
120
|
+
`);
|
|
121
|
+
const result = stmt.run(data.name, data.type, data.host ?? null, data.port ?? null, data.database_name ?? null, data.username ?? null, data.password ? encrypt(data.password) : null, data.connection_string ? encrypt(data.connection_string) : null, data.allow_read !== false ? 1 : 0, data.allow_insert ? 1 : 0, data.allow_update ? 1 : 0, data.allow_delete ? 1 : 0, data.allow_ddl ? 1 : 0, now, now);
|
|
122
|
+
return this.getDatabase(result.lastInsertRowid);
|
|
123
|
+
}
|
|
124
|
+
updateDatabase(id, data) {
|
|
125
|
+
const existing = this.getDatabase(id);
|
|
126
|
+
if (!existing)
|
|
127
|
+
return null;
|
|
128
|
+
if ((data.password || data.connection_string) && !canEncrypt()) {
|
|
129
|
+
throw new Error('MORPHEUS_SECRET must be set to update database credentials.');
|
|
130
|
+
}
|
|
131
|
+
const now = Date.now();
|
|
132
|
+
// Build dynamic update
|
|
133
|
+
const fields = [];
|
|
134
|
+
const values = [];
|
|
135
|
+
if (data.name !== undefined) {
|
|
136
|
+
fields.push('name = ?');
|
|
137
|
+
values.push(data.name);
|
|
138
|
+
}
|
|
139
|
+
if (data.type !== undefined) {
|
|
140
|
+
fields.push('type = ?');
|
|
141
|
+
values.push(data.type);
|
|
142
|
+
}
|
|
143
|
+
if (data.host !== undefined) {
|
|
144
|
+
fields.push('host = ?');
|
|
145
|
+
values.push(data.host);
|
|
146
|
+
}
|
|
147
|
+
if (data.port !== undefined) {
|
|
148
|
+
fields.push('port = ?');
|
|
149
|
+
values.push(data.port);
|
|
150
|
+
}
|
|
151
|
+
if (data.database_name !== undefined) {
|
|
152
|
+
fields.push('database_name = ?');
|
|
153
|
+
values.push(data.database_name);
|
|
154
|
+
}
|
|
155
|
+
if (data.username !== undefined) {
|
|
156
|
+
fields.push('username = ?');
|
|
157
|
+
values.push(data.username);
|
|
158
|
+
}
|
|
159
|
+
if (data.password !== undefined) {
|
|
160
|
+
fields.push('password_encrypted = ?');
|
|
161
|
+
values.push(data.password ? encrypt(data.password) : null);
|
|
162
|
+
}
|
|
163
|
+
if (data.connection_string !== undefined) {
|
|
164
|
+
fields.push('connection_string_encrypted = ?');
|
|
165
|
+
values.push(data.connection_string ? encrypt(data.connection_string) : null);
|
|
166
|
+
}
|
|
167
|
+
if (data.allow_read !== undefined) {
|
|
168
|
+
fields.push('allow_read = ?');
|
|
169
|
+
values.push(data.allow_read ? 1 : 0);
|
|
170
|
+
}
|
|
171
|
+
if (data.allow_insert !== undefined) {
|
|
172
|
+
fields.push('allow_insert = ?');
|
|
173
|
+
values.push(data.allow_insert ? 1 : 0);
|
|
174
|
+
}
|
|
175
|
+
if (data.allow_update !== undefined) {
|
|
176
|
+
fields.push('allow_update = ?');
|
|
177
|
+
values.push(data.allow_update ? 1 : 0);
|
|
178
|
+
}
|
|
179
|
+
if (data.allow_delete !== undefined) {
|
|
180
|
+
fields.push('allow_delete = ?');
|
|
181
|
+
values.push(data.allow_delete ? 1 : 0);
|
|
182
|
+
}
|
|
183
|
+
if (data.allow_ddl !== undefined) {
|
|
184
|
+
fields.push('allow_ddl = ?');
|
|
185
|
+
values.push(data.allow_ddl ? 1 : 0);
|
|
186
|
+
}
|
|
187
|
+
if (fields.length === 0)
|
|
188
|
+
return existing;
|
|
189
|
+
fields.push('updated_at = ?');
|
|
190
|
+
values.push(now);
|
|
191
|
+
values.push(id);
|
|
192
|
+
this.db.prepare(`UPDATE databases SET ${fields.join(', ')} WHERE id = ?`).run(...values);
|
|
193
|
+
return this.getDatabase(id);
|
|
194
|
+
}
|
|
195
|
+
deleteDatabase(id) {
|
|
196
|
+
const result = this.db.prepare('DELETE FROM databases WHERE id = ?').run(id);
|
|
197
|
+
return result.changes > 0;
|
|
198
|
+
}
|
|
199
|
+
updateSchema(id, schemaJson) {
|
|
200
|
+
const now = Date.now();
|
|
201
|
+
this.db.prepare('UPDATE databases SET schema_json = ?, schema_updated_at = ?, updated_at = ? WHERE id = ?').run(schemaJson, now, now, id);
|
|
202
|
+
}
|
|
203
|
+
}
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import { HumanMessage, SystemMessage, AIMessage } from "@langchain/core/messages";
|
|
2
|
+
import { ConfigManager } from "../config/manager.js";
|
|
3
|
+
import { ProviderFactory } from "./providers/factory.js";
|
|
4
|
+
import { ProviderError } from "./errors.js";
|
|
5
|
+
import { DisplayManager } from "./display.js";
|
|
6
|
+
import { Construtor } from "./tools/factory.js";
|
|
7
|
+
import { morpheusTools } from "./tools/index.js";
|
|
8
|
+
import { SQLiteChatMessageHistory } from "./memory/sqlite.js";
|
|
9
|
+
import { TaskRequestContext } from "./tasks/context.js";
|
|
10
|
+
import { updateNeoDelegateToolDescription } from "./tools/neo-tool.js";
|
|
11
|
+
export class Neo {
|
|
12
|
+
static instance = null;
|
|
13
|
+
static currentSessionId = undefined;
|
|
14
|
+
agent;
|
|
15
|
+
config;
|
|
16
|
+
display = DisplayManager.getInstance();
|
|
17
|
+
constructor(config) {
|
|
18
|
+
this.config = config || ConfigManager.getInstance().get();
|
|
19
|
+
}
|
|
20
|
+
static setSessionId(sessionId) {
|
|
21
|
+
Neo.currentSessionId = sessionId;
|
|
22
|
+
}
|
|
23
|
+
static getInstance(config) {
|
|
24
|
+
if (!Neo.instance) {
|
|
25
|
+
Neo.instance = new Neo(config);
|
|
26
|
+
}
|
|
27
|
+
return Neo.instance;
|
|
28
|
+
}
|
|
29
|
+
static resetInstance() {
|
|
30
|
+
Neo.instance = null;
|
|
31
|
+
}
|
|
32
|
+
static async refreshDelegateCatalog() {
|
|
33
|
+
const mcpTools = await Construtor.create();
|
|
34
|
+
updateNeoDelegateToolDescription(mcpTools);
|
|
35
|
+
}
|
|
36
|
+
async initialize() {
|
|
37
|
+
const neoConfig = this.config.neo || this.config.llm;
|
|
38
|
+
const mcpTools = await Construtor.create();
|
|
39
|
+
const tools = [...mcpTools, ...morpheusTools];
|
|
40
|
+
updateNeoDelegateToolDescription(mcpTools);
|
|
41
|
+
this.display.log(`Neo initialized with ${tools.length} tools.`, { source: "Neo" });
|
|
42
|
+
try {
|
|
43
|
+
this.agent = await ProviderFactory.create(neoConfig, tools);
|
|
44
|
+
}
|
|
45
|
+
catch (err) {
|
|
46
|
+
throw new ProviderError(neoConfig.provider, err, "Neo subagent initialization failed");
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
async execute(task, context, sessionId, taskContext) {
|
|
50
|
+
const neoConfig = this.config.neo || this.config.llm;
|
|
51
|
+
if (!this.agent) {
|
|
52
|
+
await this.initialize();
|
|
53
|
+
}
|
|
54
|
+
this.display.log(`Executing delegated task in Neo: ${task.slice(0, 80)}...`, {
|
|
55
|
+
source: "Neo",
|
|
56
|
+
});
|
|
57
|
+
const systemMessage = new SystemMessage(`
|
|
58
|
+
You are Neo, an execution subagent in Morpheus.
|
|
59
|
+
|
|
60
|
+
You execute tasks using MCP and internal tools.
|
|
61
|
+
Focus on verifiable execution and return objective results.
|
|
62
|
+
|
|
63
|
+
Rules:
|
|
64
|
+
1. Use tools whenever the task depends on external/system state.
|
|
65
|
+
2. Validate outputs before giving a final answer.
|
|
66
|
+
3. If blocked, explain exactly what is missing — tool name, permission, or missing input.
|
|
67
|
+
4. Keep output concise and actionable.
|
|
68
|
+
5. Respond in the language requested by the user. If not explicit, use the dominant language of the task/context.
|
|
69
|
+
6. For connectivity checks, prefer dedicated network "ping" tool semantics and avoid forcing shell flags.
|
|
70
|
+
7. If shell ping is required, include OS-aware guidance: Windows uses "-n", Linux/macOS uses "-c".
|
|
71
|
+
|
|
72
|
+
CRITICAL — NEVER FABRICATE DATA:
|
|
73
|
+
- If none of your available tools can retrieve the requested information, respond EXACTLY with:
|
|
74
|
+
"I do not have the required tool to fetch this data. Cannot retrieve: [describe what was requested]. Available tools: [list your actual tool names]."
|
|
75
|
+
- NEVER generate fake records, fake IDs, fake names, fake statuses, or fake values of any kind.
|
|
76
|
+
- If a tool call fails or returns empty results, report the actual result — do not substitute invented data.
|
|
77
|
+
- An honest "I cannot retrieve this" is always correct. A fabricated answer is never acceptable.
|
|
78
|
+
|
|
79
|
+
${context ? `Context:\n${context}` : ""}
|
|
80
|
+
`);
|
|
81
|
+
const userMessage = new HumanMessage(task);
|
|
82
|
+
const messages = [systemMessage, userMessage];
|
|
83
|
+
try {
|
|
84
|
+
const invokeContext = {
|
|
85
|
+
origin_channel: taskContext?.origin_channel ?? "api",
|
|
86
|
+
session_id: taskContext?.session_id ?? sessionId ?? "default",
|
|
87
|
+
origin_message_id: taskContext?.origin_message_id,
|
|
88
|
+
origin_user_id: taskContext?.origin_user_id,
|
|
89
|
+
};
|
|
90
|
+
const response = await TaskRequestContext.run(invokeContext, () => this.agent.invoke({ messages }));
|
|
91
|
+
const lastMessage = response.messages[response.messages.length - 1];
|
|
92
|
+
const content = typeof lastMessage.content === "string"
|
|
93
|
+
? lastMessage.content
|
|
94
|
+
: JSON.stringify(lastMessage.content);
|
|
95
|
+
const targetSession = sessionId ?? Neo.currentSessionId ?? "neo";
|
|
96
|
+
const history = new SQLiteChatMessageHistory({ sessionId: targetSession });
|
|
97
|
+
try {
|
|
98
|
+
const persisted = new AIMessage(content);
|
|
99
|
+
persisted.usage_metadata = lastMessage.usage_metadata
|
|
100
|
+
?? lastMessage.response_metadata?.usage
|
|
101
|
+
?? lastMessage.response_metadata?.tokenUsage
|
|
102
|
+
?? lastMessage.usage;
|
|
103
|
+
persisted.provider_metadata = {
|
|
104
|
+
provider: neoConfig.provider,
|
|
105
|
+
model: neoConfig.model,
|
|
106
|
+
};
|
|
107
|
+
await history.addMessage(persisted);
|
|
108
|
+
}
|
|
109
|
+
finally {
|
|
110
|
+
history.close();
|
|
111
|
+
}
|
|
112
|
+
this.display.log("Neo task completed.", { source: "Neo" });
|
|
113
|
+
return content;
|
|
114
|
+
}
|
|
115
|
+
catch (err) {
|
|
116
|
+
throw new ProviderError(neoConfig.provider, err, "Neo task execution failed");
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
async reload() {
|
|
120
|
+
this.config = ConfigManager.getInstance().get();
|
|
121
|
+
this.agent = undefined;
|
|
122
|
+
await this.initialize();
|
|
123
|
+
}
|
|
124
|
+
}
|