morpheus-cli 0.5.0 → 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.
Files changed (35) hide show
  1. package/README.md +26 -7
  2. package/dist/channels/telegram.js +173 -0
  3. package/dist/cli/commands/restart.js +15 -14
  4. package/dist/cli/commands/start.js +13 -12
  5. package/dist/config/manager.js +31 -0
  6. package/dist/config/mcp-manager.js +19 -1
  7. package/dist/config/schemas.js +2 -0
  8. package/dist/http/api.js +222 -0
  9. package/dist/runtime/memory/session-embedding-worker.js +3 -3
  10. package/dist/runtime/memory/trinity-db.js +203 -0
  11. package/dist/runtime/neo.js +16 -26
  12. package/dist/runtime/oracle.js +16 -8
  13. package/dist/runtime/session-embedding-scheduler.js +1 -1
  14. package/dist/runtime/tasks/dispatcher.js +21 -0
  15. package/dist/runtime/tasks/worker.js +4 -1
  16. package/dist/runtime/tools/__tests__/tools.test.js +1 -3
  17. package/dist/runtime/tools/factory.js +1 -1
  18. package/dist/runtime/tools/index.js +1 -3
  19. package/dist/runtime/tools/morpheus-tools.js +742 -0
  20. package/dist/runtime/tools/neo-tool.js +19 -9
  21. package/dist/runtime/tools/trinity-tool.js +98 -0
  22. package/dist/runtime/trinity-connector.js +611 -0
  23. package/dist/runtime/trinity-crypto.js +52 -0
  24. package/dist/runtime/trinity.js +246 -0
  25. package/dist/ui/assets/index-DP2V4kRd.js +112 -0
  26. package/dist/ui/assets/index-mglRG5Zw.css +1 -0
  27. package/dist/ui/index.html +2 -2
  28. package/dist/ui/sw.js +1 -1
  29. package/package.json +6 -1
  30. package/dist/runtime/tools/analytics-tools.js +0 -139
  31. package/dist/runtime/tools/config-tools.js +0 -64
  32. package/dist/runtime/tools/diagnostic-tools.js +0 -153
  33. package/dist/runtime/tools/task-query-tool.js +0 -76
  34. package/dist/ui/assets/index-20lLB1sM.js +0 -112
  35. package/dist/ui/assets/index-BJ56bRfs.css +0 -1
package/dist/http/api.js CHANGED
@@ -12,6 +12,9 @@ import { MCPManager } from '../config/mcp-manager.js';
12
12
  import { MCPServerConfigSchema } from '../config/schemas.js';
13
13
  import { Construtor } from '../runtime/tools/factory.js';
14
14
  import { TaskRepository } from '../runtime/tasks/repository.js';
