morpheus-cli 0.2.7 → 0.3.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 CHANGED
@@ -52,6 +52,10 @@ morpheus restart
52
52
 
53
53
  # Diagnose issues
54
54
  morpheus doctor
55
+
56
+ # Manage sessions
57
+ morpheus session new # Archive current and start new
58
+ morpheus session status # Check current session info
55
59
  ```
56
60
 
57
61
  ## Troubleshooting
@@ -93,6 +97,14 @@ Morpheus is built with **Node.js** and **TypeScript**, using **LangChain** as th
93
97
  ### 🖥️ Web Dashboard
94
98
  Local React-based UI to manage recordings, chat history, and system status across your agent instances.
95
99
 
100
+ **New: Interactive Web Chat**
101
+ - Full-featured chat interface accessible from the browser
102
+ - Session management: create, archive, delete, and rename sessions
103
+ - Cross-channel visibility: view and interact with sessions started on any channel (Telegram, Web, etc.)
104
+ - Real-time messaging with the Oracle agent
105
+ - Responsive design with collapsible sidebar
106
+ - Full support for Light and Dark (Matrix) themes
107
+
96
108
  #### 🔒 UI Authentication
97
109
  To protect your Web UI, use the `THE_ARCHITECT_PASS` environment variable. This ensures only authorized users can access the dashboard and API.
98
110
 
@@ -121,13 +133,15 @@ The system also supports generic environment variables that apply to all provide
121
133
  | `MORPHEUS_LLM_MAX_TOKENS` | Maximum tokens for LLM | llm.max_tokens |
122
134
  | `MORPHEUS_LLM_CONTEXT_WINDOW` | Context window size for LLM | llm.context_window |
123
135
  | `MORPHEUS_LLM_API_KEY` | Generic API key for LLM (lower precedence than provider-specific keys) | llm.api_key |
124
- | `MORPHEUS_SANTI_PROVIDER` | Sati provider to use | santi.provider |
125
- | `MORPHEUS_SANTI_MODEL` | Model name for Sati | santi.model |
126
- | `MORPHEUS_SANTI_TEMPERATURE` | Temperature setting for Sati | santi.temperature |
127
- | `MORPHEUS_SANTI_MAX_TOKENS` | Maximum tokens for Sati | santi.max_tokens |
128
- | `MORPHEUS_SANTI_CONTEXT_WINDOW` | Context window size for Sati | santi.context_window |
129
- | `MORPHEUS_SANTI_API_KEY` | Generic API key for Sati (lower precedence than provider-specific keys) | santi.api_key |
130
- | `MORPHEUS_SANTI_MEMORY_LIMIT` | Memory retrieval limit for Sati | santi.memory_limit |
136
+ | `MORPHEUS_SATI_PROVIDER` | Sati provider to use | santi.provider |
137
+ | `MORPHEUS_SATI_MODEL` | Model name for Sati | santi.model |
138
+ | `MORPHEUS_SATI_TEMPERATURE` | Temperature setting for Sati | santi.temperature |
139
+ | `MORPHEUS_SATI_MAX_TOKENS` | Maximum tokens for Sati | santi.max_tokens |
140
+ | `MORPHEUS_SATI_CONTEXT_WINDOW` | Context window size for Sati | santi.context_window |
141
+ | `MORPHEUS_SATI_API_KEY` | Generic API key for Sati (lower precedence than provider-specific keys) | santi.api_key |
142
+ | `MORPHEUS_SATI_MEMORY_LIMIT` | Memory retrieval limit for Sati | santi.memory_limit |
143
+ | `MORPHEUS_SATI_MEMORY_LIMIT` | Memory retrieval limit for Sati | santi.memory_limit |
144
+ | `MORPHEUS_SATI_ENABLED_ARCHIVED_SESSIONS`| Enable/disable retrieval of archived sessions in Sati | santi.enableArchivedSessions |
131
145
  | `MORPHEUS_AUDIO_MODEL` | Model name for audio processing | audio.model |
132
146
  | `MORPHEUS_AUDIO_ENABLED` | Enable/disable audio processing | audio.enabled |
133
147
  | `MORPHEUS_AUDIO_API_KEY` | Generic API key for audio (lower precedence than provider-specific keys) | audio.apiKey |
@@ -209,6 +223,8 @@ The Morpheus Telegram bot supports several commands for interacting with the age
209
223
  - `/help` - Show available commands
210
224
  - `/zaion` - Show system configurations
211
225
  - `/sati <qnt>` - Show specific memories
226
+ - `/newsession` - Archive current session and start fresh
227
+ - `/sessions` - List all sessions with options to switch, archive, or delete
212
228
  - `/restart` - Restart the Morpheus agent
213
229
  - `/mcp` or `/mcps` - List registered MCP servers
214
230
 
@@ -364,6 +380,33 @@ Get the current status of the Morpheus agent.
364
380
  }
365
381
  ```
