morpheus-cli 0.2.6 → 0.2.8

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.
@@ -11,6 +11,7 @@ import { Telephonist } from '../runtime/telephonist.js';
11
11
  import { readPid, isProcessRunning, checkStalePid } from '../runtime/lifecycle.js';
12
12
  import { SQLiteChatMessageHistory } from '../runtime/memory/sqlite.js';
13
13
  import { SatiRepository } from '../runtime/memory/sati/repository.js';
14
+ import { MCPManager } from '../config/mcp-manager.js';
14
15
  export class TelegramAdapter {
15
16
  bot = null;
16
17
  isConnected = false;
@@ -18,6 +19,18 @@ export class TelegramAdapter {
18
19
  config = ConfigManager.getInstance();
19
20
  oracle;
20
21
  telephonist = new Telephonist();
22
+ history = new SQLiteChatMessageHistory({ sessionId: '' });
23
+ HELP_MESSAGE = `/start - Show this welcome message and available commands
24
+ /status - Check the status of the Morpheus agent
25
+ /doctor - Diagnose environment and configuration issues
26
+ /stats - Show token usage statistics
27
+ /help - Show available commands
28
+ /zaion - Show system configurations
29
+ /sati <qnt> - Show specific memories
30
+ /newsession - Archive current session and start fresh
31
+ /sessions - List all sessions with titles and switch between them
32
+ /restart - Restart the Morpheus agent
33
+ /mcp or /mcps - List registered MCP servers`;
21
34
  constructor(oracle) {
22
35
  this.oracle = oracle;
23
36
  }
@@ -138,6 +151,45 @@ export class TelegramAdapter {
138
151
  }
139
152
  }
140
153
  });