15
+ import { DatabaseRegistry } from '../runtime/memory/trinity-db.js';
16
+ import { testConnection, introspectSchema } from '../runtime/trinity-connector.js';
17
+ import { Trinity } from '../runtime/trinity.js';
15
18
  async function readLastLines(filePath, n) {
16
19
  try {
17
20
  const content = await fs.readFile(filePath, 'utf8');
@@ -745,6 +748,225 @@ export function createApiRouter(oracle) {
745
748
  // Redirect to POST logic or just reuse
746
749
  res.status(307).redirect(307, '/api/config');
747
750
  });
751
+ // ─── Trinity Config ────────────────────────────────────────────────────────
752
+ router.get('/config/trinity', (req, res) => {
753
+ try {
754
+ const trinityConfig = configManager.getTrinityConfig();
755
+ res.json(trinityConfig);
756
+ }
757
+ catch (error) {
758
+ res.status(500).json({ error: error.message });
759
+ }
760
+ });
761
+ router.post('/config/trinity', async (req, res) => {
762
+ try {
763
+ const config = configManager.get();
764
+ await configManager.save({ ...config, trinity: req.body });
765
+ const display = DisplayManager.getInstance();
766
+ display.log('Trinity configuration updated via UI', { source: 'Zaion', level: 'info' });
767
+ res.json({ success: true });
768
+ }
769
+ catch (error) {
770
+ if (error.name === 'ZodError') {
771
+ res.status(400).json({ error: 'Validation failed', details: error.errors });
772
+ }
773
+ else {
774
+ res.status(500).json({ error: error.message });
775
+ }
776
+ }
777
+ });
778
+ router.delete('/config/trinity', async (req, res) => {
779
+ try {
780
+ const config = configManager.get();
781
+ const { trinity: _trinity, ...restConfig } = config;
782
+ await configManager.save(restConfig);
783
+ const display = DisplayManager.getInstance();
784
+ display.log('Trinity configuration removed via UI (falling back to Oracle config)', {
785
+ source: 'Zaion',
786
+ level: 'info',
787
+ });
788
+ res.json({ success: true });
789
+ }
790
+ catch (error) {
791
+ res.status(500).json({ error: error.message });
792
+ }
793
+ });
794
+ // ─── Trinity Databases CRUD ─────────────────────────────────────────────────
795
+ const DatabaseCreateSchema = z.object({
796
+ name: z.string().min(1).max(100),
797
+ type: z.enum(['postgresql', 'mysql', 'sqlite', 'mongodb']),
798
+ host: z.string().optional().nullable(),
799
+ port: z.number().int().positive().optional().nullable(),
800
+ database_name: z.string().optional().nullable(),
801
+ username: z.string().optional().nullable(),
802
+ password: z.string().optional().nullable(),
803
+ connection_string: z.string().optional().nullable(),
804
+ allow_read: z.boolean().optional(),
805
+ allow_insert: z.boolean().optional(),
806
+ allow_update: z.boolean().optional(),
807
+ allow_delete: z.boolean().optional(),
808
+ allow_ddl: z.boolean().optional(),
809
+ });
810
+ router.get('/trinity/databases', (req, res) => {
811
+ try {
812
+ const registry = DatabaseRegistry.getInstance();
813
+ const databases = registry.listDatabases().map((db) => ({
814
+ ...db,
815
+ password: db.password ? '***' : null,
816
+ connection_string: db.connection_string ? '***' : null,
817
+ }));
818
+ res.json(databases);
819
+ }
820
+ catch (error) {
821
+ res.status(500).json({ error: error.message });
822
+ }
823
+ });
824
+ router.get('/trinity/databases/:id', (req, res) => {
825
+ try {
826
+ const id = parseInt(req.params.id);
827
+ if (isNaN(id))
828
+ return res.status(400).json({ error: 'Invalid id' });
829
+ const registry = DatabaseRegistry.getInstance();
830
+ const db = registry.getDatabase(id);
831
+ if (!db)
832
+ return res.status(404).json({ error: 'Database not found' });
833
+ res.json({
834
+ ...db,
835
+ password: db.password ? '***' : null,
836
+ connection_string: db.connection_string ? '***' : null,
837
+ });
838
+ }
839
+ catch (error) {
840
+ res.status(500).json({ error: error.message });
841
+ }
842
+ });
843
+ router.post('/trinity/databases', async (req, res) => {
844
+ const parsed = DatabaseCreateSchema.safeParse(req.body);
845
+ if (!parsed.success) {
846
+ return res.status(400).json({ error: 'Invalid input', details: parsed.error.issues });
847
+ }
848
+ try {
849
+ const registry = DatabaseRegistry.getInstance();
850
+ const db = registry.createDatabase(parsed.data);
851
+ // Test connection
852
+ let connectionOk = false;
853
+ try {
854
+ connectionOk = await testConnection(db);
855
+ }
856
+ catch { /* ignore */ }
857
+ // Introspect schema if connection successful
858
+ if (connectionOk) {
859
+ try {
860
+ const schema = await introspectSchema(db);
861
+ registry.updateSchema(db.id, JSON.stringify(schema, null, 2));
862
+ await Trinity.refreshDelegateCatalog().catch(() => { });
863
+ }
864
+ catch { /* ignore schema errors */ }
865
+ }
866
+ const refreshed = registry.getDatabase(db.id);
867
+ res.status(201).json({
868
+ ...refreshed,
869
+ password: refreshed.password ? '***' : null,
870
+ connection_string: refreshed.connection_string ? '***' : null,
871
+ connection_ok: connectionOk,
872
+ });
873
+ }
874
+ catch (error) {
875
+ res.status(500).json({ error: error.message });
876
+ }
877
+ });
878
+ router.put('/trinity/databases/:id', async (req, res) => {
879
+ const id = parseInt(req.params.id);
880
+ if (isNaN(id))
881
+ return res.status(400).json({ error: 'Invalid id' });
882
+ const parsed = DatabaseCreateSchema.partial().safeParse(req.body);
883
+ if (!parsed.success) {
884
+ return res.status(400).json({ error: 'Invalid input', details: parsed.error.issues });
885
+ }
886
+ try {
887
+ const registry = DatabaseRegistry.getInstance();
888
+ const updated = registry.updateDatabase(id, parsed.data);
889
+ if (!updated)
890
+ return res.status(404).json({ error: 'Database not found' });
891
+ // Re-test and re-introspect
892
+ let connectionOk = false;
893
+ try {
894
+ connectionOk = await testConnection(updated);
895
+ }
896
+ catch { /* ignore */ }
897
+ if (connectionOk) {
898
+ try {
899
+ const schema = await introspectSchema(updated);
900
+ registry.updateSchema(id, JSON.stringify(schema, null, 2));
901
+ await Trinity.refreshDelegateCatalog().catch(() => { });
902
+ }
903
+ catch { /* ignore */ }
904
+ }
905
+ const refreshed = registry.getDatabase(id);
906
+ res.json({
907
+ ...refreshed,
908
+ password: refreshed.password ? '***' : null,
909
+ connection_string: refreshed.connection_string ? '***' : null,
910
+ connection_ok: connectionOk,
911
+ });
912
+ }
913
+ catch (error) {
914
+ res.status(500).json({ error: error.message });
915
+ }
916
+ });
917
+ router.delete('/trinity/databases/:id', (req, res) => {
918
+ const id = parseInt(req.params.id);
919
+ if (isNaN(id))
920
+ return res.status(400).json({ error: 'Invalid id' });
921
+ try {
922
+ const registry = DatabaseRegistry.getInstance();
923
+ const deleted = registry.deleteDatabase(id);
924
+ if (!deleted)
925
+ return res.status(404).json({ error: 'Database not found' });
926
+ Trinity.refreshDelegateCatalog().catch(() => { });
927
+ res.json({ success: true });
928
+ }
929
+ catch (error) {
930
+ res.status(500).json({ error: error.message });
931
+ }
932
+ });
933
+ router.post('/trinity/databases/:id/refresh-schema', async (req, res) => {
934
+ const id = parseInt(req.params.id);
935
+ if (isNaN(id))
936
+ return res.status(400).json({ error: 'Invalid id' });
937
+ try {
938
+ const registry = DatabaseRegistry.getInstance();
939
+ const db = registry.getDatabase(id);
940
+ if (!db)
941
+ return res.status(404).json({ error: 'Database not found' });
942
+ const schema = await introspectSchema(db);
943
+ registry.updateSchema(id, JSON.stringify(schema, null, 2));
944
+ await Trinity.refreshDelegateCatalog().catch(() => { });
945
+ const tableNames = schema.databases
946
+ ? schema.databases.flatMap((d) => d.tables.map((t) => `${d.name}.${t.name}`))
947
+ : schema.tables.map((t) => t.name);
948
+ res.json({ success: true, tables: tableNames, databases: schema.databases?.length });
949
+ }
950
+ catch (error) {
951
+ res.status(500).json({ error: error.message });
952
+ }
953
+ });
954
+ router.post('/trinity/databases/:id/test', async (req, res) => {
955
+ const id = parseInt(req.params.id);
956
+ if (isNaN(id))
957
+ return res.status(400).json({ error: 'Invalid id' });
958
+ try {
959
+ const registry = DatabaseRegistry.getInstance();
960
+ const db = registry.getDatabase(id);
961
+ if (!db)
962
+ return res.status(404).json({ error: 'Database not found' });
963
+ const ok = await testConnection(db);
964
+ res.json({ success: ok, status: ok ? 'connected' : 'failed' });
965
+ }
966
+ catch (error) {
967
+ res.json({ success: false, status: 'error', error: error.message });
968
+ }
969
+ });
748
970
  router.get('/logs', async (req, res) => {
749
971
  try {
750
972
  await fs.ensureDir(PATHS.logs);
@@ -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
  }
@@ -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
+ }
@@ -4,7 +4,7 @@ import { ProviderFactory } from "./providers/factory.js";
4
4
  import { ProviderError } from "./errors.js";
5
5
  import { DisplayManager } from "./display.js";
6
6
  import { Construtor } from "./tools/factory.js";
7
- import { ConfigQueryTool, ConfigUpdateTool, DiagnosticTool, MessageCountTool, TokenUsageTool, ProviderModelUsageTool, } from "./tools/index.js";
7
+ import { morpheusTools } from "./tools/index.js";
8
8
  import { SQLiteChatMessageHistory } from "./memory/sqlite.js";
9
9
  import { TaskRequestContext } from "./tasks/context.js";
10
10
  import { updateNeoDelegateToolDescription } from "./tools/neo-tool.js";
@@ -31,30 +31,13 @@ export class Neo {
31
31
  }
32
32
  static async refreshDelegateCatalog() {
33
33
  const mcpTools = await Construtor.create();
34
- const catalogTools = [
35
- ...mcpTools,
36
- ConfigQueryTool,
37
- ConfigUpdateTool,
38
- DiagnosticTool,
39
- MessageCountTool,
40
- TokenUsageTool,
41
- ProviderModelUsageTool
42
- ];
43
- updateNeoDelegateToolDescription(catalogTools);
34
+ updateNeoDelegateToolDescription(mcpTools);
44
35
  }
45
36
  async initialize() {
46
37
  const neoConfig = this.config.neo || this.config.llm;
47
38
  const mcpTools = await Construtor.create();
48
- const tools = [
49
- ...mcpTools,
50
- ConfigQueryTool,
51
- ConfigUpdateTool,
52
- DiagnosticTool,
53
- MessageCountTool,
54
- TokenUsageTool,
55
- ProviderModelUsageTool
56
- ];
57
- updateNeoDelegateToolDescription(tools);
39
+ const tools = [...mcpTools, ...morpheusTools];
40
+ updateNeoDelegateToolDescription(mcpTools);
58
41
  this.display.log(`Neo initialized with ${tools.length} tools.`, { source: "Neo" });
59
42
  try {
60
43
  this.agent = await ProviderFactory.create(neoConfig, tools);
@@ -78,13 +61,20 @@ You execute tasks using MCP and internal tools.
78
61
  Focus on verifiable execution and return objective results.
79
62
 
80
63
  Rules:
81
- 1. Use tools whenever task depends on external/system state.
82
- 2. Validate outputs before final answer.
83
- 3. If blocked, explain what is missing.
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.
84
67
  4. Keep output concise and actionable.
85
68
  5. Respond in the language requested by the user. If not explicit, use the dominant language of the task/context.
86
- 6. For connectivity checks, prefer the dedicated network "ping" tool semantics (reachability) and avoid forcing shell flags.
87
- 7. If delegating shell ping to Apoc is explicitly required, include OS-aware guidance: Windows uses "-n", Linux/macOS uses "-c".
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.
88
78
 
89
79
  ${context ? `Context:\n${context}` : ""}
90
80
  `);
@@ -9,9 +9,12 @@ import { Apoc } from "./apoc.js";
9
9
  import { TaskRequestContext } from "./tasks/context.js";
10
10
  import { TaskRepository } from "./tasks/repository.js";
11
11
  import { Neo } from "./neo.js";
12
+ import { Trinity } from "./trinity.js";
12
13
  import { NeoDelegateTool } from "./tools/neo-tool.js";
13
14
  import { ApocDelegateTool } from "./tools/apoc-tool.js";
14
- import { TaskQueryTool } from "./tools/task-query-tool.js";
15
+ import { TrinityDelegateTool } from "./tools/trinity-tool.js";
16
+ import { TaskQueryTool } from "./tools/index.js";
17
+ import { MCPManager } from "../config/mcp-manager.js";
15
18
  export class Oracle {
16
19
  provider;
17
20
  config;
@@ -41,9 +44,9 @@ export class Oracle {
41
44
  const hasCreationClaim = /(as\s+tarefas?\s+foram\s+criadas|tarefa\s+criada|nova\s+tarefa\s+criada|deleguei|delegado|delegada|tasks?\s+created|task\s+created|queued\s+for|agendei|agendado|agendada|foi\s+agendad)/i.test(raw);
42
45
  if (!hasCreationClaim)
43
46
  return false;
44
- const hasAgentMention = /\b(apoc|neo|trinit)\b/i.test(raw);
47
+ const hasAgentMention = /\b(apoc|neo|trinit|trinity)\b/i.test(raw);
45
48
  const hasUuid = /\b[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-5][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}\b/.test(raw);
46
- const hasAgentListLine = /(?:\*|-)?.{0,8}(apoc|neo|trinit)\s*[::]/i.test(raw);
49
+ const hasAgentListLine = /(?:\*|-)?.{0,8}(apoc|neo|trinit|trinity)\s*[::]/i.test(raw);
47
50
  return hasCreationClaim && (hasAgentMention || hasUuid || hasAgentListLine);
48
51
  }
49
52
  buildDelegationAck(acks) {
@@ -66,7 +69,7 @@ export class Oracle {
66
69
  }
67
70
  extractDelegationAcksFromMessages(messages) {
68
71
  const acks = [];
69
- const regex = /Task\s+([0-9a-fA-F-]{36})\s+(?:queued|already queued)\s+for\s+(Apoc|Neo|apoc|neo)\s+execution/i;
72
+ const regex = /Task\s+([0-9a-fA-F-]{36})\s+(?:queued|already queued)\s+for\s+(Apoc|Neo|Trinity|apoc|neo|trinity)\s+execution/i;
70
73
  for (const msg of messages) {
71
74
  if (!(msg instanceof ToolMessage))
72
75
  continue;
@@ -105,7 +108,7 @@ export class Oracle {
105
108
  const toolCalls = msg.tool_calls ?? [];
106
109
  if (!Array.isArray(toolCalls))
107
110
  continue;
108
- if (toolCalls.some((tc) => tc?.name === "apoc_delegate" || tc?.name === "neo_delegate")) {
111
+ if (toolCalls.some((tc) => tc?.name === "apoc_delegate" || tc?.name === "neo_delegate" || tc?.name === "trinity_delegate")) {
109
112
  return true;
110
113
  }
111
114
  }
@@ -122,10 +125,11 @@ export class Oracle {
122
125
  // Note: API Key validation is delegated to ProviderFactory or the Provider itself
123
126
  // to allow for Environment Variable fallback supported by LangChain.
124
127
  try {
125
- // Refresh Neo tool catalog so neo_delegate description contains runtime tools list.
128
+ // Refresh Neo and Trinity tool catalogs so delegate descriptions contain runtime info.
126
129
  // Fail-open: Oracle can still initialize even if catalog refresh fails.
127
130
  await Neo.refreshDelegateCatalog().catch(() => { });
128
- this.provider = await ProviderFactory.create(this.config.llm, [TaskQueryTool, NeoDelegateTool, ApocDelegateTool]);
131
+ await Trinity.refreshDelegateCatalog().catch(() => { });
132
+ this.provider = await ProviderFactory.create(this.config.llm, [TaskQueryTool, NeoDelegateTool, ApocDelegateTool, TrinityDelegateTool]);
129
133
  if (!this.provider) {
130
134
  throw new Error("Provider factory returned undefined");
131
135
  }
@@ -137,6 +141,8 @@ export class Oracle {
137
141
  databasePath: this.databasePath,
138
142
  limit: contextWindow,
139
143
  });
144
+ // Register reload callback so MCPManager.reloadAgents() can trigger a full tool reload.
145
+ MCPManager.registerReloadCallback(() => this.reloadTools());
140
146
  }
141
147
  catch (err) {
142
148
  if (err instanceof ProviderError)
@@ -250,6 +256,7 @@ Use it to inform your response and tool selection (if needed), but do not assume
250
256
  : undefined;
251
257
  Apoc.setSessionId(currentSessionId);
252
258
  Neo.setSessionId(currentSessionId);
259
+ Trinity.setSessionId(currentSessionId);
253
260
  const invokeContext = {
254
261
  origin_channel: taskContext?.origin_channel ?? "api",
255
262
  session_id: taskContext?.session_id ?? currentSessionId ?? "default",
@@ -440,7 +447,8 @@ Use it to inform your response and tool selection (if needed), but do not assume
440
447
  throw new Error("Oracle not initialized. Call initialize() first.");
441
448
  }
442
449
  await Neo.refreshDelegateCatalog().catch(() => { });
443
- this.provider = await ProviderFactory.create(this.config.llm, [TaskQueryTool, NeoDelegateTool, ApocDelegateTool]);
450
+ await Trinity.refreshDelegateCatalog().catch(() => { });
451
+ this.provider = await ProviderFactory.create(this.config.llm, [TaskQueryTool, NeoDelegateTool, ApocDelegateTool, TrinityDelegateTool]);
444
452
  await Neo.getInstance().reload();
445
453
  this.display.log(`Oracle and Neo tools reloaded`, { source: 'Oracle' });
446
454
  }
@@ -3,7 +3,7 @@ import { DisplayManager } from './display.js';
3
3
  const INTERVAL_MS = 60_000 * 5; // 5 minutos
4
4
  export function startSessionEmbeddingScheduler() {
5
5
  const display = DisplayManager.getInstance();
6
- display.log('🕒 Scheduler de embeddings iniciado', { source: 'SessionEmbeddingScheduler' });
6
+ display.log('Scheduler embeddings started.', { source: 'SessionEmbeddingScheduler' });
7
7
  // roda imediatamente na inicialização
8
8
  runSessionEmbeddingWorker().catch(console.error);
9
9
  let isRunning = false;
@@ -19,6 +19,27 @@ export class TaskDispatcher {
19
19
  ? (task.output && task.output.trim().length > 0 ? task.output : 'Task completed without output.')
20
20
  : (task.error && task.error.trim().length > 0 ? task.error : 'Task failed with unknown error.');
21
21
  repo.updateNotificationResult(task.origin_message_id, status, result);
22
+ // Send Telegram notification if the webhook has 'telegram' in notification_channels
23
+ const notification = repo.getNotificationById(task.origin_message_id);
24
+ if (notification) {
25
+ const webhook = repo.getWebhookById(notification.webhook_id);
26
+ if (webhook?.notification_channels.includes('telegram')) {
27
+ const adapter = TaskDispatcher.telegramAdapter;
28
+ if (adapter) {
29
+ try {
30
+ const icon = status === 'completed' ? '✅' : '❌';
31
+ const truncated = result.length > 3500 ? result.slice(0, 3500) + '…' : result;
32
+ await adapter.sendMessage(`${icon} Webhook: ${webhook.name}\n\n${truncated}`);
33
+ }
34
+ catch (err) {
35
+ TaskDispatcher.display.log(`Failed to send Telegram notification for webhook "${webhook.name}": ${err.message}`, { source: 'TaskDispatcher', level: 'error' });
36
+ }
37
+ }
38
+ else {
39
+ TaskDispatcher.display.log(`Telegram notification skipped for webhook "${webhook.name}" — adapter not connected.`, { source: 'TaskDispatcher', level: 'warning' });
40
+ }
41
+ }
42
+ }
22
43
  return;
23
44
  }
24
45
  if (task.origin_channel === 'ui') {
@@ -2,6 +2,7 @@ import { randomUUID } from 'crypto';
2
2
  import { DisplayManager } from '../display.js';
3
3
  import { Apoc } from '../apoc.js';
4
4
  import { Neo } from '../neo.js';
5
+ import { Trinity } from '../trinity.js';
5
6
  import { TaskRepository } from './repository.js';
6
7
  export class TaskWorker {
7
8
  workerId;
@@ -69,7 +70,9 @@ export class TaskWorker {
69
70
  break;
70
71
  }
71
72
  case 'trinit': {
72
- throw new Error('Trinit executor is not implemented yet.');
73
+ const trinity = Trinity.getInstance();
74
+ output = await trinity.execute(task.input, task.context ?? undefined, task.session_id);
75
+ break;
73
76
  }
74
77
  default: {
75
78
  throw new Error(`Unknown task agent: ${task.agent}`);