366
382
 
383
+ ### Session Endpoints
384
+
385
+ #### POST `/api/session/reset`
386
+ Archive the current session and start a new one.
387
+
388
+ * **Authentication:** Requires `Authorization` header with the password set in `THE_ARCHITECT_PASS`.
389
+ * **Response:**
390
+ ```json
391
+ {
392
+ "success": true,
393
+ "message": "New session started"
394
+ }
395
+ ```
396
+
397
+ #### POST `/api/session/status`
398
+ Get the status of the current session.
399
+
400
+ * **Authentication:** Requires `Authorization` header with the password set in `THE_ARCHITECT_PASS`.
401
+ * **Response:**
402
+ ```json
403
+ {
404
+ "id": "uuid-...",
405
+ "messageCount": 42,
406
+ "embedding_status": "pending"
407
+ }
408
+ ```
409
+
367
410
  ### Configuration Endpoints
368
411
 
369
412
  #### GET `/api/config`
@@ -848,7 +891,7 @@ version: '3.8'
848
891
 
849
892
  services:
850
893
  morpheus:
851
- image: morpheus/morpheus-agent:latest
894
+ image: marcosnunesmbs/morpheus:latest
852
895
  container_name: morpheus-agent
853
896
  ports:
854
897
  - "3333:3333"