154
+ this.bot.action('confirm_new_session', async (ctx) => {
155
+ await this.handleApproveNewSessionCommand(ctx, ctx.from.username || ctx.from.first_name);
156
+ if (ctx.updateType === 'callback_query') {
157
+ ctx.answerCbQuery();
158
+ ctx.deleteMessage().catch(() => { });
159
+ }
160
+ ctx.reply("New session created.");
161
+ });
162
+ this.bot.action('cancel_new_session', async (ctx) => {
163
+ if (ctx.updateType === 'callback_query') {
164
+ ctx.answerCbQuery();
165
+ ctx.deleteMessage().catch(() => { });
166
+ }
167
+ ctx.reply("New session cancelled.");
168
+ });
169
+ this.bot.action(/^switch_session_/, async (ctx) => {
170
+ const callbackQuery = ctx.callbackQuery;
171
+ const data = callbackQuery && 'data' in callbackQuery ? callbackQuery.data : undefined;
172
+ const sessionId = typeof data === 'string' ? data.replace('switch_session_', '') : '';
173
+ if (!sessionId || sessionId === '') {
174
+ await ctx.answerCbQuery('Invalid session ID');
175
+ return;
176
+ }
177
+ try {
178
+ // Obter a sessão atual antes de alternar
179
+ const history = new SQLiteChatMessageHistory({ sessionId: "" });
180
+ // Alternar para a nova sessão
181
+ await history.switchSession(sessionId);
182
+ await ctx.answerCbQuery();
183
+ // Remover a mensagem anterior e enviar confirmação
184
+ if (ctx.updateType === 'callback_query') {
185
+ ctx.deleteMessage().catch(() => { });
186
+ }
187
+ ctx.reply(`✅ Switched to session ID: ${sessionId}`);
188
+ }
189
+ catch (error) {
190
+ await ctx.answerCbQuery(`Error switching session: ${error.message}`);
191
+ }
192
+ });
141
193
  this.bot.launch().catch((err) => {
142
194
  if (this.isConnected) {
143
195
  this.display.log(`Telegram bot error: ${err}`, { source: 'Telegram', level: 'error' });
@@ -187,6 +239,11 @@ export class TelegramAdapter {
187
239
  this.bot = null;
188
240
  this.display.log(chalk.gray('Telegram disconnected.'), { source: 'Telegram' });
189
241
  }
242
+ /**
243
+ ** =========================
244
+ ** Commands Handlers
245
+ ** =========================
246
+ */
190
247
  async handleSystemCommand(ctx, text, user) {
191
248
  const command = text.split(' ')[0];
192
249
  const args = text.split(' ').slice(1);
@@ -215,24 +272,91 @@ export class TelegramAdapter {
215
272
  case '/restart':
216
273
  await this.handleRestartCommand(ctx, user);
217
274
  break;
275
+ case '/mcp':
276
+ case '/mcps':
277
+ await this.handleMcpListCommand(ctx, user);
278
+ break;
279
+ case '/newsession':
280
+ case '/reset':
281
+ await this.handleNewSessionCommand(ctx, user);
282
+ break;
283
+ case '/sessionstatus':
284
+ case '/session':
285
+ case '/sessions':
286
+ await this.handleSessionStatusCommand(ctx, user);
287
+ break;
218
288
  default:
219
289
  await this.handleDefaultCommand(ctx, user, command);
220
290
  }
221
291
  }
292
+ async handleNewSessionCommand(ctx, user) {
293
+ try {
294
+ await ctx.reply("Are you ready to start a new session? Please confirm.", {
295
+ parse_mode: 'Markdown', reply_markup: {
296
+ inline_keyboard: [
297
+ [{ text: 'Yes, start new session', callback_data: 'confirm_new_session' }, { text: 'No, cancel', callback_data: 'cancel_new_session' }]
298
+ ]
299
+ }
300
+ });
301
+ }
302
+ catch (e) {
303
+ await ctx.reply(`Error starting new session: ${e.message}`);
304
+ }
305
+ }
306
+ async handleApproveNewSessionCommand(ctx, user) {
307
+ try {
308
+ const history = new SQLiteChatMessageHistory({ sessionId: "" });
309
+ await history.createNewSession();
310
+ }
311
+ catch (e) {
312
+ await ctx.reply(`Error creating new session: ${e.message}`);
313
+ }
314
+ }
315
+ async handleSessionStatusCommand(ctx, user) {
316
+ try {
317
+ // Obter todas as sessões ativas e pausadas usando a nova função
318
+ const history = new SQLiteChatMessageHistory({ sessionId: "" });
319
+ const sessions = await history.listSessions();
320
+ if (sessions.length === 0) {
321
+ await ctx.reply('No active or paused sessions found.', { parse_mode: 'Markdown' });
322
+ return;
323
+ }
324
+ let response = '*Sessions:*\n\n';
325
+ const keyboard = [];
326
+ for (const session of sessions) {
327
+ const title = session.title || 'Untitled Session';
328
+ const statusEmoji = session.status === 'active' ? '🟢' : '🟡';
329
+ response += `${statusEmoji} *${title}*\n`;
330
+ response += `- ID: ${session.id}\n`;
331
+ response += `- Status: ${session.status}\n`;
332
+ response += `- Started: ${new Date(session.started_at).toLocaleString()}\n\n`;
333
+ // Adicionar botão inline para alternar para esta sessão
334
+ if (session.status !== 'active') {
335
+ keyboard.push([{
336
+ text: `Switch to: ${title}`,
337
+ callback_data: `switch_session_${session.id}`
338
+ }]);
339
+ }
340
+ }
341
+ await ctx.reply(response, {
342
+ parse_mode: 'Markdown',
343
+ reply_markup: {
344
+ inline_keyboard: keyboard
345
+ }
346
+ });
347
+ history.close();
348
+ }
349
+ catch (e) {
350
+ await ctx.reply(`Error retrieving session status: ${e.message}`);
351
+ }
352
+ }
222
353
  async handleStartCommand(ctx, user) {
223
354
  const welcomeMessage = `
224
355
  Hello, @${user}! I am ${this.config.get().agent.name}, ${this.config.get().agent.personality}.
225
356
 
226
357
  I am your local AI operator/agent. Here are the commands you can use:
227
358
 
228
- /start - Show this welcome message and available commands
229
- /status - Check the status of the Morpheus agent
230
- /doctor - Diagnose environment and configuration issues
231
- /stats - Show token usage statistics
232
- /help - Show available commands
233
- /zaion - Show system configurations
234
- /sati <qnt> - Show specific memories
235
- /restart - Restart the Morpheus agent
359
+ ${this.HELP_MESSAGE}
236
360
 
237
361
  How can I assist you today?`;
238
362
  await ctx.reply(welcomeMessage);
@@ -316,7 +440,7 @@ How can I assist you today?`;
316
440
  if (groupedStats.length > 0) {
317
441
  response += '*Breakdown by Provider and Model:*\n';
318
442
  for (const stat of groupedStats) {
319
- response += `- ${stat.provider}/${stat.model}: ${stat.totalTokens} tokens (${stat.messageCount} messages)\n`;
443
+ response += `- ${stat.provider}/${stat.model}:\n ${stat.totalTokens} tokens\n(${stat.messageCount} messages)\n\n`;
320
444
  }
321
445
  }
322
446
  else {
@@ -331,9 +455,10 @@ How can I assist you today?`;
331
455
  }
332
456
  }
333
457
  async handleDefaultCommand(ctx, user, command) {
334
- const prompt = `O usuário envio o comando: ${command},
458
+ const prompt = `O usuário enviou o comando: ${command},
335
459
  Não entendemos o comando
336
- temos os seguintes comandos disponíveis: /start, /status, /doctor, /stats, /help, /zaion, /sati <qnt>, /restart
460
+ temos os seguintes comandos disponíveis:
461
+ ${this.HELP_MESSAGE}
337
462
  Identifique se ele talvez tenha errado o comando e pergunte se ele não quis executar outro comando.
338
463
  Só faça isso agora.`;
339
464
  let response = await this.oracle.chat(prompt);
@@ -346,14 +471,7 @@ How can I assist you today?`;
346
471
  const helpMessage = `
347
472
  *Available Commands:*
348
473
 
349
- /start - Show welcome message and available commands
350
- /status - Check the status of the Morpheus agent
351
- /doctor - Diagnose environment and configuration issues
352
- /stats - Show token usage statistics
353
- /help - Show this help message
354
- /zaion - Show system configurations
355
- /sati <qnt> - Show specific memories
356
- /restart - Restart the Morpheus agent
474
+ ${this.HELP_MESSAGE}
357
475
 
358
476
  How can I assist you today?`;
359
477
  await ctx.reply(helpMessage, { parse_mode: 'Markdown' });
@@ -473,4 +591,36 @@ How can I assist you today?`;
473
591
  this.display.log(`Error checking restart notification: ${error.message}`, { source: 'Telegram', level: 'error' });
474
592
  }
475
593
  }
594
+ async handleMcpListCommand(ctx, user) {
595
+ try {
596
+ const servers = await MCPManager.listServers();
597
+ if (servers.length === 0) {
598
+ await ctx.reply('*No MCP Servers Configured*\n\nThere are currently no MCP servers configured in the system.', { parse_mode: 'Markdown' });
599
+ return;
600
+ }
601
+ let response = `*MCP Servers (${servers.length})*\n\n`;
602
+ servers.forEach((server, index) => {
603
+ const status = server.enabled ? '✅ Enabled' : '❌ Disabled';
604
+ const transport = server.config.transport.toUpperCase();
605
+ response += `*${index + 1}. ${server.name}*\n`;
606
+ response += `Status: ${status}\n`;
607
+ response += `Transport: ${transport}\n`;
608
+ if (server.config.transport === 'stdio') {
609
+ response += `Command: \`${server.config.command}\`\n`;
610
+ if (server.config.args && server.config.args.length > 0) {
611
+ response += `Args: \`${server.config.args.join(' ')}\`\n`;
612
+ }
613
+ }
614
+ else if (server.config.transport === 'http') {
615
+ response += `URL: \`${server.config.url}\`\n`;
616
+ }
617
+ response += '\n';
618
+ });
619
+ await ctx.reply(response, { parse_mode: 'Markdown' });
620
+ }
621
+ catch (error) {
622
+ this.display.log('Error listing MCP servers: ' + (error instanceof Error ? error.message : String(error)), { source: 'Telegram', level: 'error' });
623
+ await ctx.reply('An error occurred while retrieving the list of MCP servers. Please check the logs for more details.', { parse_mode: 'Markdown' });
624
+ }
625
+ }
476
626
  }
