morpheus-cli 0.4.3 → 0.4.4

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.
@@ -522,44 +522,83 @@ How can I assist you today?`;
522
522
  }
523
523
  }
524
524
  async handleDoctorCommand(ctx, user) {
525
- // Implementação simplificada do diagnóstico
526
525
  const config = this.config.get();
527
526
  let response = '*Morpheus Doctor*\n\n';
528
527
  // Verificar versão do Node.js
529
528
  const nodeVersion = process.version;
530
529
  const majorVersion = parseInt(nodeVersion.replace('v', '').split('.')[0], 10);
531
530
  if (majorVersion >= 18) {
532
- response += '✅ Node.js Version: ' + nodeVersion + ' (Satisfied)\n';
531
+ response += '✅ Node.js: ' + nodeVersion + '\n';
533
532
  }
534
533
  else {
535
- response += '❌ Node.js Version: ' + nodeVersion + ' (Required: >=18)\n';
534
+ response += '❌ Node.js: ' + nodeVersion + ' (Required: >=18)\n';
536
535
  }
537
- // Verificar configuração
538
536
  if (config) {
539
537
  response += '✅ Configuration: Valid\n';
540
- // Verificar se chave de API disponível para o provedor ativo
538
+ // Helper para verificar API key de um provider
539
+ const hasApiKey = (provider, apiKey) => {
540
+ if (apiKey)
541
+ return true;
542
+ if (provider === 'openai')
543
+ return !!process.env.OPENAI_API_KEY;
544
+ if (provider === 'anthropic')
545
+ return !!process.env.ANTHROPIC_API_KEY;
546
+ if (provider === 'gemini' || provider === 'google')
547
+ return !!process.env.GOOGLE_API_KEY;
548
+ if (provider === 'openrouter')
549
+ return !!process.env.OPENROUTER_API_KEY;
550
+ return false; // ollama and others don't need keys
551
+ };
552
+ // Oracle (LLM)
541
553
  const llmProvider = config.llm?.provider;
542
554
  if (llmProvider && llmProvider !== 'ollama') {
543
- const hasLlmApiKey = config.llm?.api_key ||
544
- (llmProvider === 'openai' && process.env.OPENAI_API_KEY) ||
545
- (llmProvider === 'anthropic' && process.env.ANTHROPIC_API_KEY) ||
546
- (llmProvider === 'gemini' && process.env.GOOGLE_API_KEY) ||
547
- (llmProvider === 'openrouter' && process.env.OPENROUTER_API_KEY);
548
- if (hasLlmApiKey) {
549
- response += `✅ LLM API key available for ${llmProvider}\n`;
555
+ if (hasApiKey(llmProvider, config.llm?.api_key)) {
556
+ response += `✅ Oracle API key (${llmProvider})\n`;
550
557
  }
551
558
  else {
552
- response += `❌ LLM API key missing for ${llmProvider}. Either set in config or define environment variable.\n`;
559
+ response += `❌ Oracle API key missing (${llmProvider})\n`;
553
560
  }
554
561
  }
555
- // Verificar token do Telegram se ativado
562
+ // Sati
563
+ const sati = config.sati;
564
+ const satiProvider = sati?.provider || llmProvider;
565
+ if (satiProvider && satiProvider !== 'ollama') {
566
+ if (hasApiKey(satiProvider, sati?.api_key ?? config.llm?.api_key)) {
567
+ response += `✅ Sati API key (${satiProvider})\n`;
568
+ }
569
+ else {
570
+ response += `❌ Sati API key missing (${satiProvider})\n`;
571
+ }
572
+ }
573
+ // Apoc
574
+ const apoc = config.apoc;
575
+ const apocProvider = apoc?.provider || llmProvider;
576
+ if (apocProvider && apocProvider !== 'ollama') {
577
+ if (hasApiKey(apocProvider, apoc?.api_key ?? config.llm?.api_key)) {
578
+ response += `✅ Apoc API key (${apocProvider})\n`;
579
+ }
580
+ else {
581
+ response += `❌ Apoc API key missing (${apocProvider})\n`;
582
+ }
583
+ }
584
+ // Telegram token
556
585
  if (config.channels?.telegram?.enabled) {
557
586
  const hasTelegramToken = config.channels.telegram?.token || process.env.TELEGRAM_BOT_TOKEN;
558
587
  if (hasTelegramToken) {
559
- response += '✅ Telegram bot token available\n';
588
+ response += '✅ Telegram token\n';
560
589
  }
561
590
  else {
562
- response += '❌ Telegram bot token missing. Either set in config or define TELEGRAM_BOT_TOKEN environment variable.\n';
591
+ response += '❌ Telegram token missing\n';
592
+ }
593
+ }
594
+ // Audio API key
595
+ if (config.audio?.enabled) {
596
+ const audioKey = config.audio?.apiKey || process.env.GOOGLE_API_KEY;
597
+ if (audioKey) {
598
+ response += '✅ Audio API key\n';
599
+ }
600
+ else {
601
+ response += '❌ Audio API key missing\n';
563
602
  }
564
603
  }
565
604
  }
@@ -570,29 +609,46 @@ How can I assist you today?`;
570
609
  }
