morpheus-cli 0.1.5 → 0.1.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (36) hide show
  1. package/README.md +266 -213
  2. package/bin/morpheus.js +30 -0
  3. package/dist/channels/telegram.js +2 -2
  4. package/dist/cli/commands/config.js +18 -3
  5. package/dist/cli/commands/init.js +3 -3
  6. package/dist/cli/index.js +4 -0
  7. package/dist/config/mcp-loader.js +42 -0
  8. package/dist/config/schemas.js +13 -0
  9. package/dist/http/__tests__/auth.test.js +53 -0
  10. package/dist/http/api.js +12 -0
  11. package/dist/http/middleware/auth.js +23 -0
  12. package/dist/http/server.js +2 -1
  13. package/dist/runtime/__tests__/manual_start_verify.js +1 -0
  14. package/dist/runtime/agent.js +79 -7
  15. package/dist/runtime/audio-agent.js +11 -1
  16. package/dist/runtime/display.js +12 -0
  17. package/dist/runtime/memory/sqlite.js +139 -9
  18. package/dist/runtime/providers/factory.js +27 -2
  19. package/dist/runtime/scaffold.js +5 -0
  20. package/dist/runtime/tools/__tests__/tools.test.js +127 -0
  21. package/dist/runtime/tools/analytics-tools.js +73 -0
  22. package/dist/runtime/tools/config-tools.js +70 -0
  23. package/dist/runtime/tools/diagnostic-tools.js +124 -0
  24. package/dist/runtime/tools/factory.js +55 -11
  25. package/dist/runtime/tools/index.js +4 -0
  26. package/dist/types/auth.js +4 -0
  27. package/dist/types/config.js +1 -0
  28. package/dist/types/mcp.js +11 -0
  29. package/dist/types/tools.js +2 -0
  30. package/dist/types/usage.js +1 -0
  31. package/dist/ui/assets/index-4kQpg2wK.js +50 -0
  32. package/dist/ui/assets/index-CwL7mn36.css +1 -0
  33. package/dist/ui/index.html +2 -2
  34. package/package.json +2 -1
  35. package/dist/ui/assets/index-D1kvj0eG.css +0 -1
  36. package/dist/ui/assets/index-DTh8waF7.js +0 -50