@@ -144,7 +144,7 @@ export const doctorCommand = new Command('doctor')
144
144
  }
145
145
  // 5. Check Sati Memory DB
146
146
  try {
147
- const satiDbPath = path.join(PATHS.memory, 'santi-memory.db');
147
+ const satiDbPath = path.join(PATHS.memory, 'sati-memory.db');
148
148
  if (await fs.pathExists(satiDbPath)) {
149
149
  console.log(chalk.green('✓') + ' Sati Memory: Database exists');
150
150
  }
@@ -0,0 +1,79 @@
1
+ import { Command } from 'commander';
2
+ import chalk from 'chalk';
3
+ import { ConfigManager } from '../../config/manager.js';
4
+ import { confirm } from '@inquirer/prompts';
5
+ const session = new Command('session')
6
+ .description('Manage chat sessions');
7
+ session.command('new')
8
+ .description('Archive current session and start a new one')
9
+ .action(async () => {
10
+ const confirmNew = await confirm({
11
+ message: 'Are you sure you want to start a new session?',
12
+ default: false,
13
+ });
14
+ if (confirmNew) {
15
+ const config = ConfigManager.getInstance().get();
16
+ const port = config.ui.port || 3333;
17
+ const authPass = process.env.THE_ARCHITECT_PASS || 'iamthearchitect';
18
+ try {
19
+ const response = await fetch(`http://localhost:${port}/api/session/reset`, {
20
+ method: 'POST',
21
+ headers: {
22
+ 'Content-Type': 'application/json',
23
+ 'x-architect-pass': authPass
24
+ }
25
+ });
26
+ if (response.ok) {
27
+ console.log(chalk.green('✓ New session started successfully on running Morpheus instance.'));
28
+ }
29
+ else {
30
+ const errorText = await response.text();
31
+ console.log(chalk.red(`Failed: ${response.status} ${response.statusText}`));
32
+ if (errorText)
33
+ console.log(chalk.gray(errorText));
34
+ }
35
+ }
36
+ catch (err) {
37
+ console.log(chalk.red('Could not connect to Morpheus daemon.'));
38
+ console.log(chalk.yellow(`Ensure Morpheus is running and listening on port ${port}.`));
39
+ }
40
+ }
41
+ else {
42
+ console.log(chalk.yellow('Session reset cancelled. Current session is intact.'));
43
+ }
44
+ });
45
+ session.command('status')
46
+ .description('Get current session status')
47
+ .action(async () => {
48
+ const config = ConfigManager.getInstance().get();
49
+ const port = config.ui.port || 3333;
50
+ const authPass = process.env.THE_ARCHITECT_PASS || 'iamthearchitect';
51
+ try {
52
+ const response = await fetch(`http://localhost:${port}/api/session/status`, {
53
+ method: 'GET',
54
+ headers: {
55
+ 'Content-Type': 'application/json',
56
+ 'x-architect-pass': authPass
57
+ }
58
+ });
59
+ if (response.ok) {
60
+ const data = await response.json();
61
+ console.log(chalk.bold('Current Session Status:'));
62
+ console.log(`- Session ID: ${data.id}`);
63
+ console.log(`- Messages in Session: ${data.messageCount}`);
64
+ console.log(`- Embedded: ${data.embedded}`);
65
+ console.log(`- Embedding Status: ${data.embedding_status}`);
66
+ }
67
+ else {
68
+ const errorText = await response.text();
69
+ console.log(chalk.red(`Failed: ${response.status} ${response.statusText}`));
70
+ if (errorText)
71
+ console.log(chalk.gray(errorText));
72
+ }
73
+ }
74
+ catch (err) {
75
+ console.log(chalk.red('Could not connect to Morpheus daemon.'));
76
+ console.log(chalk.yellow(`Ensure Morpheus is running and listening on port ${port}.`));
77
+ }
78
+ });
79
+ export const sessionCommand = session;
@@ -12,6 +12,7 @@ import { Oracle } from '../../runtime/oracle.js';
12
12
  import { ProviderError } from '../../runtime/errors.js';
13
13
  import { HttpServer } from '../../http/server.js';
14
14
  import { getVersion } from '../utils/version.js';
15
+ import { startSessionEmbeddingScheduler } from '../../runtime/session-embedding-scheduler.js';
15
16
  export const startCommand = new Command('start')
16
17
  .description('Start the Morpheus agent')
17
18
  .option('--ui', 'Enable web UI', true)
@@ -103,6 +104,8 @@ export const startCommand = new Command('start')
103
104
  display.log(chalk.yellow('Telegram enabled but no token provided. Skipping.'));
104
105
  }