@@ -19,6 +19,18 @@ export class TelegramAdapter {
19
19
  config = ConfigManager.getInstance();
20
20
  oracle;
21
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`;
22
34
  constructor(oracle) {
23
35
  this.oracle = oracle;
24
36
  }
@@ -139,6 +151,120 @@ export class TelegramAdapter {
139
151
  }
140
152
  }
141
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}`, { show_alert: true });
191
+ }
192
+ });
193
+ // --- Archive Flow ---
194
+ this.bot.action(/^ask_archive_session_/, async (ctx) => {
195
+ const data = ctx.callbackQuery.data;
196
+ const sessionId = data.replace('ask_archive_session_', '');
197
+ // Fetch session title for better UX (optional, but nice) - for now just use ID
198
+ await ctx.reply(`⚠️ **ARCHIVE SESSION?**\n\nAre you sure you want to archive session \`${sessionId}\`?\n\nIt will be moved to long-term memory (SATI) and removed from the active list. This action cannot be easily undone via Telegram.`, {
199
+ parse_mode: 'Markdown',
200
+ reply_markup: {
201
+ inline_keyboard: [
202
+ [
203
+ { text: '✅ Yes, Archive', callback_data: `confirm_archive_session_${sessionId}` },
204
+ { text: '❌ Cancel', callback_data: 'cancel_session_action' }
205
+ ]
206
+ ]
207
+ }
208
+ });
209
+ await ctx.answerCbQuery();
210
+ });
211
+ this.bot.action(/^confirm_archive_session_/, async (ctx) => {
212
+ const data = ctx.callbackQuery.data;
213
+ const sessionId = data.replace('confirm_archive_session_', '');
214
+ try {
215
+ const history = new SQLiteChatMessageHistory({ sessionId: "" });
216
+ await history.archiveSession(sessionId);
217
+ await ctx.answerCbQuery('Session archived successfully');
218
+ if (ctx.updateType === 'callback_query') {
219
+ ctx.deleteMessage().catch(() => { });
220
+ }
221
+ await ctx.reply(`✅ Session \`${sessionId}\` has been archived and moved to long-term memory.`, { parse_mode: 'Markdown' });
222
+ }
223
+ catch (error) {
224
+ await ctx.answerCbQuery(`Error archiving: ${error.message}`, { show_alert: true });
225
+ }
226
+ });
227
+ // --- Delete Flow ---
228
+ this.bot.action(/^ask_delete_session_/, async (ctx) => {
229
+ const data = ctx.callbackQuery.data;
230
+ const sessionId = data.replace('ask_delete_session_', '');
231
+ await ctx.reply(`🚫 **DELETE SESSION?**\n\nAre you sure you want to PERMANENTLY DELETE session \`${sessionId}\`?\n\nThis action is **IRREVERSIBLE**. All data will be lost.`, {
232
+ parse_mode: 'Markdown',
233
+ reply_markup: {
234
+ inline_keyboard: [
235
+ [
236
+ { text: '🗑️ Yes, DELETE PERMANENTLY', callback_data: `confirm_delete_session_${sessionId}` },
237
+ { text: '❌ Cancel', callback_data: 'cancel_session_action' }
238
+ ]
239
+ ]
240
+ }
241
+ });
242
+ await ctx.answerCbQuery();
243
+ });
244
+ this.bot.action(/^confirm_delete_session_/, async (ctx) => {
245
+ const data = ctx.callbackQuery.data;
246
+ const sessionId = data.replace('confirm_delete_session_', '');
247
+ try {
248
+ const history = new SQLiteChatMessageHistory({ sessionId: "" });
249
+ await history.deleteSession(sessionId);
250
+ await ctx.answerCbQuery('Session deleted successfully');
251
+ if (ctx.updateType === 'callback_query') {
252
+ ctx.deleteMessage().catch(() => { });
253
+ }
254
+ await ctx.reply(`🗑️ Session \`${sessionId}\` has been permanently deleted.`, { parse_mode: 'Markdown' });
255
+ }
256
+ catch (error) {
257
+ await ctx.answerCbQuery(`Error deleting: ${error.message}`, { show_alert: true });
258
+ }
259
+ });
260
+ // --- Cancel Action ---
261
+ this.bot.action('cancel_session_action', async (ctx) => {
262
+ await ctx.answerCbQuery('Action cancelled');
263
+ if (ctx.updateType === 'callback_query') {
264
+ ctx.deleteMessage().catch(() => { });
265
+ }
266
+ await ctx.reply('Action cancelled.');
267
+ });
142
268
  this.bot.launch().catch((err) => {
143
269
  if (this.isConnected) {
144
270
  this.display.log(`Telegram bot error: ${err}`, { source: 'Telegram', level: 'error' });
@@ -188,6 +314,11 @@ export class TelegramAdapter {
188
314
  this.bot = null;
189
315
  this.display.log(chalk.gray('Telegram disconnected.'), { source: 'Telegram' });
190
316
  }
317
+ /**
318
+ ** =========================
319
+ ** Commands Handlers
320
+ ** =========================
321
+ */
191
322
  async handleSystemCommand(ctx, text, user) {
192
323
  const command = text.split(' ')[0];
193
324
  const args = text.split(' ').slice(1);
@@ -220,24 +351,97 @@ export class TelegramAdapter {
220
351
  case '/mcps':
221
352
  await this.handleMcpListCommand(ctx, user);
222
353
  break;
354
+ case '/newsession':
355
+ case '/reset':
356
+ await this.handleNewSessionCommand(ctx, user);
357
+ break;
358
+ case '/sessionstatus':
359
+ case '/session':
360
+ case '/sessions':
361
+ await this.handleSessionStatusCommand(ctx, user);
362
+ break;
223
363
  default:
224
364
  await this.handleDefaultCommand(ctx, user, command);
225
365
  }
226
366
  }
367
+ async handleNewSessionCommand(ctx, user) {
368
+ try {
369
+ await ctx.reply("Are you ready to start a new session? Please confirm.", {
370
+ parse_mode: 'Markdown', reply_markup: {
371
+ inline_keyboard: [
372
+ [{ text: 'Yes, start new session', callback_data: 'confirm_new_session' }, { text: 'No, cancel', callback_data: 'cancel_new_session' }]
373
+ ]
374
+ }
375
+ });
376
+ }
377
+ catch (e) {
378
+ await ctx.reply(`Error starting new session: ${e.message}`);
379
+ }
380
+ }
381
+ async handleApproveNewSessionCommand(ctx, user) {
382
+ try {
383
+ const history = new SQLiteChatMessageHistory({ sessionId: "" });
384
+ await history.createNewSession();
385
+ }
386
+ catch (e) {
387
+ await ctx.reply(`Error creating new session: ${e.message}`);
388
+ }
389
+ }
390
+ async handleSessionStatusCommand(ctx, user) {
391
+ try {
392
+ // Obter todas as sessões ativas e pausadas usando a nova função
393
+ const history = new SQLiteChatMessageHistory({ sessionId: "" });
394
+ const sessions = await history.listSessions();
395
+ if (sessions.length === 0) {
396
+ await ctx.reply('No active or paused sessions found.', { parse_mode: 'Markdown' });
397
+ return;
398
+ }
399
+ let response = '*Sessions:*\n\n';
400
+ const keyboard = [];
401
+ for (const session of sessions) {
402
+ const title = session.title || 'Untitled Session';
403
+ const statusEmoji = session.status === 'active' ? '🟢' : '🟡';
404
+ response += `${statusEmoji} *${title}*\n`;
405
+ response += `- ID: ${session.id}\n`;
406
+ response += `- Status: ${session.status}\n`;
407
+ response += `- Started: ${new Date(session.started_at).toLocaleString()}\n\n`;
408
+ // Adicionar botão inline para alternar para esta sessão
409
+ const sessionButtons = [];
410
+ if (session.status !== 'active') {
411
+ sessionButtons.push({
412
+ text: `➡️ Switch`,
413
+ callback_data: `switch_session_${session.id}`
414
+ });
415
+ }
416
+ sessionButtons.push({
417
+ text: `📂 Archive`,
418
+ callback_data: `ask_archive_session_${session.id}`
419
+ });
420
+ sessionButtons.push({
421
+ text: `🗑️ Delete`,
422
+ callback_data: `ask_delete_session_${session.id}`
423
+ });
424
+ keyboard.push(sessionButtons);
425
+ }
426
+ await ctx.reply(response, {
427
+ parse_mode: 'Markdown',
428
+ reply_markup: {
429
+ inline_keyboard: keyboard
430
+ }
431
+ });
432
+ history.close();
433
+ }
434
+ catch (e) {
435
+ await ctx.reply(`Error retrieving session status: ${e.message}`);
436
+ }
437
+ }
227
438
  async handleStartCommand(ctx, user) {
228
439
  const welcomeMessage = `
229
440
  Hello, @${user}! I am ${this.config.get().agent.name}, ${this.config.get().agent.personality}.
230
441
 
231
442
  I am your local AI operator/agent. Here are the commands you can use:
232
443
 
233
- /start - Show this welcome message and available commands
234
- /status - Check the status of the Morpheus agent
235
- /doctor - Diagnose environment and configuration issues
236
- /stats - Show token usage statistics
237
- /help - Show available commands
238
- /zaion - Show system configurations
239
- /sati <qnt> - Show specific memories
240
- /restart - Restart the Morpheus agent
444
+ ${this.HELP_MESSAGE}
241
445
 
242
446
  How can I assist you today?`;
243
447
  await ctx.reply(welcomeMessage);
@@ -321,7 +525,7 @@ How can I assist you today?`;
321
525
  if (groupedStats.length > 0) {
322
526
  response += '*Breakdown by Provider and Model:*\n';
323
527
  for (const stat of groupedStats) {
324
- response += `- ${stat.provider}/${stat.model}: ${stat.totalTokens} tokens (${stat.messageCount} messages)\n`;
528
+ response += `- ${stat.provider}/${stat.model}:\n ${stat.totalTokens} tokens\n(${stat.messageCount} messages)\n\n`;
325
529
  }
326
530
  }
327
531
  else {
@@ -336,9 +540,10 @@ How can I assist you today?`;
336
540
  }
337
541
  }
338
542
  async handleDefaultCommand(ctx, user, command) {
339
- const prompt = `O usuário envio o comando: ${command},
543
+ const prompt = `O usuário enviou o comando: ${command},
340
544
  Não entendemos o comando
341
- temos os seguintes comandos disponíveis: /start, /status, /doctor, /stats, /help, /zaion, /sati <qnt>, /restart, /mcp, /mcps
545
+ temos os seguintes comandos disponíveis:
546
+ ${this.HELP_MESSAGE}
342
547
  Identifique se ele talvez tenha errado o comando e pergunte se ele não quis executar outro comando.
343
548
  Só faça isso agora.`;
344
549
  let response = await this.oracle.chat(prompt);
@@ -351,17 +556,9 @@ How can I assist you today?`;
351
556
  const helpMessage = `
352
557
  *Available Commands:*
353
558
 
354
- /start - Show welcome message and available commands
355
- /status - Check the status of the Morpheus agent
356
- /doctor - Diagnose environment and configuration issues
357
- /stats - Show token usage statistics
358
- /help - Show this help message
359
- /zaion - Show system configurations
360
- /sati <qnt> - Show specific memories
361
- /restart - Restart the Morpheus agent
362
- /mcp or /mcps - List registered MCP servers
559
+ ${this.HELP_MESSAGE}
363
560
 
364
- How can I assist you today? `;
561
+ How can I assist you today?`;
365
562
  await ctx.reply(helpMessage, { parse_mode: 'Markdown' });
366
563
  }
367
564
  async handleZaionCommand(ctx, user) {
@@ -423,6 +620,7 @@ How can I assist you today? `;
423
620
  async handleRestartCommand(ctx, user) {
424
621
  // Store the user ID who requested the restart
425
622
  const userId = ctx.from.id;
623
+ const updateId = ctx.update.update_id;
426
624
  // Save the user ID to a temporary file so the restarted process can notify them
427
625
  const restartNotificationFile = path.join(os.tmpdir(), 'morpheus_restart_notification.json');
428
626
  try {
@@ -433,6 +631,15 @@ How can I assist you today? `;
433
631
  }
434
632
  // Respond to the user first
435
633
  await ctx.reply('🔄 Restart initiated. The Morpheus agent will restart shortly.');
634
+ // Acknowledge this update to Telegram by advancing the offset past it.
635
+ // Without this, Telegraf's in-memory offset is lost on process.exit() and the
636
+ // /restart message gets re-delivered on the next startup, causing an infinite loop.
637
+ try {
638
+ await ctx.telegram.callApi('getUpdates', { offset: updateId + 1, limit: 1, timeout: 0 });
639
+ }
640
+ catch (e) {
641
+ // Best-effort — proceed with restart regardless
642
+ }
436
643
  // Schedule the restart after a short delay to ensure the response is sent
437
644
  setTimeout(() => {
438
645
  // Stop the bot to prevent processing more messages
@@ -452,7 +659,7 @@ How can I assist you today? `;
452
659
  restartProcess.unref();
453
660
  // Exit the current process
454
661
  process.exit(0);
455
- }, 500); // Shorter delay to minimize chance of processing more messages
662
+ }, 500);
456
663
  }
