morpheus-cli 0.4.2 → 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
|
|
531
|
+
response += '✅ Node.js: ' + nodeVersion + '\n';
|
|
533
532
|
}
|
|
534
533
|
else {
|
|
535
|
-
response += '❌ Node.js
|
|
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
|
-
//
|
|
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
|
-
|
|
544
|
-
|
|
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 += `❌
|
|
559
|
+
response += `❌ Oracle API key missing (${llmProvider})\n`;
|
|
553
560
|
}
|
|
554
561
|
}
|
|
555
|
-
//
|
|
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
|
|
588
|
+
response += '✅ Telegram token\n';
|
|
560
589
|
}
|
|
561
590
|
else {
|
|
562
|
-
response += '❌ Telegram
|
|
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,
|
|
577
|
-
limit: 100,
|
|
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 += `
|
|
583
|
-
response += `
|
|
584
|
-
response += `Total Tokens: ${
|
|
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 += '*
|
|
636
|
+
response += '*By Provider/Model:*\n';
|
|
587
637
|
for (const stat of groupedStats) {
|
|
588
|
-
response +=
|
|
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`;
|
|
@@ -4,7 +4,7 @@ import fs from 'fs-extra';
|
|
|
4
4
|
import { confirm } from '@inquirer/prompts';
|
|
5
5
|
import { scaffold } from '../../runtime/scaffold.js';
|
|
6
6
|
import { DisplayManager } from '../../runtime/display.js';
|
|
7
|
-
import { writePid, readPid, isProcessRunning, clearPid, checkStalePid, killProcess } from '../../runtime/lifecycle.js';
|
|
7
|
+
import { writePid, readPid, isProcessRunning, clearPid, checkStalePid, killProcess, waitForProcessDeath } from '../../runtime/lifecycle.js';
|
|
8
8
|
import { ConfigManager } from '../../config/manager.js';
|
|
9
9
|
import { renderBanner } from '../utils/render.js';
|
|
10
10
|
import { TelegramAdapter } from '../../channels/telegram.js';
|
|
@@ -28,7 +28,8 @@ export const startCommand = new Command('start')
|
|
|
28
28
|
// Cleanup stale PID first
|
|
29
29
|
await checkStalePid();
|
|
30
30
|
const existingPid = await readPid();
|
|
31
|
-
if (
|
|
31
|
+
// Guard: skip if the stored PID is our own (container restart PID reuse scenario)
|
|
32
|
+
if (existingPid !== null && existingPid !== process.pid && isProcessRunning(existingPid)) {
|
|
32
33
|
display.log(chalk.yellow(`Morpheus is already running (PID: ${existingPid})`));
|
|
33
34
|
let shouldKill = options.yes;
|
|
34
35
|
if (!shouldKill) {
|
|
@@ -48,10 +49,13 @@ export const startCommand = new Command('start')
|
|
|
48
49
|
display.log(chalk.cyan(`Stopping existing process (PID: ${existingPid})...`));
|
|
49
50
|
const killed = killProcess(existingPid);
|
|
50
51
|
if (killed) {
|
|
51
|
-
display.log(chalk.green('
|
|
52
|
+
display.log(chalk.green('Terminated'));
|
|
52
53
|
await clearPid();
|
|
53
|
-
//
|
|
54
|
-
|
|
54
|
+
// Wait up to 5 s for the process to actually die before continuing
|
|
55
|
+
const died = await waitForProcessDeath(existingPid, 5000);
|
|
56
|
+
if (!died) {
|
|
57
|
+
display.log(chalk.yellow('Warning: process may still be running. Proceeding anyway.'));
|
|
58
|
+
}
|
|
55
59
|
}
|
|
56
60
|
else {
|
|
57
61
|
display.log(chalk.red('Failed to stop the process'));
|
|
@@ -31,15 +31,37 @@ export function isProcessRunning(pid) {
|
|
|
31
31
|
return e.code === 'EPERM'; // If EPERM, it exists but we lack permission (still running)
|
|
32
32
|
}
|
|
33
33
|
}
|
|
34
|
+
/**
|
|
35
|
+
* Waits until the given PID is no longer running, or until timeout is reached.
|
|
36
|
+
* Returns true if the process died, false if timeout was reached.
|
|
37
|
+
*/
|
|
38
|
+
export async function waitForProcessDeath(pid, timeoutMs = 5000, intervalMs = 200) {
|
|
39
|
+
const deadline = Date.now() + timeoutMs;
|
|
40
|
+
while (Date.now() < deadline) {
|
|
41
|
+
if (!isProcessRunning(pid))
|
|
42
|
+
return true;
|
|
43
|
+
await new Promise(resolve => setTimeout(resolve, intervalMs));
|
|
44
|
+
}
|
|
45
|
+
return !isProcessRunning(pid);
|
|
46
|
+
}
|
|
34
47
|
export async function checkStalePid() {
|
|
35
48
|
const pid = await readPid();
|
|
36
49
|
if (pid !== null) {
|
|
50
|
+
// Never treat our own PID as stale — this avoids self-kill loops in containers
|
|
51
|
+
// where a restarted process may inherit the same PID that was previously written.
|
|
52
|
+
if (pid === process.pid) {
|
|
53
|
+
await clearPid();
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
37
56
|
if (!isProcessRunning(pid)) {
|
|
38
57
|
await clearPid();
|
|
39
58
|
}
|
|
40
59
|
}
|
|
41
60
|
}
|
|
42
61
|
export function killProcess(pid) {
|
|
62
|
+
// Safety guard: never kill ourselves
|
|
63
|
+
if (pid === process.pid)
|
|
64
|
+
return false;
|
|
43
65
|
try {
|
|
44
66
|
process.kill(pid, 'SIGTERM');
|
|
45
67
|
return true;
|
|
@@ -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
|
-
|
|
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
|
|
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
|
|
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().
|
|
33
|
-
end: z.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
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
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
|
|
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
|
-
|
|
95
|
+
// The messages table uses `created_at` (Unix ms integer), not `timestamp`
|
|
96
|
+
let whereClause = "";
|
|
73
97
|
const params = [];
|
|
74
98
|
if (timeRange) {
|
|
75
|
-
|
|
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
|
|
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
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
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
|
|
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().
|
|
100
|
-
end: z.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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
54
|
+
// ── Short-term memory DB ────────────────────────────────────────
|
|
47
55
|
try {
|
|
48
|
-
|
|
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
|
-
|
|
58
|
+
const stat = await fsPromises.stat(dbPath);
|
|
59
|
+
components.shortMemoryDb = {
|
|
52
60
|
status: "healthy",
|
|
53
|
-
message: "
|
|
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.
|
|
66
|
+
components.shortMemoryDb = {
|
|
59
67
|
status: "error",
|
|
60
|
-
message: `
|
|
61
|
-
details: {}
|
|
68
|
+
message: `Short-memory DB not accessible: ${error.message}`,
|
|
69
|
+
details: {},
|
|
62
70
|
};
|
|
63
71
|
}
|
|
64
|
-
//
|
|
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
|
|
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
|
-
//
|
|
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
|
-
|
|
95
|
-
|
|
125
|
+
const logsDir = path.join(morpheusRoot, "logs");
|
|
126
|
+
await fsPromises.access(logsDir);
|
|
127
|
+
components.logs = {
|
|
96
128
|
status: "healthy",
|
|
97
|
-
message: "
|
|
98
|
-
details: {
|
|
129
|
+
message: "Logs directory is accessible",
|
|
130
|
+
details: { path: logsDir },
|
|
99
131
|
};
|
|
100
132
|
}
|
|
101
|
-
catch
|
|
102
|
-
components.
|
|
103
|
-
status: "
|
|
104
|
-
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
|
|
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
|
+
"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"
|