@@ -0,0 +1,127 @@
1
+ import { describe, it, expect, vi } from 'vitest';
2
+ import { ConfigQueryTool, ConfigUpdateTool } from '../config-tools.js';
3
+ import { DiagnosticTool } from '../diagnostic-tools.js';
4
+ import { MessageCountTool, TokenUsageTool } from '../analytics-tools.js';
5
+ import { ConfigManager } from '../../../config/manager.js';
6
+ // Mock the ConfigManager for testing
7
+ vi.mock('../../config/manager.js', () => ({
8
+ ConfigManager: {
9
+ getInstance: vi.fn(() => ({
10
+ load: vi.fn(async () => ({
11
+ llm: { provider: 'openai', model: 'gpt-4' },
12
+ logging: { enabled: true, level: 'info' },
13
+ ui: { enabled: true, port: 3000 }
14
+ })),
15
+ get: vi.fn(() => ({
16
+ llm: { provider: 'openai', model: 'gpt-4' },
17
+ logging: { enabled: true, level: 'info' },
18
+ ui: { enabled: true, port: 3000 }
19
+ })),
20
+ save: vi.fn(async (newConfig) => {
21
+ // Mock save implementation
22
+ })
23
+ }))
24
+ }
25
+ }));
26
+ describe('Config Tools', () => {
27
+ describe('ConfigQueryTool', () => {
28
+ it('should query all configuration values when no key is provided', async () => {
29
+ const result = await ConfigQueryTool.invoke({});
30
+ const parsedResult = JSON.parse(result);
31
+ expect(parsedResult).toHaveProperty('llm');
32
+ expect(parsedResult).toHaveProperty('logging');
33
+ expect(parsedResult).toHaveProperty('ui');
34
+ });
35
+ it('should query specific configuration value when key is provided', async () => {
36
+ const result = await ConfigQueryTool.invoke({ key: 'llm' });
37
+ const parsedResult = JSON.parse(result);
38
+ expect(parsedResult).toHaveProperty('llm');
39
+ const llmConfig = parsedResult.llm;
40
+ expect(llmConfig).toHaveProperty('provider');
41
+ expect(llmConfig).toHaveProperty('model');
42
+ });
43
+ });
44
+ describe('ConfigUpdateTool', () => {
45
+ it('should update configuration values', async () => {
46
+ const updates = { 'ui.port': 4000 };
47
+ const result = await ConfigUpdateTool.invoke({ updates });
48
+ const parsedResult = JSON.parse(result);
49
+ expect(parsedResult).toHaveProperty('success', true);
50
+ });
51
+ it('should return error when update fails', async () => {
52
+ // Create a new mock instance for this specific test
53
+ const originalGetInstance = ConfigManager.getInstance;
54
+ const mockGetInstance = vi.fn(() => ({
55
+ load: vi.fn(async () => ({
56
+ llm: { provider: 'openai', model: 'gpt-4' },
57
+ logging: { enabled: true, level: 'info' },
58
+ ui: { enabled: true, port: 3000 }
59
+ })),
60
+ get: vi.fn(() => ({
61
+ llm: { provider: 'openai', model: 'gpt-4' },
62
+ logging: { enabled: true, level: 'info' },
63
+ ui: { enabled: true, port: 3000 }
64
+ })),
65
+ save: vi.fn(async () => {
66
+ throw new Error('Save failed');
67
+ })
68
+ }));
69
+ // Replace the getInstance method temporarily
70
+ ConfigManager.getInstance = mockGetInstance;
71
+ const updates = { 'invalid.field': 'value' };
72
+ const result = await ConfigUpdateTool.invoke({ updates });
73
+ const parsedResult = JSON.parse(result);
74
+ expect(parsedResult).toHaveProperty('error');
75
+ // Restore the original method
76
+ ConfigManager.getInstance = originalGetInstance;
77
+ });
78
+ });
79
+ });
80
+ describe('DiagnosticTool', () => {
81
+ it('should return a diagnostic report with component statuses', async () => {
82
+ const result = await DiagnosticTool.invoke({});
83
+ const parsedResult = JSON.parse(result);
84
+ expect(parsedResult).toHaveProperty('timestamp');
85
+ expect(parsedResult).toHaveProperty('components');
86
+ expect(parsedResult.components).toHaveProperty('config');
87
+ expect(parsedResult.components).toHaveProperty('storage');
88
+ expect(parsedResult.components).toHaveProperty('network');
89
+ expect(parsedResult.components).toHaveProperty('agent');
90
+ });
91
+ });
92
+ describe('Analytics Tools', () => {
93
+ // Note: These tools now use the existing SQLite class and access the database
94
+ describe('MessageCountTool', () => {
95
+ it('should return message count', async () => {
96
+ const result = await MessageCountTool.invoke({});
97
+ const parsedResult = JSON.parse(result);
98
+ // Should return a number (message count) or an error if database doesn't exist
99
+ if (typeof parsedResult === 'object' && parsedResult.error) {
100
+ // If there's an error (e.g., database doesn't exist), that's acceptable in test environment
101
+ expect(parsedResult).toHaveProperty('error');
102
+ }
103
+ else {
104
+ // Otherwise, should return a number
105
+ expect(typeof parsedResult).toBe('number');
106
+ }
107
+ });
108
+ });
109
+ describe('TokenUsageTool', () => {
110
+ it('should return token usage statistics', async () => {
111
+ const result = await TokenUsageTool.invoke({});
112
+ const parsedResult = JSON.parse(result);
113
+ // Should return token statistics or an error if database doesn't exist
114
+ if (typeof parsedResult === 'object' && parsedResult.error) {
115
+ // If there's an error (e.g., database doesn't exist), that's acceptable in test environment
116
+ expect(parsedResult).toHaveProperty('error');
117
+ }
118
+ else {
119
+ // Otherwise, should return token statistics
120
+ expect(parsedResult).toHaveProperty('totalTokens');
121
+ expect(parsedResult).toHaveProperty('inputTokens');
122
+ expect(parsedResult).toHaveProperty('outputTokens');
123
+ expect(parsedResult).toHaveProperty('timestamp');
124
+ }
125
+ });
126
+ });
127
+ });
@@ -0,0 +1,73 @@
1
+ import { tool } from "langchain";
2
+ import * as z from "zod";
3
+ import path from "path";
4
+ import Database from "better-sqlite3";
5
+ import { homedir } from "os";
6
+ // Tool for querying message counts from the database
7
+ const dbPath = path.join(homedir(), ".morpheus", "memory", "short-memory.db");
8
+ export const MessageCountTool = tool(async ({ timeRange }) => {
9
+ try {
10
+ // Connect to database
11
+ const db = new Database(dbPath);
12
+ let query = "SELECT COUNT(*) as count FROM messages";
13
+ const params = [];
14
+ if (timeRange) {
15
+ query += " WHERE timestamp BETWEEN ? AND ?";
16
+ params.push(timeRange.start);
17
+ params.push(timeRange.end);
18
+ }
19
+ const result = db.prepare(query).get(params);
20
+ db.close();
21
+ return JSON.stringify(result.count);
22
+ }
23
+ catch (error) {
24
+ console.error("Error in MessageCountTool:", error);
25
+ return JSON.stringify({ error: `Failed to count messages: ${error.message}` });
26
+ }
27
+ }, {
28
+ name: "message_count",
29
+ description: "Returns count of stored messages. Accepts an optional 'timeRange' parameter with start and end timestamps for filtering.",
30
+ schema: z.object({
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"),
34
+ }).optional(),
35
+ }),
36
+ });
37
+ // Tool for querying token usage statistics from the database
38
+ export const TokenUsageTool = tool(async ({ timeRange }) => {
39
+ try {
40
+ // Connect to database
41
+ const db = new Database(dbPath);
42
+ let query = "SELECT SUM(input_tokens) as inputTokens, SUM(output_tokens) as outputTokens, SUM(input_tokens + output_tokens) as totalTokens FROM messages";
43
+ const params = [];
44
+ if (timeRange) {
45
+ query += " WHERE timestamp BETWEEN ? AND ?";
46
+ params.push(timeRange.start);
47
+ params.push(timeRange.end);
48
+ }
49
+ const result = db.prepare(query).get(params);
50
+ db.close();
51
+ // Handle potential null values
52
+ const tokenStats = {
53
+ totalTokens: result.totalTokens || 0,
54
+ inputTokens: result.inputTokens || 0,
55
+ outputTokens: result.outputTokens || 0,
56
+ timestamp: new Date().toISOString()
57
+ };
58
+ return JSON.stringify(tokenStats);
59
+ }
60
+ catch (error) {
61
+ console.error("Error in TokenUsageTool:", error);
62
+ return JSON.stringify({ error: `Failed to get token usage: ${error.message}` });
63
+ }
64
+ }, {
65
+ name: "token_usage",
66
+ description: "Returns token usage statistics. Accepts an optional 'timeRange' parameter with start and end timestamps for filtering.",
67
+ schema: z.object({
68
+ timeRange: z.object({
69
+ start: z.string().regex(/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(\.\d{3})?Z?$/, "ISO date string"),
70
+ end: z.string().regex(/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(\.\d{3})?Z?$/, "ISO date string"),
71
+ }).optional(),
72
+ }),
73
+ });
@@ -0,0 +1,70 @@
1
+ import { tool } from "langchain";
2
+ import * as z from "zod";
3
+ import { ConfigManager } from "../../config/manager.js";
4
+ // Tool for querying configuration values
5
+ export const ConfigQueryTool = tool(async ({ key }) => {
6
+ try {
7
+ const configManager = ConfigManager.getInstance();
8
+ // Load config if not already loaded
9
+ await configManager.load();
10
+ const config = configManager.get();
11
+ if (key) {
12
+ // Return specific configuration value
13
+ const value = config[key];
14
+ return JSON.stringify({ [key]: value });
15
+ }
16
+ else {
17
+ // Return all configuration values
18
+ return JSON.stringify(config);
19
+ }
20
+ }
21
+ catch (error) {
22
+ console.error("Error in ConfigQueryTool:", error);
23
+ return JSON.stringify({ error: "Failed to query configuration" });
24
+ }
25
+ }, {
26
+ name: "config_query",
27
+ description: "Queries current configuration values. Accepts an optional 'key' parameter to get a specific configuration value, or no parameter to get all configuration values.",
28
+ schema: z.object({
29
+ key: z.string().optional(),
30
+ }),
31
+ });
32
+ // Tool for updating configuration values
33
+ export const ConfigUpdateTool = tool(async ({ updates }) => {
34
+ try {
35
+ const configManager = ConfigManager.getInstance();
36
+ // Load current config
37
+ await configManager.load();
38
+ const currentConfig = configManager.get();
39
+ // Create new config with updates
40
+ const newConfig = { ...currentConfig, ...updates };
41
+ // Save the updated config
42
+ await configManager.save(newConfig);
43
+ return JSON.stringify({ success: true, message: "Configuration updated successfully" });
44
+ }
45
+ catch (error) {
46
+ console.error("Error in ConfigUpdateTool:", error);
47
+ return JSON.stringify({ error: `Failed to update configuration: ${error.message}` });
48
+ }
49
+ }, {
50
+ name: "config_update",
51
+ description: "Updates configuration values with validation. Accepts an 'updates' object containing key-value pairs to update.",
52
+ schema: z.object({
53
+ updates: z.object({
54
+ // Define common config properties that might be updated
55
+ // Using optional fields to allow flexible updates
56
+ "llm.provider": z.string().optional(),
57
+ "llm.model": z.string().optional(),
58
+ "llm.temperature": z.number().optional(),
59
+ "llm.api_key": z.string().optional(),
60
+ "ui.enabled": z.boolean().optional(),
61
+ "ui.port": z.number().optional(),
62
+ "logging.enabled": z.boolean().optional(),
63
+ "logging.level": z.enum(['debug', 'info', 'warn', 'error']).optional(),
64
+ "audio.enabled": z.boolean().optional(),
65
+ "audio.provider": z.string().optional(),
66
+ "memory.limit": z.number().optional(),
67
+ // Add more specific fields as needed, or use a catch-all for other properties
68
+ }).passthrough(), // Allow additional properties not explicitly defined
69
+ }),
70
+ });
@@ -0,0 +1,124 @@
1
+ import { tool } from "langchain";
2
+ import * as z from "zod";
3
+ import { ConfigManager } from "../../config/manager.js";
4
+ import { promises as fsPromises } from "fs";
5
+ import path from "path";
6
+ import { homedir } from "os";
7
+ // Tool for performing system diagnostics
8
+ export const DiagnosticTool = tool(async () => {
9
+ try {
10
+ const timestamp = new Date().toISOString();
11
+ const components = {};
12
+ // Check configuration
13
+ try {
14
+ const configManager = ConfigManager.getInstance();
15
+ await configManager.load();
16
+ const config = configManager.get();
17
+ // Basic validation - check if required fields exist
18
+ const requiredFields = ['llm', 'logging', 'ui'];
19
+ const missingFields = requiredFields.filter(field => !(field in config));
20
+ if (missingFields.length === 0) {
21
+ components.config = {
22
+ status: "healthy",
23
+ message: "Configuration is valid and complete",
24
+ details: {
25
+ llmProvider: config.llm?.provider,
26
+ uiEnabled: config.ui?.enabled,
27
+ uiPort: config.ui?.port
28
+ }
29
+ };
30
+ }
31
+ else {
32
+ components.config = {
33
+ status: "warning",
34
+ message: `Missing required configuration fields: ${missingFields.join(', ')}`,
35
+ details: { missingFields }
36
+ };
37
+ }
38
+ }
39
+ catch (error) {
40
+ components.config = {
41
+ status: "error",
42
+ message: `Configuration error: ${error.message}`,
43
+ details: {}
44
+ };
45
+ }
46
+ // Check storage/database
47
+ try {
48
+ // For now, we'll check if the data directory exists
49
+ const dbPath = path.join(homedir(), ".morpheus", "memory", "short-memory.db");
50
+ await fsPromises.access(dbPath);
51
+ components.storage = {
52
+ status: "healthy",
53
+ message: "Database file is accessible",
54
+ details: { path: dbPath }
55
+ };
56
+ }
57
+ catch (error) {
58
+ components.storage = {
59
+ status: "error",
60
+ message: `Storage error: ${error.message}`,
61
+ details: {}
62
+ };
63
+ }
64
+ // Check network connectivity (basic check)
65
+ try {
66
+ // For now, we'll just check if we can reach the LLM provider configuration
67
+ const configManager = ConfigManager.getInstance();
68
+ await configManager.load();
69
+ const config = configManager.get();
70
+ if (config.llm && config.llm.provider) {
71
+ components.network = {
72
+ status: "healthy",
73
+ message: `LLM provider configured: ${config.llm.provider}`,
74
+ details: { provider: config.llm.provider }
75
+ };
76
+ }
77
+ else {
78
+ components.network = {
79
+ status: "warning",
80
+ message: "No LLM provider configured",
81
+ details: {}
82
+ };
83
+ }
84
+ }
85
+ catch (error) {
86
+ components.network = {
87
+ status: "error",
88
+ message: `Network check error: ${error.message}`,
89
+ details: {}
90
+ };
91
+ }
92
+ // Check if the agent is running
93
+ try {
94
+ // This is a basic check - in a real implementation, we might check if the agent process is running
95
+ components.agent = {
96
+ status: "healthy",
97
+ message: "Agent is running",
98
+ details: { uptime: "N/A - runtime information not available in this context" }
99
+ };
100
+ }
101
+ catch (error) {
102
+ components.agent = {
103
+ status: "error",
104
+ message: `Agent check error: ${error.message}`,
105
+ details: {}
106
+ };
107
+ }
108
+ return JSON.stringify({
109
+ timestamp,
110
+ components
111
+ });
112
+ }
113
+ catch (error) {
114
+ console.error("Error in DiagnosticTool:", error);
115
+ return JSON.stringify({
116
+ timestamp: new Date().toISOString(),
117
+ error: "Failed to run diagnostics"
118
+ });
119
+ }
120
+ }, {
121
+ name: "diagnostic_check",
122
+ description: "Performs system health diagnostics and returns a comprehensive report on system components.",
123
+ schema: z.object({}),
124
+ });
@@ -1,33 +1,77 @@
1
1
  import { MultiServerMCPClient } from "@langchain/mcp-adapters";