457
664
  async checkAndSendRestartNotification() {
458
665
  const restartNotificationFile = path.join(os.tmpdir(), 'morpheus_restart_notification.json');
@@ -49,7 +49,7 @@ export const doctorCommand = new Command('doctor')
49
49
  }
50
50
  // Check API keys availability for active providers
51
51
  const llmProvider = config.llm?.provider;
52
- const santiProvider = config.santi?.provider;
52
+ const satiProvider = config.sati?.provider;
53
53
  // Check LLM provider API key
54
54
  if (llmProvider && llmProvider !== 'ollama') {
55
55
  const hasLlmApiKey = config.llm?.api_key ||
@@ -65,18 +65,18 @@ export const doctorCommand = new Command('doctor')
65
65
  allPassed = false;
66
66
  }
67
67
  }
68
- // Check Santi provider API key
69
- if (santiProvider && santiProvider !== 'ollama') {
70
- const hasSantiApiKey = config.santi?.api_key ||
71
- (santiProvider === 'openai' && process.env.OPENAI_API_KEY) ||
72
- (santiProvider === 'anthropic' && process.env.ANTHROPIC_API_KEY) ||
73
- (santiProvider === 'gemini' && process.env.GOOGLE_API_KEY) ||
74
- (santiProvider === 'openrouter' && process.env.OPENROUTER_API_KEY);
68
+ // Check Sati provider API key
69
+ if (satiProvider && satiProvider !== 'ollama') {
70
+ const hasSantiApiKey = config.sati?.api_key ||
71
+ (satiProvider === 'openai' && process.env.OPENAI_API_KEY) ||
72
+ (satiProvider === 'anthropic' && process.env.ANTHROPIC_API_KEY) ||
73
+ (satiProvider === 'gemini' && process.env.GOOGLE_API_KEY) ||
74
+ (satiProvider === 'openrouter' && process.env.OPENROUTER_API_KEY);
75
75
  if (hasSantiApiKey) {
76
- console.log(chalk.green('✓') + ` Santi API key available for ${santiProvider}`);
76
+ console.log(chalk.green('✓') + ` Sati API key available for ${satiProvider}`);
77
77
  }
78
78
  else {
79
- console.log(chalk.red('✗') + ` Santi API key missing for ${santiProvider}. Either set in config or define environment variable.`);
79
+ console.log(chalk.red('✗') + ` Sati API key missing for ${satiProvider}. Either set in config or define environment variable.`);
80
80
  allPassed = false;
81
81
  }
82
82
  }
@@ -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
  }