571
610
  async handleStatsCommand(ctx, user) {
572
611
  try {
573
- // Criar instância temporária do histórico para obter estatísticas
574
612
  const history = new SQLiteChatMessageHistory({
575
613
  sessionId: "default",
576
- databasePath: undefined, // Usará o caminho padrão
577
- limit: 100, // Limite arbitrário para esta operação
614
+ databasePath: undefined,
615
+ limit: 100,
578
616
  });
579
617
  const stats = await history.getGlobalUsageStats();
580
618
  const groupedStats = await history.getUsageStatsByProviderAndModel();
619
+ // Totals from global stats
620
+ const totalTokens = stats.totalInputTokens + stats.totalOutputTokens;
621
+ // Aggregate audio seconds and cost from grouped stats
622
+ const totalAudioSeconds = groupedStats.reduce((sum, s) => sum + (s.totalAudioSeconds || 0), 0);
623
+ const totalCost = stats.totalEstimatedCostUsd;
581
624
  let response = '*Token Usage Statistics*\n\n';
582
- response += `Total Input Tokens: ${stats.totalInputTokens}\n`;
583
- response += `Total Output Tokens: ${stats.totalOutputTokens}\n`;
584
- response += `Total Tokens: ${stats.totalInputTokens + stats.totalOutputTokens}\n\n`;
625
+ response += `Input Tokens: ${stats.totalInputTokens.toLocaleString()}\n`;
626
+ response += `Output Tokens: ${stats.totalOutputTokens.toLocaleString()}\n`;
627
+ response += `Total Tokens: ${totalTokens.toLocaleString()}\n`;
628
+ if (totalAudioSeconds > 0) {
629
+ response += `Audio Processed: ${totalAudioSeconds.toFixed(1)}s\n`;
630
+ }
631
+ if (totalCost != null) {
632
+ response += `Estimated Cost: $${totalCost.toFixed(4)}\n`;
633
+ }
634
+ response += '\n';
585
635
  if (groupedStats.length > 0) {
586
- response += '*Breakdown by Provider and Model:*\n';
636
+ response += '*By Provider/Model:*\n';
587
637
  for (const stat of groupedStats) {
588
- response += `- ${stat.provider}/${stat.model}:\n ${stat.totalTokens} tokens\n(${stat.messageCount} messages)\n\n`;
638
+ response += `\n*${stat.provider}/${stat.model}*\n`;
639
+ response += ` Tokens: ${stat.totalTokens.toLocaleString()} (${stat.messageCount} msgs)\n`;
640
+ if (stat.totalAudioSeconds > 0) {
641
+ response += ` Audio: ${stat.totalAudioSeconds.toFixed(1)}s\n`;
642
+ }
643
+ if (stat.estimatedCostUsd != null) {
644
+ response += ` Cost: $${stat.estimatedCostUsd.toFixed(4)}\n`;
645
+ }
589
646
  }
590
647
  }
591
648
  else {
592
649
  response += 'No detailed usage statistics available.';
593
650
  }
594
651
  await ctx.reply(response, { parse_mode: 'Markdown' });
595
- // Fechar conexão com o banco de dados
596
652
  history.close();
597
653
  }
598
654
  catch (error) {
@@ -632,11 +688,39 @@ How can I assist you today?`;
632
688
  response += `*Agent:*\n`;
633
689
  response += `- Name: ${config.agent.name}\n`;
634
690
  response += `- Personality: ${config.agent.personality}\n\n`;
635
- response += `*LLM:*\n`;
691
+ response += `*Oracle (LLM):*\n`;
636
692
  response += `- Provider: ${config.llm.provider}\n`;
637
693
  response += `- Model: ${config.llm.model}\n`;
638
694
  response += `- Temperature: ${config.llm.temperature}\n`;
639
695
  response += `- Context Window: ${config.llm.context_window || 100}\n\n`;
696
+ // Sati config (falls back to llm if not set)
697
+ const sati = config.sati;
698
+ response += `*Sati (Memory):*\n`;
699
+ if (sati?.provider) {
700
+ response += `- Provider: ${sati.provider}\n`;
701
+ response += `- Model: ${sati.model || config.llm.model}\n`;
702
+ response += `- Temperature: ${sati.temperature ?? config.llm.temperature}\n`;
703
+ response += `- Memory Limit: ${sati.memory_limit ?? 1000}\n`;
704
+ }
705
+ else {
706
+ response += `- Inherits Oracle config\n`;
707
+ }
708
+ response += '\n';
709
+ // Apoc config (falls back to llm if not set)
710
+ const apoc = config.apoc;
711
+ response += `*Apoc (DevTools):*\n`;
712
+ if (apoc?.provider) {
713
+ response += `- Provider: ${apoc.provider}\n`;
714
+ response += `- Model: ${apoc.model || config.llm.model}\n`;
715
+ response += `- Temperature: ${apoc.temperature ?? 0.2}\n`;
716
+ if (apoc.working_dir)
717
+ response += `- Working Dir: ${apoc.working_dir}\n`;
718
+ response += `- Timeout: ${apoc.timeout_ms ?? 30000}ms\n`;
719
+ }
720
+ else {
721
+ response += `- Inherits Oracle config\n`;
722
+ }
723
+ response += '\n';
640
724
  response += `*Channels:*\n`;
641
725
  response += `- Telegram Enabled: ${config.channels.telegram.enabled}\n`;
642
726
  response += `- Discord Enabled: ${config.channels.discord.enabled}\n\n`;
@@ -1,22 +1,22 @@
1
1
  import { tool } from "langchain";
2
2
  import * as z from "zod";
3
- import path from "path";
4
3
  import Database from "better-sqlite3";
5
4
  import { homedir } from "os";
6
- // Tool for querying message counts from the database
5
+ import path from "path";
7
6
  const dbPath = path.join(homedir(), ".morpheus", "memory", "short-memory.db");
7
+ // Tool for querying message counts from the database
8
8
  export const MessageCountTool = tool(async ({ timeRange }) => {
9
9
  try {
10
- // Connect to database
11
10
  const db = new Database(dbPath);
11
+ // The messages table uses `created_at` (Unix ms integer), not `timestamp`
12
12
  let query = "SELECT COUNT(*) as count FROM messages";
13
13
  const params = [];
14
14
  if (timeRange) {
15
- query += " WHERE timestamp BETWEEN ? AND ?";
16
- params.push(timeRange.start);
17
- params.push(timeRange.end);
15
+ query += " WHERE created_at BETWEEN ? AND ?";
16
+ params.push(new Date(timeRange.start).getTime());
17
+ params.push(new Date(timeRange.end).getTime());
18
18
  }
19
- const result = db.prepare(query).get(params);
19
+ const result = db.prepare(query).get(...params);
20
20
  db.close();
21
21
  return JSON.stringify(result.count);
22
22
  }
@@ -26,11 +26,11 @@ export const MessageCountTool = tool(async ({ timeRange }) => {
26
26
  }
27
27
  }, {
28
28
  name: "message_count",
29
- description: "Returns count of stored messages. Accepts an optional 'timeRange' parameter with start and end timestamps for filtering.",
29
+ description: "Returns count of stored messages. Accepts an optional 'timeRange' parameter with ISO date strings (start/end) for filtering.",
30
30
  schema: z.object({
31
31
  timeRange: z.object({
32
- start: z.string().regex(/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(\.\d{3})?Z?$/, "ISO date string"),
33
- end: z.string().regex(/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(\.\d{3})?Z?$/, "ISO date string"),
32
+ start: z.string().describe("ISO date string, e.g. 2026-01-01T00:00:00Z"),
33
+ end: z.string().describe("ISO date string, e.g. 2026-12-31T23:59:59Z"),
34
34
  }).optional(),
35
35
  }),
36
36
  });
@@ -39,20 +39,44 @@ export const ProviderModelUsageTool = tool(async () => {
39
39
  try {
40
40
  const db = new Database(dbPath);
41
41
  const query = `
42
- SELECT
43
- provider,
44
- COALESCE(model, 'unknown') as model,
45
- SUM(input_tokens) as totalInputTokens,
46
- SUM(output_tokens) as totalOutputTokens,
47
- SUM(total_tokens) as totalTokens,
48
- COUNT(*) as messageCount
49
- FROM messages
50
- WHERE provider IS NOT NULL
51
- GROUP BY provider, COALESCE(model, 'unknown')
52
- ORDER BY provider, model
42
+ SELECT
43
+ m.provider,
44
+ COALESCE(m.model, 'unknown') as model,
45
+ SUM(m.input_tokens) as totalInputTokens,
46
+ SUM(m.output_tokens) as totalOutputTokens,
47
+ SUM(m.total_tokens) as totalTokens,
48
+ COUNT(*) as messageCount,
49
+ COALESCE(SUM(m.audio_duration_seconds), 0) as totalAudioSeconds,
50
+ p.input_price_per_1m,
51
+ p.output_price_per_1m
52
+ FROM messages m
53
+ LEFT JOIN model_pricing p ON p.provider = m.provider AND p.model = COALESCE(m.model, 'unknown')
54
+ WHERE m.provider IS NOT NULL
55
+ GROUP BY m.provider, COALESCE(m.model, 'unknown')
56
+ ORDER BY m.provider, m.model
53
57
  `;
54
- const results = db.prepare(query).all();
58
+ const rows = db.prepare(query).all();
55
59
  db.close();
60
+ const results = rows.map(row => {
61
+ const inputTokens = row.totalInputTokens || 0;
62
+ const outputTokens = row.totalOutputTokens || 0;
63
+ let estimatedCostUsd = null;
64
+ if (row.input_price_per_1m != null && row.output_price_per_1m != null) {
65
+ estimatedCostUsd =
66
+ (inputTokens / 1_000_000) * row.input_price_per_1m +
67
+ (outputTokens / 1_000_000) * row.output_price_per_1m;
68
+ }
69
+ return {
70
+ provider: row.provider,
71
+ model: row.model,
72
+ totalInputTokens: inputTokens,
73
+ totalOutputTokens: outputTokens,
74
+ totalTokens: row.totalTokens || 0,
75
+ messageCount: row.messageCount || 0,
76
+ totalAudioSeconds: row.totalAudioSeconds || 0,
77
+ estimatedCostUsd,
78
+ };
79
+ });
56
80
  return JSON.stringify(results);
57
81
  }
58
82
  catch (error) {
@@ -61,31 +85,43 @@ export const ProviderModelUsageTool = tool(async () => {
61
85
  }
62
86
  }, {
63
87
  name: "provider_model_usage",
64
- description: "Returns token usage statistics grouped by provider and model.",
88
+ description: "Returns token usage statistics grouped by provider and model, including audio duration and estimated cost in USD (when pricing is configured).",
65
89
  schema: z.object({}),
66
90
  });
67
- // Tool for querying token usage statistics from the database
91
+ // Tool for querying global token usage statistics from the database
68
92
  export const TokenUsageTool = tool(async ({ timeRange }) => {
69
93
  try {
70
- // Connect to database
71
94
  const db = new Database(dbPath);
72
- let query = "SELECT SUM(input_tokens) as inputTokens, SUM(output_tokens) as outputTokens, SUM(input_tokens + output_tokens) as totalTokens FROM messages";
95
+ // The messages table uses `created_at` (Unix ms integer), not `timestamp`
96
+ let whereClause = "";
73
97
  const params = [];
74
98
  if (timeRange) {
75
- query += " WHERE timestamp BETWEEN ? AND ?";
76
- params.push(timeRange.start);
77
- params.push(timeRange.end);
99
+ whereClause = " WHERE created_at BETWEEN ? AND ?";
100
+ params.push(new Date(timeRange.start).getTime());
101
+ params.push(new Date(timeRange.end).getTime());
78
102
  }
79
- const result = db.prepare(query).get(params);
103
+ const row = db.prepare(`SELECT
104
+ SUM(input_tokens) as inputTokens,
105
+ SUM(output_tokens) as outputTokens,
106
+ SUM(total_tokens) as totalTokens,
107
+ COALESCE(SUM(audio_duration_seconds), 0) as totalAudioSeconds
108
+ FROM messages${whereClause}`).get(...params);
109
+ // Estimated cost via model_pricing join
110
+ const costRow = db.prepare(`SELECT
111
+ SUM((COALESCE(m.input_tokens, 0) / 1000000.0) * p.input_price_per_1m
112
+ + (COALESCE(m.output_tokens, 0) / 1000000.0) * p.output_price_per_1m) as totalCost
113
+ FROM messages m
114
+ INNER JOIN model_pricing p ON p.provider = m.provider AND p.model = COALESCE(m.model, 'unknown')
115
+ WHERE m.provider IS NOT NULL${whereClause ? whereClause.replace("WHERE", "AND") : ""}`).get(...params);
80
116
  db.close();
81
- // Handle potential null values
82
- const tokenStats = {
83
- totalTokens: result.totalTokens || 0,
84
- inputTokens: result.inputTokens || 0,
85
- outputTokens: result.outputTokens || 0,
86
- timestamp: new Date().toISOString()
87
- };
88
- return JSON.stringify(tokenStats);
117
+ return JSON.stringify({
118
+ inputTokens: row.inputTokens || 0,
119
+ outputTokens: row.outputTokens || 0,
120
+ totalTokens: row.totalTokens || 0,
121
+ totalAudioSeconds: row.totalAudioSeconds || 0,
122
+ estimatedCostUsd: costRow.totalCost ?? null,
123
+ timestamp: new Date().toISOString(),
124
+ });
89
125
  }
90
126
  catch (error) {
91
127
  console.error("Error in TokenUsageTool:", error);
@@ -93,11 +129,11 @@ export const TokenUsageTool = tool(async ({ timeRange }) => {
93
129
  }
94
130
  }, {
95
131
  name: "token_usage",
96
- description: "Returns token usage statistics. Accepts an optional 'timeRange' parameter with start and end timestamps for filtering.",
132
+ description: "Returns global token usage statistics including input/output tokens, total tokens, audio duration in seconds, and estimated cost in USD (when pricing is configured). Accepts an optional 'timeRange' parameter with ISO date strings for filtering.",
97
133
  schema: z.object({
98
134
  timeRange: z.object({
99
- start: z.string().regex(/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(\.\d{3})?Z?$/, "ISO date string"),
100
- end: z.string().regex(/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(\.\d{3})?Z?$/, "ISO date string"),
135
+ start: z.string().describe("ISO date string, e.g. 2026-01-01T00:00:00Z"),
136
+ end: z.string().describe("ISO date string, e.g. 2026-12-31T23:59:59Z"),
101
137
  }).optional(),
102
138
  }),
103
139
  });
@@ -9,30 +9,38 @@ export const DiagnosticTool = tool(async () => {
9
9
  try {
10
10
  const timestamp = new Date().toISOString();
11
11
  const components = {};
12
- // Check configuration
12
+ const morpheusRoot = path.join(homedir(), ".morpheus");
13
+ // ── Configuration ──────────────────────────────────────────────
13
14
  try {
14
15
  const configManager = ConfigManager.getInstance();
15
16
  await configManager.load();
16
17
  const config = configManager.get();
17
- // Basic validation - check if required fields exist
18
- const requiredFields = ['llm', 'logging', 'ui'];
18
+ const requiredFields = ["llm", "logging", "ui"];
19
19
  const missingFields = requiredFields.filter(field => !(field in config));
20
20
  if (missingFields.length === 0) {
21
+ const sati = config.sati;
22
+ const apoc = config.apoc;
21
23
  components.config = {
22
24
  status: "healthy",
23
25
  message: "Configuration is valid and complete",
24
26
  details: {
25
- llmProvider: config.llm?.provider,
27
+ oracleProvider: config.llm?.provider,
28
+ oracleModel: config.llm?.model,
29
+ satiProvider: sati?.provider ?? `${config.llm?.provider} (inherited)`,
30
+ satiModel: sati?.model ?? `${config.llm?.model} (inherited)`,
31
+ apocProvider: apoc?.provider ?? `${config.llm?.provider} (inherited)`,
32
+ apocModel: apoc?.model ?? `${config.llm?.model} (inherited)`,
33
+ apocWorkingDir: apoc?.working_dir ?? "not set",
26
34
  uiEnabled: config.ui?.enabled,
27
- uiPort: config.ui?.port
28
- }
35
+ uiPort: config.ui?.port,
36
+ },
29
37
  };
30
38
  }
31
39
  else {
32
40
  components.config = {
33
41
  status: "warning",
34
- message: `Missing required configuration fields: ${missingFields.join(', ')}`,
35
- details: { missingFields }
42
+ message: `Missing required configuration fields: ${missingFields.join(", ")}`,
43
+ details: { missingFields },
36
44
  };
37
45
  }
38
46
  }
@@ -40,45 +48,62 @@ export const DiagnosticTool = tool(async () => {
40
48
  components.config = {
41
49
  status: "error",
42
50
  message: `Configuration error: ${error.message}`,
43
- details: {}
51
+ details: {},
44
52
  };
45
53
  }
46
- // Check storage/database
54
+ // ── Short-term memory DB ────────────────────────────────────────
47
55
  try {
48
- // For now, we'll check if the data directory exists
49
- const dbPath = path.join(homedir(), ".morpheus", "memory", "short-memory.db");
56
+ const dbPath = path.join(morpheusRoot, "memory", "short-memory.db");
50
57
  await fsPromises.access(dbPath);
51
- components.storage = {
58
+ const stat = await fsPromises.stat(dbPath);
59
+ components.shortMemoryDb = {
52
60
  status: "healthy",
53
- message: "Database file is accessible",
54
- details: { path: dbPath }
61
+ message: "Short-memory database is accessible",
62
+ details: { path: dbPath, sizeBytes: stat.size },
55
63
  };
56
64
  }
57
65
  catch (error) {
58
- components.storage = {
66
+ components.shortMemoryDb = {
59
67
  status: "error",
60
- message: `Storage error: ${error.message}`,
61
- details: {}
68
+ message: `Short-memory DB not accessible: ${error.message}`,
69
+ details: {},
62
70
  };
63
71
  }
64
- // Check network connectivity (basic check)
72
+ // ── Sati long-term memory DB ────────────────────────────────────
73
+ try {
74
+ const satiDbPath = path.join(morpheusRoot, "memory", "sati-memory.db");
75
+ await fsPromises.access(satiDbPath);
76
+ const stat = await fsPromises.stat(satiDbPath);
77
+ components.satiMemoryDb = {
78
+ status: "healthy",
79
+ message: "Sati memory database is accessible",
80
+ details: { path: satiDbPath, sizeBytes: stat.size },
81
+ };
82
+ }
83
+ catch {
84
+ // Sati DB may not exist yet if no memories have been stored — treat as warning
85
+ components.satiMemoryDb = {
86
+ status: "warning",
87
+ message: "Sati memory database does not exist yet (no memories stored yet)",
88
+ details: {},
89
+ };
90
+ }
91
+ // ── LLM provider configured ─────────────────────────────────────
65
92
  try {
66
- // For now, we'll just check if we can reach the LLM provider configuration
67
93
  const configManager = ConfigManager.getInstance();
68
- await configManager.load();
69
94
  const config = configManager.get();
70
- if (config.llm && config.llm.provider) {
95
+ if (config.llm?.provider) {
71
96
  components.network = {
72
97
  status: "healthy",
73
- message: `LLM provider configured: ${config.llm.provider}`,
74
- details: { provider: config.llm.provider }
98
+ message: `Oracle LLM provider configured: ${config.llm.provider}`,
99
+ details: { provider: config.llm.provider, model: config.llm.model },
75
100
  };
76
101
  }
77
102
  else {
78
103
  components.network = {
79
104
  status: "warning",
80
- message: "No LLM provider configured",
81
- details: {}
105
+ message: "No Oracle LLM provider configured",
106
+ details: {},
82
107
  };
83
108
  }
84
109
  }
@@ -86,39 +111,43 @@ export const DiagnosticTool = tool(async () => {
86
111
  components.network = {
87
112
  status: "error",
88
113
  message: `Network check error: ${error.message}`,
89
- details: {}
114
+ details: {},
90
115
  };
91
116
  }
92
- // Check if the agent is running
117
+ // ── Agent process ───────────────────────────────────────────────
118
+ components.agent = {
119
+ status: "healthy",
120
+ message: "Agent is running (this tool is executing inside the agent process)",
121
+ details: { pid: process.pid, uptime: `${Math.floor(process.uptime())}s` },
122
+ };
123
+ // ── Logs directory ──────────────────────────────────────────────
93
124
  try {
94
- // This is a basic check - in a real implementation, we might check if the agent process is running
95
- components.agent = {
125
+ const logsDir = path.join(morpheusRoot, "logs");
126
+ await fsPromises.access(logsDir);
127
+ components.logs = {
96
128
  status: "healthy",
97
- message: "Agent is running",
98
- details: { uptime: "N/A - runtime information not available in this context" }
129
+ message: "Logs directory is accessible",
130
+ details: { path: logsDir },
99
131
  };
100
132
  }
101
- catch (error) {
102
- components.agent = {
103
- status: "error",
104
- message: `Agent check error: ${error.message}`,
105
- details: {}
133
+ catch {
134
+ components.logs = {
135
+ status: "warning",
136
+ message: "Logs directory not found (will be created on first log write)",
137
+ details: {},
106
138
  };
107
139
  }
108
- return JSON.stringify({
109
- timestamp,
110
- components
111
- });
140
+ return JSON.stringify({ timestamp, components });
112
141
  }
113
142
  catch (error) {
114
143
  console.error("Error in DiagnosticTool:", error);
115
144
  return JSON.stringify({
116
145
  timestamp: new Date().toISOString(),
117
- error: "Failed to run diagnostics"
146
+ error: "Failed to run diagnostics",
118
147
  });
119
148
  }
120
149
  }, {
121
150
  name: "diagnostic_check",
122
- description: "Performs system health diagnostics and returns a comprehensive report on system components.",
151
+ description: "Performs system health diagnostics and returns a comprehensive report covering configuration (Oracle/Sati/Apoc), short-memory DB, Sati long-term memory DB, LLM provider, agent process, and logs directory.",
123
152
  schema: z.object({}),
124
153
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "morpheus-cli",
3
- "version": "0.4.3",
3
+ "version": "0.4.4",
4
4
  "description": "Morpheus is a local AI agent for developers, running as a CLI daemon that connects to LLMs, local tools, and MCPs, enabling interaction via Terminal, Telegram, and Discord. Inspired by the character Morpheus from *The Matrix*, the project acts as an intelligent orchestrator, bridging the gap between the developer and complex systems.",
5
5
  "bin": {
6
6
  "morpheus": "./bin/morpheus.js"