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.
- package/README.md +129 -0
- package/bin/morpheus.js +19 -0
- package/dist/channels/__tests__/telegram.test.js +63 -0
- package/dist/channels/telegram.js +89 -0
- package/dist/cli/commands/config.js +81 -0
- package/dist/cli/commands/doctor.js +69 -0
- package/dist/cli/commands/init.js +108 -0
- package/dist/cli/commands/start.js +145 -0
- package/dist/cli/commands/status.js +21 -0
- package/dist/cli/commands/stop.js +28 -0
- package/dist/cli/index.js +38 -0
- package/dist/cli/utils/render.js +11 -0
- package/dist/config/__tests__/manager.test.js +11 -0
- package/dist/config/manager.js +55 -0
- package/dist/config/paths.js +15 -0
- package/dist/config/schemas.js +35 -0
- package/dist/config/utils.js +21 -0
- package/dist/http/__tests__/config_api.test.js +79 -0
- package/dist/http/api.js +134 -0
- package/dist/http/server.js +52 -0
- package/dist/runtime/__tests__/agent.test.js +91 -0
- package/dist/runtime/__tests__/agent_persistence.test.js +148 -0
- package/dist/runtime/__tests__/display.test.js +135 -0
- package/dist/runtime/__tests__/manual_start_verify.js +42 -0
- package/dist/runtime/__tests__/manual_us1.js +33 -0
- package/dist/runtime/agent.js +84 -0
- package/dist/runtime/display.js +146 -0
- package/dist/runtime/errors.js +16 -0
- package/dist/runtime/lifecycle.js +41 -0
- package/dist/runtime/memory/__tests__/sqlite.test.js +179 -0
- package/dist/runtime/memory/sqlite.js +192 -0
- package/dist/runtime/providers/factory.js +62 -0
- package/dist/runtime/scaffold.js +31 -0
- package/dist/runtime/types.js +1 -0
- package/dist/types/config.js +24 -0
- package/dist/types/display.js +1 -0
- package/dist/ui/assets/index-nNle8n-Z.css +1 -0
- package/dist/ui/assets/index-ySbKLOXZ.js +50 -0
- package/dist/ui/index.html +14 -0
- package/dist/ui/vite.svg +31 -0
- 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
|
+
}
|