morpheus-cli 0.1.0

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 (41) hide show
  1. package/README.md +129 -0
  2. package/bin/morpheus.js +19 -0
  3. package/dist/channels/__tests__/telegram.test.js +63 -0
  4. package/dist/channels/telegram.js +89 -0
  5. package/dist/cli/commands/config.js +81 -0
  6. package/dist/cli/commands/doctor.js +69 -0
  7. package/dist/cli/commands/init.js +108 -0
  8. package/dist/cli/commands/start.js +145 -0
  9. package/dist/cli/commands/status.js +21 -0
  10. package/dist/cli/commands/stop.js +28 -0
  11. package/dist/cli/index.js +38 -0
  12. package/dist/cli/utils/render.js +11 -0
  13. package/dist/config/__tests__/manager.test.js +11 -0
  14. package/dist/config/manager.js +55 -0
  15. package/dist/config/paths.js +15 -0
  16. package/dist/config/schemas.js +35 -0
  17. package/dist/config/utils.js +21 -0
  18. package/dist/http/__tests__/config_api.test.js +79 -0
  19. package/dist/http/api.js +134 -0
  20. package/dist/http/server.js +52 -0
  21. package/dist/runtime/__tests__/agent.test.js +91 -0
  22. package/dist/runtime/__tests__/agent_persistence.test.js +148 -0
  23. package/dist/runtime/__tests__/display.test.js +135 -0
  24. package/dist/runtime/__tests__/manual_start_verify.js +42 -0
  25. package/dist/runtime/__tests__/manual_us1.js +33 -0
  26. package/dist/runtime/agent.js +84 -0
  27. package/dist/runtime/display.js +146 -0
  28. package/dist/runtime/errors.js +16 -0
  29. package/dist/runtime/lifecycle.js +41 -0
  30. package/dist/runtime/memory/__tests__/sqlite.test.js +179 -0
  31. package/dist/runtime/memory/sqlite.js +192 -0
  32. package/dist/runtime/providers/factory.js +62 -0
  33. package/dist/runtime/scaffold.js +31 -0
  34. package/dist/runtime/types.js +1 -0
  35. package/dist/types/config.js +24 -0
  36. package/dist/types/display.js +1 -0
  37. package/dist/ui/assets/index-nNle8n-Z.css +1 -0
  38. package/dist/ui/assets/index-ySbKLOXZ.js +50 -0
  39. package/dist/ui/index.html +14 -0
  40. package/dist/ui/vite.svg +31 -0
  41. package/package.json +62 -0