@@ -117,15 +117,15 @@ export const initCommand = new Command('init')
117
117
  ],
118
118
  default: 'no',
119
119
  });
120
- let santiProvider = provider;
121
- let santiModel = model;
122
- let santiApiKey = apiKey;
120
+ let satiProvider = provider;
121
+ let satiModel = model;
122
+ let satiApiKey = apiKey;
123
123
  // If using main settings and no new key provided, use existing if available
124
- if (configureSati === 'no' && !santiApiKey && hasExistingKey) {
125
- santiApiKey = currentConfig.llm.api_key;
124
+ if (configureSati === 'no' && !satiApiKey && hasExistingKey) {
125
+ satiApiKey = currentConfig.llm.api_key;
126
126
  }
127
127
  if (configureSati === 'yes') {
128
- santiProvider = await select({
128
+ satiProvider = await select({
129
129
  message: 'Select Sati LLM Provider:',
130
130
  choices: [
131
131
  { name: 'OpenAI', value: 'openai' },
@@ -134,10 +134,10 @@ export const initCommand = new Command('init')
134
134
  { name: 'Ollama', value: 'ollama' },
135
135
  { name: 'Google Gemini', value: 'gemini' },
136
136
  ],
137
- default: currentConfig.santi?.provider || provider,
137
+ default: currentConfig.sati?.provider || provider,
138
138
  });
139
139
  let defaultSatiModel = 'gpt-3.5-turbo';
140
- switch (santiProvider) {
140
+ switch (satiProvider) {
141
141
  case 'openai':
142
142
  defaultSatiModel = 'gpt-4o';
143
143
  break;
@@ -154,59 +154,59 @@ export const initCommand = new Command('init')
154
154
  defaultSatiModel = 'gemini-pro';
155
155
  break;
156
156
  }
157
- if (santiProvider === currentConfig.santi?.provider) {
158
- defaultSatiModel = currentConfig.santi?.model || defaultSatiModel;
157
+ if (satiProvider === currentConfig.sati?.provider) {
158
+ defaultSatiModel = currentConfig.sati?.model || defaultSatiModel;
159
159
  }
160
- santiModel = await input({
160
+ satiModel = await input({
161
161
  message: 'Enter Sati Model Name:',
162
162
  default: defaultSatiModel,
163
163
  });
164
- const hasExistingSatiKey = !!currentConfig.santi?.api_key;
165
- let santiKeyMsg = hasExistingSatiKey
164
+ const hasExistingSatiKey = !!currentConfig.sati?.api_key;
165
+ let satiKeyMsg = hasExistingSatiKey
166
166
  ? 'Enter Sati API Key (leave empty to preserve existing, or if using env vars):'
167
167
  : 'Enter Sati API Key (leave empty if using env vars):';
168
168
  // Add info about environment variables to the message
169
- if (santiProvider === 'openai') {
170
- santiKeyMsg = `${santiKeyMsg} (Env var: OPENAI_API_KEY)`;
169
+ if (satiProvider === 'openai') {
170
+ satiKeyMsg = `${satiKeyMsg} (Env var: OPENAI_API_KEY)`;
171
171
  }
172
- else if (santiProvider === 'anthropic') {
173
- santiKeyMsg = `${santiKeyMsg} (Env var: ANTHROPIC_API_KEY)`;
172
+ else if (satiProvider === 'anthropic') {
173
+ satiKeyMsg = `${satiKeyMsg} (Env var: ANTHROPIC_API_KEY)`;
174
174
  }
175
- else if (santiProvider === 'gemini') {
176
- santiKeyMsg = `${santiKeyMsg} (Env var: GOOGLE_API_KEY)`;
175
+ else if (satiProvider === 'gemini') {
176
+ satiKeyMsg = `${satiKeyMsg} (Env var: GOOGLE_API_KEY)`;
177
177
  }
178
- else if (santiProvider === 'openrouter') {
179
- santiKeyMsg = `${santiKeyMsg} (Env var: OPENROUTER_API_KEY)`;
178
+ else if (satiProvider === 'openrouter') {
179
+ satiKeyMsg = `${satiKeyMsg} (Env var: OPENROUTER_API_KEY)`;
180
180
  }
181
- const keyInput = await password({ message: santiKeyMsg });
181
+ const keyInput = await password({ message: satiKeyMsg });
182
182
  if (keyInput) {
183
- santiApiKey = keyInput;
183
+ satiApiKey = keyInput;
184
184
  }
185
185
  else if (hasExistingSatiKey) {
186
- santiApiKey = currentConfig.santi?.api_key;
186
+ satiApiKey = currentConfig.sati?.api_key;
187
187
  }
188
188
  else {
189
- santiApiKey = undefined; // Ensure we don't accidentally carry over invalid state
189
+ satiApiKey = undefined; // Ensure we don't accidentally carry over invalid state
190
190
  }
191
191
  // Base URL Configuration for Sati OpenRouter
192
- if (santiProvider === 'openrouter') {
192
+ if (satiProvider === 'openrouter') {
193
193
  const satiBaseUrl = await input({
194
194
  message: 'Enter Sati OpenRouter Base URL:',
195
- default: currentConfig.santi?.base_url || 'https://openrouter.ai/api/v1',
195
+ default: currentConfig.sati?.base_url || 'https://openrouter.ai/api/v1',
196
196
  });
197
- await configManager.set('santi.base_url', satiBaseUrl);
197
+ await configManager.set('sati.base_url', satiBaseUrl);
198
198
  }
199
199
  }
200
200
  const memoryLimit = await input({
201
201
  message: 'Sati Memory Retrieval Limit (messages):',
202
- default: currentConfig.santi?.memory_limit?.toString() || '1000',
202
+ default: currentConfig.sati?.memory_limit?.toString() || '1000',
203
203
  validate: (val) => !isNaN(Number(val)) && Number(val) > 0 || 'Must be a positive number'
204
204
  });
205
- await configManager.set('santi.provider', santiProvider);
206
- await configManager.set('santi.model', santiModel);
207
- await configManager.set('santi.memory_limit', Number(memoryLimit));
208
- if (santiApiKey) {
209
- await configManager.set('santi.api_key', santiApiKey);
205
+ await configManager.set('sati.provider', satiProvider);
206
+ await configManager.set('sati.model', satiModel);
207
+ await configManager.set('sati.memory_limit', Number(memoryLimit));
208
+ if (satiApiKey) {
209
+ await configManager.set('sati.api_key', satiApiKey);
210
210
  }
211
211
  // Audio Configuration
212
212
  const audioEnabled = await confirm({
@@ -99,7 +99,7 @@ export const restartCommand = new Command('restart')
99
99
  // Initialize Web UI
100
100
  if (options.ui && config.ui.enabled) {
101
101
  try {
102
- httpServer = new HttpServer();
102
+ httpServer = new HttpServer(oracle);
103
103
  // Use CLI port if provided and valid, otherwise fallback to config or default
104
104
  const port = parseInt(options.port) || config.ui.port || 3333;
105
105
  httpServer.start(port);