2
2
  import { DisplayManager } from "../display.js";
3
+ import { loadMCPConfig } from "../../config/mcp-loader.js";
4
+ const display = DisplayManager.getInstance();
5
+ // Fields not supported by Google Gemini API
6
+ const UNSUPPORTED_SCHEMA_FIELDS = ['examples', 'additionalInfo', 'default', '$schema'];
7
+ /**
8
+ * Recursively removes unsupported fields from JSON schema objects.
9
+ * This is needed because some MCP servers (like Coolify) return schemas
10
+ * with fields that Gemini doesn't accept.
11
+ */
12
+ function sanitizeSchema(obj) {
13
+ if (obj === null || typeof obj !== 'object') {
14
+ return obj;
15
+ }
16
+ if (Array.isArray(obj)) {
17
+ return obj.map(sanitizeSchema);
18
+ }
19
+ const sanitized = {};
20
+ for (const [key, value] of Object.entries(obj)) {
21
+ if (!UNSUPPORTED_SCHEMA_FIELDS.includes(key)) {
22
+ sanitized[key] = sanitizeSchema(value);
23
+ }
24
+ }
25
+ return sanitized;
26
+ }
27
+ /**
28
+ * Wraps a tool to sanitize its schema for Gemini compatibility.
29
+ * Creates a proxy that intercepts schema access and sanitizes the output.
30
+ */
31
+ function wrapToolWithSanitizedSchema(tool) {
32
+ display.log('Tool loaded: - ' + tool.name, { source: 'ToolsFactory' });
33
+ // The MCP tools have a schema property that returns JSON Schema
34
+ // We need to intercept and sanitize it
35
+ const originalSchema = tool.schema;
36
+ if (originalSchema && typeof originalSchema === 'object') {
37
+ // Sanitize the schema object directly
38
+ const sanitized = sanitizeSchema(originalSchema);
39
+ tool.schema = sanitized;
40
+ }
41
+ return tool;
42
+ }
3
43
  export class ToolsFactory {
4
44
  static async create() {
5
45
  const display = DisplayManager.getInstance();
46
+ const mcpServers = await loadMCPConfig();
47
+ const serverCount = Object.keys(mcpServers).length;
48
+ if (serverCount === 0) {
49
+ display.log('No MCP servers configured in mcps.json', { level: 'info', source: 'ToolsFactory' });
50
+ return [];
51
+ }
6
52
  const client = new MultiServerMCPClient({
7
- mcpServers: {
8
- coingecko: {
9
- transport: "stdio",
10
- command: "npx",
11
- args: ["-y", "mcp_coingecko_price_ts"],
12
- },
13
- },
53
+ mcpServers: mcpServers,
54
+ onConnectionError: "ignore",
14
55
  // log the MCP client's internal events
15
56
  beforeToolCall: ({ serverName, name, args }) => {
16
- display.log(`MCP Tool Call - Server: ${serverName}, Tool: ${name}, Args: ${JSON.stringify(args)}`);
57
+ display.log(`MCP Tool Call - Server: ${serverName}, Tool: ${name}, Args: ${JSON.stringify(args)}`, { source: 'MCPServer' });
17
58
  return;
18
59
  },
19
60
  // log the results of tool calls
20
61
  afterToolCall: (res) => {
21
- display.log(`MCP Tool Result - ${JSON.stringify(res)}`);
62
+ display.log(`MCP Tool Result - ${JSON.stringify(res)}`, { source: 'MCPServer' });
22
63
  return;
23
64
  }
24
65
  });
25
66
  try {
26
67
  const tools = await client.getTools();
27
- return tools;
68
+ // Sanitize tool schemas to remove fields not supported by Gemini
69
+ const sanitizedTools = tools.map(tool => wrapToolWithSanitizedSchema(tool));
70
+ display.log(`Loaded ${sanitizedTools.length} MCP tools (schemas sanitized for Gemini compatibility)`, { level: 'info', source: 'ToolsFactory' });
71
+ return sanitizedTools;
28
72
  }
29
73
  catch (error) {
30
- display.log(`Failed to initialize MCP tools: ${error}`, { level: 'warning' });
74
+ display.log(`Failed to initialize MCP tools: ${error}`, { level: 'warning', source: 'ToolsFactory' });
31
75
  return []; // Return empty tools on failure to allow agent to start
32
76
  }
33
77
  }
@@ -0,0 +1,4 @@
1
+ // Export all tools from the tools module
2
+ export * from './config-tools.js';
3
+ export * from './diagnostic-tools.js';
4
+ export * from './analytics-tools.js';
@@ -0,0 +1,4 @@
1
+ /**
2
+ * Shared constants and interfaces for HTTP Authentication
3
+ */
4
+ export const AUTH_HEADER = 'x-architect-pass';
@@ -9,6 +9,7 @@ export const DEFAULT_CONFIG = {
9
9
  retention: '14d',
10
10
  },
11
11
  audio: {
12
+ provider: 'google',
12
13
  enabled: true,
13
14
  maxDurationSeconds: 300,
14
15
  supportedMimeTypes: ['audio/ogg', 'audio/mp3', 'audio/mpeg', 'audio/wav'],
@@ -0,0 +1,11 @@
1
+ export const DEFAULT_MCP_TEMPLATE = {
2
+ "_comment": "MCP Server Configuration for Morpheus",
3
+ "_docs": "Add your MCP servers below. Each key is a unique server name.",
4
+ "example": {
5
+ "_comment": "EXAMPLE - Remove or replace this entry with your own MCPs",
6
+ "transport": "stdio",
7
+ "command": "npx",
8
+ "args": ["-y", "your-mcp-package-name"],
9
+ "env": {}
10
+ }
11
+ };
@@ -0,0 +1,2 @@
1
+ // Tool type definitions for Morpheus internal tools
2
+ export {};
@@ -0,0 +1 @@
1
+ export {};