105
106
  }
107
+ // Start Background Services
108
+ startSessionEmbeddingScheduler();
106
109
  // Handle graceful shutdown
107
110
  const shutdown = async (signal) => {
108
111
  display.stopSpinner();
package/dist/cli/index.js CHANGED
@@ -6,6 +6,7 @@ import { configCommand } from './commands/config.js';
6
6
  import { doctorCommand } from './commands/doctor.js';
7
7
  import { initCommand } from './commands/init.js';
8
8
  import { restartCommand } from './commands/restart.js';
9
+ import { sessionCommand } from './commands/session.js';
9
10
  import { scaffold } from '../runtime/scaffold.js';
10
11
  import { getVersion } from './utils/version.js';
11
12
  export async function cli() {
@@ -24,7 +25,8 @@ export async function cli() {
24
25
  program.addCommand(statusCommand);
25
26
  program.addCommand(configCommand);
26
27
  program.addCommand(doctorCommand);
27
- program.parse(process.argv);
28
+ program.addCommand(sessionCommand);
29
+ await program.parseAsync(process.argv);
28
30
  }
29
31
  // Support direct execution via tsx
30
32
  if (import.meta.url.startsWith('file:') && (process.argv[1]?.endsWith('index.ts') || process.argv[1]?.endsWith('cli/index.js'))) {
package/dist/http/api.js CHANGED
@@ -23,6 +23,31 @@ async function readLastLines(filePath, n) {
23
23
  export function createApiRouter() {
24
24
  const router = Router();
25
25
  const configManager = ConfigManager.getInstance();
26
+ const history = new SQLiteChatMessageHistory({ sessionId: 'api-reader' });
27
+ router.post('/session/reset', async (req, res) => {
28
+ // if (!oracle) {
29
+ // return res.status(503).json({ error: 'Oracle unavailable' });
30
+ // }
31
+ try {
32
+ await history.createNewSession();
33
+ res.json({ success: true, message: 'New session started' });
34
+ }
35
+ catch (err) {
36
+ res.status(500).json({ error: err.message });
37
+ }
38
+ });
39
+ router.post('/session/status', async (req, res) => {
40
+ try {
41
+ const sessionStatus = await history.getSessionStatus();
42
+ if (!sessionStatus) {
43
+ return res.status(404).json({ error: 'No session found' });
44
+ }
45
+ res.json(sessionStatus);
46
+ }
47
+ catch (err) {
48
+ res.status(500).json({ error: err.message });
49
+ }
50
+ });
26
51
  router.get('/status', async (req, res) => {
27
52
  let version = 'unknown';
28
53
  try {
@@ -0,0 +1,54 @@
1
+ import Database from 'better-sqlite3';
2
+ import { EmbeddingService } from './embedding.service.js';
3
+ import path from 'path';
4
+ import { homedir } from 'os';
5
+ import loadVecExtension from './sqlite-vec.js';
6
+ const db = new Database(path.join(homedir(), '.morpheus', 'memory', 'sati-memory.db'));
7
+ db.pragma('journal_mode = WAL');
8
+ // 🔥 ISSO AQUI É O QUE ESTÁ FALTANDO
9
+ loadVecExtension(db);
10
+ const embeddingService = await EmbeddingService.getInstance();
11
+ const BATCH_SIZE = 50;
12
+ async function run() {
13
+ console.log('🔎 Buscando memórias sem embedding vetorial...');
14
+ while (true) {
15
+ const rows = db.prepare(`
16
+ SELECT m.rowid, m.summary, m.details
17
+ FROM long_term_memory m
18
+ LEFT JOIN memory_vec v ON m.rowid = v.rowid
19
+ WHERE v.rowid IS NULL
20
+ LIMIT ?
21
+ `).all(BATCH_SIZE);
22
+ if (rows.length === 0) {
23
+ console.log('✅ Todas as memórias já possuem embedding.');
24
+ break;
25
+ }
26
+ console.log(`⚙️ Processando batch de ${rows.length} memórias...`);
27
+ const vectors = [];
28
+ for (const row of rows) {
29
+ const text = `${row.summary} ${row.details || ''}`.trim();
30
+ const vector = await embeddingService.generate(text);
31
+ vectors.push({ rowid: row.rowid, vector });
32
+ }
33
+ const insertVec = db.prepare(`
34
+ INSERT INTO memory_vec (embedding)
35
+ VALUES (?)
36
+ `);
37
+ const insertMap = db.prepare(`
38
+ INSERT INTO memory_embedding_map (memory_id, vec_rowid)
39
+ VALUES (?, ?)
40
+ `);
41
+ const transaction = db.transaction((items) => {
42
+ for (const item of items) {
43
+ const result = insertVec.run(new Float32Array(item.vector));
44
+ const vecRowId = result.lastInsertRowid;
45
+ insertMap.run(item.memory_id, vecRowId);
46
+ }
47
+ });
48
+ transaction(vectors);
49
+ }
50
+ console.log('🎉 Backfill concluído.');
51
+ }
52
+ run().catch(err => {
53
+ console.error('❌ Erro no backfill:', err);
54
+ });
@@ -0,0 +1,21 @@
1
+ import { pipeline } from '@xenova/transformers';
2
+ export class EmbeddingService {
3
+ static instance;
4
+ extractor;
5
+ constructor() { }
6
+ static async getInstance() {
7
+ if (!EmbeddingService.instance) {
8
+ const service = new EmbeddingService();
9
+ service.extractor = await pipeline('feature-extraction', 'Xenova/all-MiniLM-L6-v2');
10
+ EmbeddingService.instance = service;
11
+ }
12
+ return EmbeddingService.instance;
13
+ }
14
+ async generate(text) {
15
+ const output = await this.extractor(text, {
16
+ pooling: 'mean',
17
+ normalize: true,
18
+ });
19
+ return Array.from(output.data);
20
+ }
21
+ }
@@ -18,16 +18,16 @@ export class SatiMemoryMiddleware {
18
18
  try {
19
19
  // Extract recent messages content strings for context
20
20
  const recentText = history.slice(-10).map(m => m.content.toString());
21
- display.log(`[Sati] Searching memories for: "${currentMessage.substring(0, 50)}${currentMessage.length > 50 ? '...' : ''}"`, { source: 'Sati' });
21
+ display.log(`Searching memories for: "${currentMessage.substring(0, 50)}${currentMessage.length > 50 ? '...' : ''}"`, { source: 'Sati' });
22
22
  const result = await this.service.recover(currentMessage, recentText);
23
23
  if (result.relevant_memories.length === 0) {
24
- display.log('[Sati] No relevant memories found', { source: 'Sati' });
24
+ display.log('No relevant memories found', { source: 'Sati' });
25
25
  return null;
26
26
  }
27
27
  const memoryContext = result.relevant_memories
28
28
  .map(m => `- [${m.category.toUpperCase()}] ${m.summary}`)
29
29
  .join('\n');
30
- display.log(`[Sati] Retrieved ${result.relevant_memories.length} memories.`, { source: 'Sati' });
30
+ display.log(`Retrieved ${result.relevant_memories.length} memories.`, { source: 'Sati' });
31
31
  return new AIMessage(`
32
32
  ### LONG-TERM MEMORY (SATI)
33
33
  The following information was retrieved from previous sessions. Use it if relevant:
@@ -36,7 +36,7 @@ export class SatiMemoryMiddleware {
36
36
  `);
37
37
  }
38
38
  catch (error) {
39
- display.log(`[SatiMiddleware] Error in beforeAgent: ${error}`, { source: 'Sati' });
39
+ display.log(`Error in beforeAgent: ${error}`, { source: 'Sati' });
40
40
  // Fail open: return null so execution continues without memory
41
41
  return null;
42
42
  }
@@ -52,7 +52,7 @@ export class SatiMemoryMiddleware {
52
52
  ]);
53
53
  }
54
54
  catch (error) {
55
- console.error('[SatiMiddleware] Error in afterAgent:', error);
55
+ display.log(`Error in afterAgent: ${error}`, { source: 'Sati' });
56
56
  }
57
57
  }
58
58
  }