@@ -0,0 +1,91 @@
1
+ import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
2
+ import { Agent } from '../agent.js';
3
+ import { ProviderFactory } from '../providers/factory.js';
4
+ import { DEFAULT_CONFIG } from '../../types/config.js';
5
+ import { AIMessage } from '@langchain/core/messages';
6
+ import * as fs from 'fs-extra';
7
+ import * as path from 'path';
8
+ import { homedir } from 'os';
9
+ vi.mock('../providers/factory.js');
10
+ describe('Agent', () => {
11
+ let agent;
12
+ const mockProvider = {
13
+ invoke: vi.fn(),
14
+ };
15
+ beforeEach(async () => {
16
+ vi.resetAllMocks();
17
+ // Clean up any existing test database
18
+ const defaultDbPath = path.join(homedir(), ".morpheus", "memory", "short-memory.db");
19
+ if (fs.existsSync(defaultDbPath)) {
20
+ try {
21
+ const Database = (await import("better-sqlite3")).default;
22
+ const db = new Database(defaultDbPath);
23
+ db.exec("DELETE FROM messages");
24
+ db.close();
25
+ }
26
+ catch (err) {
27
+ // Ignore errors if database doesn't exist or is corrupted
28
+ }
29
+ }
30
+ mockProvider.invoke.mockResolvedValue(new AIMessage('Hello world'));
31
+ vi.mocked(ProviderFactory.create).mockReturnValue(mockProvider);
32
+ agent = new Agent(DEFAULT_CONFIG);
33
+ });
34
+ afterEach(async () => {
35
+ // Clean up after each test
36
+ if (agent) {
37
+ try {
38
+ await agent.clearMemory();
39
+ }
40
+ catch (err) {
41
+ // Ignore cleanup errors
42
+ }
43
+ }
44
+ });
45
+ it('should initialize successfully', async () => {
46
+ await agent.initialize();
47
+ expect(ProviderFactory.create).toHaveBeenCalledWith(DEFAULT_CONFIG.llm);
48
+ });
49
+ it('should chat successfully', async () => {
50
+ await agent.initialize();
51
+ const response = await agent.chat('Hi');
52
+ expect(response).toBe('Hello world');
53
+ expect(mockProvider.invoke).toHaveBeenCalled();
54
+ });
55
+ it('should throw if not initialized', async () => {
56
+ await expect(agent.chat('Hi')).rejects.toThrow('initialize() first');
57
+ });
58
+ it('should maintain history', async () => {
59
+ await agent.initialize();
60
+ // Clear any residual history from previous tests
61
+ await agent.clearMemory();
62
+ // First turn
63
+ await agent.chat('Hi');
64
+ const history1 = await agent.getHistory();
65
+ expect(history1).toHaveLength(2);
66
+ expect(history1[0].content).toBe('Hi'); // User
67
+ expect(history1[1].content).toBe('Hello world'); // AI
68
+ // Second turn
69
+ // Update mock return value for next call
70
+ mockProvider.invoke.mockResolvedValue(new AIMessage('I am fine'));
71
+ await agent.chat('How are you?');
72
+ const history2 = await agent.getHistory();
73
+ expect(history2).toHaveLength(4);
74
+ expect(history2[2].content).toBe('How are you?');
75
+ expect(history2[3].content).toBe('I am fine');
76
+ });
77
+ describe('Configuration Validation', () => {
78
+ it('should throw if llm provider is missing', async () => {
79
+ const invalidConfig = JSON.parse(JSON.stringify(DEFAULT_CONFIG));
80
+ delete invalidConfig.llm.provider; // Invalid
81
+ const badAgent = new Agent(invalidConfig);
82
+ await expect(badAgent.initialize()).rejects.toThrow('LLM provider not specified');
83
+ });
84
+ it('should propagate ProviderError during initialization', async () => {
85
+ const mockError = new Error("Mock Factory Error");
86
+ vi.mocked(ProviderFactory.create).mockImplementation(() => { throw mockError; });
87
+ // ProviderError constructs message as: "Provider {provider} failed: {originalError.message}"
88
+ await expect(agent.initialize()).rejects.toThrow('Provider openai failed: Mock Factory Error');
89
+ });
90
+ });
91
+ });
@@ -0,0 +1,148 @@
1
+ import { describe, it, expect, beforeEach, afterEach, vi } from "vitest";
2
+ import { Agent } from "../agent.js";
3
+ import { ProviderFactory } from "../providers/factory.js";
4
+ import { DEFAULT_CONFIG } from "../../types/config.js";
5
+ import { AIMessage } from "@langchain/core/messages";
6
+ import * as fs from "fs-extra";
7
+ import * as path from "path";
8
+ import { tmpdir } from "os";
9
+ import { homedir } from "os";
10
+ vi.mock("../providers/factory.js");
11
+ describe("Agent Persistence Integration", () => {
12
+ let agent;
13
+ let testDbPath;
14
+ const mockProvider = {
15
+ invoke: vi.fn(),
16
+ };
17
+ beforeEach(async () => {
18
+ vi.resetAllMocks();
19
+ // Clean up by clearing all data from the database (safer than deleting the locked file)
20
+ const defaultDbPath = path.join(homedir(), ".morpheus", "memory", "short-memory.db");
21
+ if (fs.existsSync(defaultDbPath)) {
22
+ try {
23
+ const Database = (await import("better-sqlite3")).default;
24
+ const db = new Database(defaultDbPath);
25
+ db.exec("DELETE FROM messages");
26
+ db.close();
27
+ }
28
+ catch (err) {
29
+ // Ignore errors if database doesn't exist or is corrupted
30
+ }
31
+ }
32
+ // Create a temporary test database path
33
+ const tempDir = path.join(tmpdir(), "morpheus-test-agent", Date.now().toString());
34
+ testDbPath = path.join(tempDir, "short-memory.db");
35
+ // Mock the SQLiteChatMessageHistory to use test path
36
+ // We'll use the default ~/.morpheus path for this test
37
+ mockProvider.invoke.mockResolvedValue(new AIMessage("Test response"));
38
+ vi.mocked(ProviderFactory.create).mockReturnValue(mockProvider);
39
+ agent = new Agent(DEFAULT_CONFIG);
40
+ });
41
+ afterEach(async () => {
42
+ // Clean up test database
43
+ const defaultDbPath = path.join(homedir(), ".morpheus", "memory", "short-memory.db");
44
+ if (fs.existsSync(defaultDbPath)) {
45
+ // We can't delete it if it's open, so we'll just clear it
46
+ // The agent should have closed the connection
47
+ }
48
+ });
49
+ describe("Database File Creation", () => {
50
+ it("should create database file on initialization", async () => {
51
+ await agent.initialize();
52
+ const defaultDbPath = path.join(homedir(), ".morpheus", "memory", "short-memory.db");
53
+ expect(fs.existsSync(defaultDbPath)).toBe(true);
54
+ });
55
+ });
56
+ describe("Message Persistence", () => {
57
+ it("should persist messages to database", async () => {
58
+ await agent.initialize();
59
+ // Send a message
60
+ await agent.chat("Hello, Agent!");
61
+ // Verify history contains the message
62
+ const history = await agent.getHistory();
63
+ expect(history).toHaveLength(2); // User message + AI response
64
+ expect(history[0].content).toBe("Hello, Agent!");
65
+ expect(history[1].content).toBe("Test response");
66
+ });
67
+ it("should restore conversation history on restart", async () => {
68
+ // First session: send a message
69
+ await agent.initialize();
70
+ await agent.chat("Remember this message");
71
+ const firstHistory = await agent.getHistory();
72
+ expect(firstHistory).toHaveLength(2);
73
+ // Simulate restart: create new agent instance
74
+ const agent2 = new Agent(DEFAULT_CONFIG);
75
+ await agent2.initialize();
76
+ // Verify history was restored
77
+ const restoredHistory = await agent2.getHistory();
78
+ expect(restoredHistory.length).toBeGreaterThanOrEqual(2);
79
+ // Check that the old messages are present
80
+ const contents = restoredHistory.map(m => m.content);
81
+ expect(contents).toContain("Remember this message");
82
+ expect(contents).toContain("Test response");
83
+ });
84
+ it("should accumulate messages across multiple conversations", async () => {
85
+ await agent.initialize();
86
+ // First conversation
87
+ await agent.chat("First message");
88
+ // Second conversation
89
+ mockProvider.invoke.mockResolvedValue(new AIMessage("Second response"));
90
+ await agent.chat("Second message");
91
+ // Verify all messages are persisted
92
+ const history = await agent.getHistory();
93
+ expect(history).toHaveLength(4);
94
+ expect(history[0].content).toBe("First message");
95
+ expect(history[1].content).toBe("Test response");
96
+ expect(history[2].content).toBe("Second message");
97
+ expect(history[3].content).toBe("Second response");
98
+ });
99
+ });
100
+ describe("Memory Clearing", () => {
101
+ it("should clear all persisted messages", async () => {
102
+ await agent.initialize();
103
+ // Add some messages
104
+ await agent.chat("Message 1");
105
+ await agent.chat("Message 2");
106
+ // Verify messages exist
107
+ let history = await agent.getHistory();
108
+ expect(history.length).toBeGreaterThan(0);
109
+ // Clear memory
110
+ await agent.clearMemory();
111
+ // Verify messages are cleared
112
+ history = await agent.getHistory();
113
+ expect(history).toEqual([]);
114
+ });
115
+ it("should start fresh after clearing memory", async () => {
116
+ await agent.initialize();
117
+ // Add and clear messages
118
+ await agent.chat("Old message");
119
+ await agent.clearMemory();
120
+ // Add new message
121
+ mockProvider.invoke.mockResolvedValue(new AIMessage("New response"));
122
+ await agent.chat("New message");
123
+ // Verify only new messages exist
124
+ const history = await agent.getHistory();
125
+ expect(history).toHaveLength(2);
126
+ expect(history[0].content).toBe("New message");
127
+ expect(history[1].content).toBe("New response");
128
+ });
129
+ });
130
+ describe("Context Preservation", () => {
131
+ it("should include history in conversation context", async () => {
132
+ await agent.initialize();
133
+ // First message
134
+ await agent.chat("My name is Alice");
135
+ // Second message (should have context)
136
+ mockProvider.invoke.mockResolvedValue(new AIMessage("Hello Alice!"));
137
+ await agent.chat("What is my name?");
138
+ // Verify the provider was called with full history
139
+ const lastCall = mockProvider.invoke.mock.calls[1];
140
+ const messagesPassedToLLM = lastCall[0];
141
+ // Should include: system message, previous user message, previous AI response, new user message
142
+ expect(messagesPassedToLLM.length).toBeGreaterThanOrEqual(4);
143
+ // Check that context includes the original message
144
+ const contents = messagesPassedToLLM.map((m) => m.content);
145
+ expect(contents).toContain("My name is Alice");
146
+ });
147
+ });
148
+ });
@@ -0,0 +1,135 @@
1
+ import { describe, it, expect, vi, beforeEach } from 'vitest';
2
+ import { DisplayManager } from '../display.js';
3
+ import winston from 'winston';
4
+ import DailyRotateFile from 'winston-daily-rotate-file';
5
+ // Hoisted mocks to ensure they are available for the module mock factory
6
+ const mocks = vi.hoisted(() => {
7
+ return {
8
+ start: vi.fn(),
9
+ stop: vi.fn(),
10
+ succeed: vi.fn(),
11
+ fail: vi.fn(),
12
+ loggerLog: vi.fn(),
13
+ };
14
+ });
15
+ // We need a way to control isSpinning.
16
+ // Since the factory runs hoisting, we can't easily closure over a let variable unless we put it on the mocks object or similar.
17
+ // But we can just make the mock implementation toggle a property on itself or use a side channel?
18
+ // Simplest: Mock object manages its own state? No, DisplayManager reads `.isSpinning`.
19
+ // Let's implement logic IN the mock.
20
+ let isSpinningInternal = false;
21
+ vi.mock('ora', () => {
22
+ return {
23
+ default: vi.fn(() => ({
24
+ start: mocks.start.mockImplementation(() => { isSpinningInternal = true; }),
25
+ stop: mocks.stop.mockImplementation(() => { isSpinningInternal = false; }),
26
+ succeed: mocks.succeed.mockImplementation(() => { isSpinningInternal = false; }),
27
+ fail: mocks.fail.mockImplementation(() => { isSpinningInternal = false; }),
28
+ get isSpinning() { return isSpinningInternal; },
29
+ set text(val) { },
30
+ get text() { return 'loading...'; }
31
+ }))
32
+ };
33
+ });
34
+ vi.mock('winston', () => ({
35
+ default: {
36
+ createLogger: vi.fn(() => ({
37
+ log: mocks.loggerLog,
38
+ })),
39
+ format: {
40
+ combine: vi.fn(),
41
+ timestamp: vi.fn(),
42
+ printf: vi.fn(),
43
+ },
44
+ transports: {
45
+ DailyRotateFile: vi.fn(),
46
+ }
47
+ }
48
+ }));
49
+ vi.mock('winston-daily-rotate-file', () => {
50
+ return {
51
+ default: vi.fn()
52
+ };
53
+ });
54
+ describe('DisplayManager', () => {
55
+ beforeEach(() => {
56
+ vi.clearAllMocks();
57
+ isSpinningInternal = false;
58
+ // We cannot reset the singleton instance as it is private and static.
59
+ // So we must ensure state is consistent via public API in tests or beforeEach.
60
+ const dm = DisplayManager.getInstance();
61
+ dm.stopSpinner();
62
+ });
63
+ it('should be a singleton', () => {
64
+ const instance1 = DisplayManager.getInstance();
65
+ const instance2 = DisplayManager.getInstance();
66
+ expect(instance1).toBe(instance2);
67
+ });
68
+ it('should start spinner', () => {
69
+ const dm = DisplayManager.getInstance();
70
+ dm.startSpinner('test spinner');
71
+ expect(mocks.start).toHaveBeenCalledWith('test spinner');
72
+ expect(isSpinningInternal).toBe(true);
73
+ });
74
+ it('should not start spinner again if already spinning', () => {
75
+ const dm = DisplayManager.getInstance();
76
+ dm.startSpinner('first');
77
+ expect(mocks.start).toHaveBeenCalledTimes(1);
78
+ // Call again
79
+ dm.startSpinner('second');
80
+ expect(mocks.start).toHaveBeenCalledTimes(1); // Should not call start again
81
+ // It might update text, but start is not called
82
+ });
83
+ it('should stop spinner', () => {
84
+ const dm = DisplayManager.getInstance();
85
+ dm.startSpinner();
86
+ dm.stopSpinner();
87
+ expect(mocks.stop).toHaveBeenCalled();
88
+ expect(isSpinningInternal).toBe(false);
89
+ });
90
+ it('should log message and preserve spinner state', () => {
91
+ const dm = DisplayManager.getInstance();
92
+ dm.startSpinner('spinning');
93
+ const consoleSpy = vi.spyOn(console, 'log').mockImplementation(() => { });
94
+ dm.log('hello world');
95
+ // Flow: stop spinner -> log -> start spinner
96
+ // Calls: start(spinning) -> stop() -> log -> start(spinning)
97
+ // Check stop called (after initial start)
98
+ expect(mocks.stop).toHaveBeenCalled();
99
+ // Check log called
100
+ expect(consoleSpy).toHaveBeenCalledWith('hello world');
101
+ // Check start called again (restore)
102
+ // Total start calls: 1 (initial) + 1 (restore) = 2
103
+ expect(mocks.start).toHaveBeenCalledTimes(2);
104
+ expect(mocks.start).toHaveBeenLastCalledWith('loading...'); // Use mocked text getter
105
+ });
106
+ it('should log message without spinner interaction if not spinning', () => {
107
+ const dm = DisplayManager.getInstance();
108
+ // Ensure stopped
109
+ dm.stopSpinner();
110
+ vi.clearAllMocks(); // Clear calls from setup
111
+ const consoleSpy = vi.spyOn(console, 'log').mockImplementation(() => { });
112
+ dm.log('hello world');
113
+ expect(mocks.stop).not.toHaveBeenCalled();
114
+ expect(mocks.start).not.toHaveBeenCalled();
115
+ expect(consoleSpy).toHaveBeenCalledWith('hello world');
116
+ });
117
+ it('should initialize logger with config', async () => {
118
+ const dm = DisplayManager.getInstance();
119
+ await dm.initialize({ enabled: true, level: 'info', retention: '14d' });
120
+ expect(winston.createLogger).toHaveBeenCalled();
121
+ // Check DailyRotateFile instantiation
122
+ expect(DailyRotateFile).toHaveBeenCalledWith(expect.objectContaining({
123
+ maxFiles: '14d'
124
+ }));
125
+ });
126
+ it('should log to logger if initialized', async () => {
127
+ const dm = DisplayManager.getInstance();
128
+ await dm.initialize({ enabled: true, level: 'info', retention: '14d' });
129
+ dm.log('test message', { level: 'info' });
130
+ expect(mocks.loggerLog).toHaveBeenCalledWith(expect.objectContaining({
131
+ level: 'info',
132
+ message: 'test message'
133
+ }));
134
+ });
135
+ });
@@ -0,0 +1,42 @@
1
+ import { Agent } from '../agent.js';
2
+ import chalk from 'chalk';
3
+ const start = Date.now();
4
+ console.log(chalk.blue('Running Manual Start Verification...'));
5
+ const mockConfig = {
6
+ agent: { name: 'TestAgent', personality: 'Robot' },
7
+ llm: { provider: 'openai', model: 'gpt-3.5-turbo', temperature: 0.1, api_key: 'sk-mock-key' },
8
+ channels: {
9
+ telegram: { enabled: false, allowedUsers: [] },
10
+ discord: { enabled: false }
11
+ },
12
+ ui: { enabled: false, port: 3333 },
13
+ logging: { enabled: false, level: 'info', retention: '1d' }
14
+ };
15
+ const run = async () => {
16
+ try {
17
+ console.log(chalk.gray('1. Instantiating Agent...'));
18
+ const agent = new Agent(mockConfig);
19
+ console.log(chalk.gray('2. Initializing Agent...'));
20
+ await agent.initialize();
21
+ const duration = (Date.now() - start) / 1000;
22
+ console.log(chalk.green(`✓ Agent initialized successfully in ${duration}s`));
23
+ if (duration > 5) {
24
+ console.log(chalk.red(`✗ Startup took too long (> 5s)`));
25
+ process.exit(1);
26
+ }
27
+ console.log(chalk.gray('3. Testing Initialization Check...'));
28
+ try {
29
+ await agent.chat('Hello');
30
+ // This might fail if using real network, but we just want to ensure it tries
31
+ }
32
+ catch (e) {
33
+ console.log(chalk.yellow(`Chat check: ${e.message}`));
34
+ // Expected to fail with mock key on real network, that's fine for "Start" verification
35
+ }
36
+ }
37
+ catch (error) {
38
+ console.error(chalk.red('Verification Failed:'), error);
39
+ process.exit(1);
40
+ }
41
+ };
42
+ run();
@@ -0,0 +1,33 @@
1
+ import { Agent } from '../agent.js';
2
+ import { DEFAULT_CONFIG } from '../../types/config.js';
3
+ // Verify environment requirements
4
+ if (!process.env.OPENAI_API_KEY) {
5
+ console.error("Skipping manual test: OPENAI_API_KEY not found in environment");
6
+ process.exit(0);
7
+ }
8
+ const manualConfig = {
9
+ ...DEFAULT_CONFIG,
10
+ llm: {
11
+ provider: 'openai',
12
+ model: 'gpt-3.5-turbo',
13
+ temperature: 0.7,
14
+ api_key: process.env.OPENAI_API_KEY
15
+ }
16
+ };
17
+ async function run() {
18
+ console.log("Initializing Agent...");
19
+ const agent = new Agent(manualConfig);
20
+ await agent.initialize();
21
+ console.log("Sending message: 'Hello, are you there?'");
22
+ try {
23
+ const response = await agent.chat("Hello, are you there?");
24
+ console.log("Response received:");
25
+ console.log("---------------------------------------------------");
26
+ console.log(response);
27
+ console.log("---------------------------------------------------");
28
+ }
29
+ catch (error) {
30
+ console.error("Chat failed:", error);
31
+ }
32
+ }
33
+ run();
@@ -0,0 +1,84 @@
1
+ import { HumanMessage, SystemMessage, AIMessage } from "@langchain/core/messages";
2
+ import { ProviderFactory } from "./providers/factory.js";
3
+ import { ConfigManager } from "../config/manager.js";
4
+ import { ProviderError } from "./errors.js";
5
+ import { DisplayManager } from "./display.js";
6
+ import { SQLiteChatMessageHistory } from "./memory/sqlite.js";
7
+ export class Agent {
8
+ provider;
9
+ config;
10
+ history;
11
+ display = DisplayManager.getInstance();
12
+ constructor(config) {
13
+ this.config = config || ConfigManager.getInstance().get();
14
+ }
15
+ async initialize() {
16
+ if (!this.config.llm) {
17
+ throw new Error("LLM configuration missing in config object.");
18
+ }
19
+ // Basic validation before provider creation
20
+ if (!this.config.llm.provider) {
21
+ throw new Error("LLM provider not specified in configuration.");
22
+ }
23
+ // Note: API Key validation is delegated to ProviderFactory or the Provider itself
24
+ // to allow for Environment Variable fallback supported by LangChain.
25
+ try {
26
+ this.provider = ProviderFactory.create(this.config.llm);
27
+ if (!this.provider) {
28
+ throw new Error("Provider factory returned undefined");
29
+ }
30
+ // Initialize persistent memory with SQLite
31
+ this.history = new SQLiteChatMessageHistory({
32
+ sessionId: "default",
33
+ });
34
+ }
35
+ catch (err) {
36
+ if (err instanceof ProviderError)
37
+ throw err; // Re-throw known errors
38
+ // Wrap unknown errors
39
+ throw new ProviderError(this.config.llm.provider || 'unknown', err, "Agent initialization failed");
40
+ }
41
+ }
42
+ async chat(message) {
43
+ if (!this.provider) {
44
+ throw new Error("Agent not initialized. Call initialize() first.");
45
+ }
46
+ if (!this.history) {
47
+ throw new Error("Message history not initialized. Call initialize() first.");
48
+ }
49
+ try {
50
+ this.display.log('Processing message...', { source: 'Agent' });
51
+ const userMessage = new HumanMessage(message);
52
+ const systemMessage = new SystemMessage(`You are ${this.config.agent.name}, ${this.config.agent.personality}. You are a personal dev assistent.`);
53
+ // Load existing history from database
54
+ const previousMessages = await this.history.getMessages();
55
+ const messages = [
56
+ systemMessage,
57
+ ...previousMessages,
58
+ userMessage
59
+ ];
60
+ const response = await this.provider.invoke(messages);
61
+ const content = typeof response.content === 'string' ? response.content : JSON.stringify(response.content);
62
+ // Persist messages to database
63
+ await this.history.addMessage(userMessage);
64
+ await this.history.addMessage(new AIMessage(content));
65
+ this.display.log('Response generated.', { source: 'Agent' });
66
+ return content;
67
+ }
68
+ catch (err) {
69
+ throw new ProviderError(this.config.llm.provider, err, "Chat request failed");
70
+ }
71
+ }
72
+ async getHistory() {
73
+ if (!this.history) {
74
+ throw new Error("Message history not initialized. Call initialize() first.");
75
+ }
76
+ return await this.history.getMessages();
77
+ }
78
+ async clearMemory() {
79
+ if (!this.history) {
80
+ throw new Error("Message history not initialized. Call initialize() first.");
81
+ }
82
+ await this.history.clear();
83
+ }
84
+ }
@@ -0,0 +1,146 @@
1
+ import ora from 'ora';
2
+ import chalk from 'chalk';
3
+ import winston from 'winston';
4
+ import DailyRotateFile from 'winston-daily-rotate-file';
5
+ import { LOGS_DIR } from '../config/paths.js';
6
+ export class DisplayManager {
7
+ static instance;
8
+ spinner;
9
+ logger;
10
+ constructor() {
11
+ this.spinner = ora();
12
+ }
13
+ static getInstance() {
14
+ if (!DisplayManager.instance) {
15
+ DisplayManager.instance = new DisplayManager();
16
+ }
17
+ return DisplayManager.instance;
18
+ }
19
+ async initialize(config) {
20
+ if (!config.enabled) {
21
+ return;
22
+ }
23
+ const { combine, timestamp, printf } = winston.format;
24
+ const logFormat = printf(({ level, message, timestamp, ...meta }) => {
25
+ const metaStr = Object.keys(meta).length ? JSON.stringify(meta) : '';
26
+ return `${timestamp} [${level}] ${message} ${metaStr}`.trim();
27
+ });
28
+ this.logger = winston.createLogger({
29
+ level: config.level,
30
+ format: combine(timestamp(), logFormat),
31
+ transports: [
32
+ new DailyRotateFile({
33
+ dirname: LOGS_DIR,
34
+ filename: 'morpheus-%DATE%.log',
35
+ datePattern: 'YYYY-MM-DD',
36
+ maxFiles: config.retention,
37
+ }),
38
+ ],
39
+ });
40
+ }
41
+ startSpinner(text) {
42
+ if (this.spinner.isSpinning) {
43
+ if (text) {
44
+ this.spinner.text = text;
45
+ }
46
+ return;
47
+ }
48
+ this.spinner.start(text);
49
+ }
50
+ updateSpinner(text) {
51
+ if (this.spinner.isSpinning) {
52
+ this.spinner.text = text;
53
+ }
54
+ }
55
+ stopSpinner(success) {
56
+ if (!this.spinner.isSpinning)
57
+ return;
58
+ if (success === true) {
59
+ this.spinner.succeed();
60
+ }
61
+ else if (success === false) {
62
+ this.spinner.fail();
63
+ }
64
+ else {
65
+ this.spinner.stop();
66
+ }
67
+ }
68
+ log(message, options) {
69
+ const wasSpinning = this.spinner.isSpinning;
70
+ const previousText = this.spinner.text;
71
+ if (wasSpinning) {
72
+ this.spinner.stop();
73
+ }
74
+ let prefix = '';
75
+ if (options?.source) {
76
+ let color = chalk.blue;
77
+ if (options.source === 'Telegram') {
78
+ color = chalk.green;
79
+ }
80
+ else if (options.source === 'Agent') {
81
+ color = chalk.hex('#FFA500');
82
+ }
83
+ prefix = color(`[${options.source}] `);
84
+ }
85
+ let formattedMessage = message;
86
+ if (options?.level) {
87
+ switch (options.level) {
88
+ case 'error':
89
+ formattedMessage = chalk.red(message);
90
+ break;
91
+ case 'warning':
92
+ formattedMessage = chalk.yellow(message);
93
+ break;
94
+ case 'success':
95
+ formattedMessage = chalk.green(message);
96
+ break;
97
+ case 'info':
98
+ case 'debug':
99
+ default:
100
+ formattedMessage = message;
101
+ }
102
+ }
103
+ // Only print debug messages if we were verbose? Or always print?
104
+ // Console output should probably filter debug unless debug mode is on.
105
+ // But currently DisplayManager doesn't seem to hold global 'debug' state for console,
106
+ // it just prints what it's told.
107
+ // For now I'll assume console log logic remains matching inputs roughly.
108
+ // Spec doesn't strictly say suppress debug on console, but implies user value focus.
109
+ // 'debug' level usually shouldn't clutter console unless requested.
110
+ // But existing log() Implementation didn't handle 'debug' specifically before I added it to type.
111
+ // I'll leave console behavior as is (prints everything).
112
+ console.log(`${prefix}${formattedMessage}`);
113
+ if (this.logger) {
114
+ try {
115
+ const level = this.mapLevel(options?.level);
116
+ const meta = options?.meta || {};
117
+ if (options?.source) {
118
+ meta.source = options.source;
119
+ }
120
+ this.logger.log({
121
+ level,
122
+ message,
123
+ ...meta
124
+ });
125
+ }
126
+ catch (err) {
127
+ // Safe logging fail-safe as per T015
128
+ // Do not crash if file logging fails, maybe print to stderr?
129
+ // But if logging itself fails, maybe just ignore to keep CLI running.
130
+ }
131
+ }
132
+ if (wasSpinning) {
133
+ this.spinner.start(previousText);
134
+ }
135
+ }
136
+ mapLevel(level) {
137
+ switch (level) {
138
+ case 'warning': return 'warn';
139
+ case 'success': return 'info';
140
+ case 'error': return 'error';
141
+ case 'debug': return 'debug';
142
+ case 'info':
143
+ default: return 'info';
144
+ }
145
+ }
146
+ }