morpheus-cli 0.1.11 → 0.2.2
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 +14 -5
- package/dist/cli/commands/doctor.js +36 -1
- package/dist/cli/commands/init.js +92 -0
- package/dist/cli/commands/start.js +2 -1
- package/dist/cli/index.js +1 -17
- package/dist/cli/utils/render.js +2 -1
- package/dist/cli/utils/version.js +16 -0
- package/dist/config/manager.js +16 -0
- package/dist/config/schemas.js +15 -8
- package/dist/http/api.js +46 -0
- package/dist/runtime/__tests__/manual_santi_verify.js +55 -0
- package/dist/runtime/display.js +3 -0
- package/dist/runtime/memory/sati/__tests__/repository.test.js +71 -0
- package/dist/runtime/memory/sati/__tests__/service.test.js +99 -0
- package/dist/runtime/memory/sati/index.js +59 -0
- package/dist/runtime/memory/sati/repository.js +219 -0
- package/dist/runtime/memory/sati/service.js +142 -0
- package/dist/runtime/memory/sati/system-prompts.js +40 -0
- package/dist/runtime/memory/sati/types.js +1 -0
- package/dist/runtime/memory/sqlite.js +5 -1
- package/dist/runtime/migration.js +53 -1
- package/dist/runtime/oracle.js +32 -7
- package/dist/runtime/santi/contracts.js +1 -0
- package/dist/runtime/santi/middleware.js +61 -0
- package/dist/runtime/santi/santi.js +109 -0
- package/dist/runtime/santi/store.js +158 -0
- package/dist/runtime/tools/factory.js +31 -25
- package/dist/types/config.js +1 -0
- package/dist/ui/assets/index-Ddyo4FWH.js +50 -0
- package/dist/ui/assets/{index-AEbYNHuy.css → index-tz0YVye-.css} +1 -1
- package/dist/ui/index.html +2 -2
- package/package.json +3 -3
- package/dist/ui/assets/index-BjnI8c1U.js +0 -50
package/README.md
CHANGED
|
@@ -80,9 +80,9 @@ Morpheus is built with **Node.js** and **TypeScript**, using **LangChain** as th
|
|
|
80
80
|
|
|
81
81
|
### Core Components
|
|
82
82
|
|
|
83
|
-
- **Runtime (`src/runtime/`)**: The heart of the application. Manages the agent lifecycle, provider instantiation, and command execution.
|
|
83
|
+
- **Runtime (`src/runtime/`)**: The heart of the application. Manages the Oracle (agent) lifecycle, provider instantiation, and command execution.
|
|
84
84
|
- **CLI (`src/cli/`)**: Built with `commander`, handles user interaction, configuration, and daemon control (`start`, `stop`, `status`).
|
|
85
|
-
- **Configuration (`src/config/`)**: Singleton-based configuration manager using `zod` for validation and `js-yaml` for persistence (`~/.morpheus/
|
|
85
|
+
- **Configuration (`src/config/`)**: Singleton-based configuration manager using `zod` for validation and `js-yaml` for persistence (`~/.morpheus/zaion.yaml`).
|
|
86
86
|
- **Channels (`src/channels/`)**: Adapters for external communication. Currently supports Telegram (`telegraf`) with strict user whitelisting.
|
|
87
87
|
|
|
88
88
|
## Features
|
|
@@ -115,6 +115,12 @@ When enabled:
|
|
|
115
115
|
### 🧩 MCP Support (Model Context Protocol)
|
|
116
116
|
Full integration with [Model Context Protocol](https://modelcontextprotocol.io/), allowing Morpheus to use standardized tools from any MCP-compatible server.
|
|
117
117
|
|
|
118
|
+
### 🧠 Sati (Long-Term Memory)
|
|
119
|
+
Morpheus features a dedicated middleware system called **Sati** (Mindfulness) that provides long-term memory capabilities.
|
|
120
|
+
- **Automated Storage**: Automatically extracts and saves preferences, project details, and facts from conversations.
|
|
121
|
+
- **Contextual Retrieval**: Injects relevant memories into the context based on your current query.
|
|
122
|
+
- **Data Privacy**: Stored in a local, independent SQLite database (`santi-memory.db`), ensuring sensitive data is handled securely and reducing context window usage.
|
|
123
|
+
|
|
118
124
|
### 📊 Usage Analytics
|
|
119
125
|
Track your token usage across different providers and models directly from the Web UI. View detailed breakdowns of input/output tokens and message counts to monitor costs and activity.
|
|
120
126
|
|
|
@@ -169,7 +175,7 @@ npm start -- status
|
|
|
169
175
|
|
|
170
176
|
### 4. Configuration
|
|
171
177
|
|
|
172
|
-
The configuration file is located at `~/.morpheus/
|
|
178
|
+
The configuration file is located at `~/.morpheus/zaion.yaml`. You can edit it manually or use the `morpheus config` command.
|
|
173
179
|
|
|
174
180
|
```yaml
|
|
175
181
|
agent:
|
|
@@ -179,9 +185,12 @@ llm:
|
|
|
179
185
|
provider: "openai" # options: openai, anthropic, ollama, gemini
|
|
180
186
|
model: "gpt-4-turbo"
|
|
181
187
|
temperature: 0.7
|
|
188
|
+
context_window: 100 # Number of messages to load into LLM context
|
|
182
189
|
api_key: "sk-..."
|
|
183
|
-
|
|
184
|
-
|
|
190
|
+
santi: # Optional: Sati (Long-Term Memory) specific settings
|
|
191
|
+
provider: "openai" # defaults to llm.provider
|
|
192
|
+
model: "gpt-4o"
|
|
193
|
+
memory_limit: 1000 # Number of messages/items to retrieve
|
|
185
194
|
channels:
|
|
186
195
|
telegram:
|
|
187
196
|
enabled: true
|
|
@@ -23,8 +23,30 @@ export const doctorCommand = new Command('doctor')
|
|
|
23
23
|
// 2. Check Configuration
|
|
24
24
|
try {
|
|
25
25
|
if (await fs.pathExists(PATHS.config)) {
|
|
26
|
-
await ConfigManager.getInstance().load();
|
|
26
|
+
const config = await ConfigManager.getInstance().load();
|
|
27
27
|
console.log(chalk.green('✓') + ' Configuration: Valid');
|
|
28
|
+
// Check context window configuration
|
|
29
|
+
const contextWindow = config.llm?.context_window;
|
|
30
|
+
const deprecatedLimit = config.memory?.limit;
|
|
31
|
+
if (contextWindow !== undefined) {
|
|
32
|
+
if (typeof contextWindow === 'number' && contextWindow > 0) {
|
|
33
|
+
console.log(chalk.green('✓') + ` LLM context window: ${contextWindow} messages`);
|
|
34
|
+
}
|
|
35
|
+
else {
|
|
36
|
+
console.log(chalk.red('✗') + ` LLM context window has invalid value, using default: 100 messages`);
|
|
37
|
+
allPassed = false;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
else {
|
|
41
|
+
console.log(chalk.yellow('⚠') + ' LLM context window not configured, using default: 100 messages');
|
|
42
|
+
}
|
|
43
|
+
// Check for deprecated field
|
|
44
|
+
if (deprecatedLimit !== undefined && contextWindow === undefined) {
|
|
45
|
+
console.log(chalk.yellow('⚠') + ' Deprecated config detected: \'memory.limit\' should be migrated to \'llm.context_window\'. Will auto-migrate on next start.');
|
|
46
|
+
}
|
|
47
|
+
else if (deprecatedLimit !== undefined && contextWindow !== undefined) {
|
|
48
|
+
console.log(chalk.yellow('⚠') + ' Found both \'memory.limit\' and \'llm.context_window\'. Remove \'memory.limit\' from config.');
|
|
49
|
+
}
|
|
28
50
|
}
|
|
29
51
|
else {
|
|
30
52
|
console.log(chalk.yellow('!') + ' Configuration: Missing (will be created on start)');
|
|
@@ -58,6 +80,19 @@ export const doctorCommand = new Command('doctor')
|
|
|
58
80
|
console.log(chalk.red('✗') + ` Logs: Cannot write to ${PATHS.logs}`);
|
|
59
81
|
allPassed = false;
|
|
60
82
|
}
|
|
83
|
+
// 5. Check Sati Memory DB
|
|
84
|
+
try {
|
|
85
|
+
const satiDbPath = path.join(PATHS.memory, 'santi-memory.db');
|
|
86
|
+
if (await fs.pathExists(satiDbPath)) {
|
|
87
|
+
console.log(chalk.green('✓') + ' Sati Memory: Database exists');
|
|
88
|
+
}
|
|
89
|
+
else {
|
|
90
|
+
console.log(chalk.yellow('!') + ' Sati Memory: Database not initialized (will be created on start)');
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
catch (error) {
|
|
94
|
+
console.log(chalk.red('✗') + ` Sati Memory: Check failed (${error.message})`);
|
|
95
|
+
}
|
|
61
96
|
console.log(chalk.gray('================'));
|
|
62
97
|
if (allPassed) {
|
|
63
98
|
console.log(chalk.green('Diagnostics Passed. You are ready to run Morpheus!'));
|
|
@@ -4,6 +4,7 @@ import chalk from 'chalk';
|
|
|
4
4
|
import { ConfigManager } from '../../config/manager.js';
|
|
5
5
|
import { renderBanner } from '../utils/render.js';
|
|
6
6
|
import { DisplayManager } from '../../runtime/display.js';
|
|
7
|
+
import { SatiRepository } from '../../runtime/memory/sati/repository.js';
|
|
7
8
|
// import { scaffold } from '../../runtime/scaffold.js';
|
|
8
9
|
export const initCommand = new Command('init')
|
|
9
10
|
.description('Initialize Morpheus configuration')
|
|
@@ -74,6 +75,89 @@ export const initCommand = new Command('init')
|
|
|
74
75
|
if (apiKey) {
|
|
75
76
|
await configManager.set('llm.api_key', apiKey);
|
|
76
77
|
}
|
|
78
|
+
// Context Window Configuration
|
|
79
|
+
const contextWindow = await input({
|
|
80
|
+
message: 'Context Window Size (number of messages to send to LLM):',
|
|
81
|
+
default: currentConfig.llm.context_window?.toString() || '100',
|
|
82
|
+
validate: (val) => (!isNaN(Number(val)) && Number(val) > 0) || 'Must be a positive number'
|
|
83
|
+
});
|
|
84
|
+
await configManager.set('llm.context_window', Number(contextWindow));
|
|
85
|
+
// Santi (Memory Agent) Configuration
|
|
86
|
+
display.log(chalk.blue('\nSati (Memory Agent) Configuration'));
|
|
87
|
+
const configureSanti = await select({
|
|
88
|
+
message: 'Configure Sati separately?',
|
|
89
|
+
choices: [
|
|
90
|
+
{ name: 'No (Use main LLM settings)', value: 'no' },
|
|
91
|
+
{ name: 'Yes', value: 'yes' },
|
|
92
|
+
],
|
|
93
|
+
default: 'no',
|
|
94
|
+
});
|
|
95
|
+
let santiProvider = provider;
|
|
96
|
+
let santiModel = model;
|
|
97
|
+
let santiApiKey = apiKey;
|
|
98
|
+
// If using main settings and no new key provided, use existing if available
|
|
99
|
+
if (configureSanti === 'no' && !santiApiKey && hasExistingKey) {
|
|
100
|
+
santiApiKey = currentConfig.llm.api_key;
|
|
101
|
+
}
|
|
102
|
+
if (configureSanti === 'yes') {
|
|
103
|
+
santiProvider = await select({
|
|
104
|
+
message: 'Select Sati LLM Provider:',
|
|
105
|
+
choices: [
|
|
106
|
+
{ name: 'OpenAI', value: 'openai' },
|
|
107
|
+
{ name: 'Anthropic', value: 'anthropic' },
|
|
108
|
+
{ name: 'Ollama', value: 'ollama' },
|
|
109
|
+
{ name: 'Google Gemini', value: 'gemini' },
|
|
110
|
+
],
|
|
111
|
+
default: currentConfig.santi?.provider || provider,
|
|
112
|
+
});
|
|
113
|
+
let defaultSantiModel = 'gpt-3.5-turbo';
|
|
114
|
+
switch (santiProvider) {
|
|
115
|
+
case 'openai':
|
|
116
|
+
defaultSantiModel = 'gpt-4o';
|
|
117
|
+
break;
|
|
118
|
+
case 'anthropic':
|
|
119
|
+
defaultSantiModel = 'claude-3-5-sonnet-20240620';
|
|
120
|
+
break;
|
|
121
|
+
case 'ollama':
|
|
122
|
+
defaultSantiModel = 'llama3';
|
|
123
|
+
break;
|
|
124
|
+
case 'gemini':
|
|
125
|
+
defaultSantiModel = 'gemini-pro';
|
|
126
|
+
break;
|
|
127
|
+
}
|
|
128
|
+
if (santiProvider === currentConfig.santi?.provider) {
|
|
129
|
+
defaultSantiModel = currentConfig.santi?.model || defaultSantiModel;
|
|
130
|
+
}
|
|
131
|
+
santiModel = await input({
|
|
132
|
+
message: 'Enter Sati Model Name:',
|
|
133
|
+
default: defaultSantiModel,
|
|
134
|
+
});
|
|
135
|
+
const hasExistingSantiKey = !!currentConfig.santi?.api_key;
|
|
136
|
+
const santiKeyMsg = hasExistingSantiKey
|
|
137
|
+
? 'Enter Sati API Key (leave empty to preserve existing):'
|
|
138
|
+
: 'Enter Sati API Key:';
|
|
139
|
+
const keyInput = await password({ message: santiKeyMsg });
|
|
140
|
+
if (keyInput) {
|
|
141
|
+
santiApiKey = keyInput;
|
|
142
|
+
}
|
|
143
|
+
else if (hasExistingSantiKey) {
|
|
144
|
+
santiApiKey = currentConfig.santi?.api_key;
|
|
145
|
+
}
|
|
146
|
+
else {
|
|
147
|
+
santiApiKey = undefined; // Ensure we don't accidentally carry over invalid state
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
const memoryLimit = await input({
|
|
151
|
+
message: 'Sati Memory Retrieval Limit (messages):',
|
|
152
|
+
default: currentConfig.santi?.memory_limit?.toString() || '1000',
|
|
153
|
+
validate: (val) => !isNaN(Number(val)) && Number(val) > 0 || 'Must be a positive number'
|
|
154
|
+
});
|
|
155
|
+
await configManager.set('santi.provider', santiProvider);
|
|
156
|
+
await configManager.set('santi.model', santiModel);
|
|
157
|
+
await configManager.set('santi.memory_limit', Number(memoryLimit));
|
|
158
|
+
if (santiApiKey) {
|
|
159
|
+
await configManager.set('santi.api_key', santiApiKey);
|
|
160
|
+
}
|
|
77
161
|
// Audio Configuration
|
|
78
162
|
const audioEnabled = await confirm({
|
|
79
163
|
message: 'Enable Audio Transcription? (Requires Gemini)',
|
|
@@ -152,6 +236,14 @@ export const initCommand = new Command('init')
|
|
|
152
236
|
await configManager.set('channels.telegram.allowedUsers', allowedUsers);
|
|
153
237
|
}
|
|
154
238
|
}
|
|
239
|
+
// Initialize Sati Memory (Long-term memory)
|
|
240
|
+
try {
|
|
241
|
+
SatiRepository.getInstance().initialize();
|
|
242
|
+
display.log(chalk.green('Long-term memory initialized.'));
|
|
243
|
+
}
|
|
244
|
+
catch (e) {
|
|
245
|
+
display.log(chalk.yellow(`Warning: Could not initialize long-term memory: ${e.message}`));
|
|
246
|
+
}
|
|
155
247
|
display.log(chalk.green('\nConfiguration saved successfully!'));
|
|
156
248
|
display.log(chalk.cyan(`Run 'morpheus start' to launch ${name}.`));
|
|
157
249
|
}
|
|
@@ -11,6 +11,7 @@ import { PATHS } from '../../config/paths.js';
|
|
|
11
11
|
import { Oracle } from '../../runtime/oracle.js';
|
|
12
12
|
import { ProviderError } from '../../runtime/errors.js';
|
|
13
13
|
import { HttpServer } from '../../http/server.js';
|
|
14
|
+
import { getVersion } from '../utils/version.js';
|
|
14
15
|
export const startCommand = new Command('start')
|
|
15
16
|
.description('Start the Morpheus agent')
|
|
16
17
|
.option('--ui', 'Enable web UI', true)
|
|
@@ -19,7 +20,7 @@ export const startCommand = new Command('start')
|
|
|
19
20
|
.action(async (options) => {
|
|
20
21
|
const display = DisplayManager.getInstance();
|
|
21
22
|
try {
|
|
22
|
-
renderBanner();
|
|
23
|
+
renderBanner(getVersion());
|
|
23
24
|
await scaffold(); // Ensure env exists
|
|
24
25
|
// Cleanup stale PID first
|
|
25
26
|
await checkStalePid();
|
package/dist/cli/index.js
CHANGED
|
@@ -1,7 +1,4 @@
|
|
|
1
1
|
import { Command } from 'commander';
|
|
2
|
-
import { readFileSync } from 'fs';
|
|
3
|
-
import { fileURLToPath } from 'url';
|
|
4
|
-
import { dirname, join } from 'path';
|
|
5
2
|
import { startCommand } from './commands/start.js';
|
|
6
3
|
import { stopCommand } from './commands/stop.js';
|
|
7
4
|
import { statusCommand } from './commands/status.js';
|
|
@@ -9,20 +6,7 @@ import { configCommand } from './commands/config.js';
|
|
|
9
6
|
import { doctorCommand } from './commands/doctor.js';
|
|
10
7
|
import { initCommand } from './commands/init.js';
|
|
11
8
|
import { scaffold } from '../runtime/scaffold.js';
|
|
12
|
-
|
|
13
|
-
const getVersion = () => {
|
|
14
|
-
try {
|
|
15
|
-
const __filename = fileURLToPath(import.meta.url);
|
|
16
|
-
const __dirname = dirname(__filename);
|
|
17
|
-
// Assuming dist/cli/index.js -> package.json is 2 levels up
|
|
18
|
-
const pkgPath = join(__dirname, '../../package.json');
|
|
19
|
-
const pkg = JSON.parse(readFileSync(pkgPath, 'utf-8'));
|
|
20
|
-
return pkg.version;
|
|
21
|
-
}
|
|
22
|
-
catch (e) {
|
|
23
|
-
return '0.1.0';
|
|
24
|
-
}
|
|
25
|
-
};
|
|
9
|
+
import { getVersion } from './utils/version.js';
|
|
26
10
|
export async function cli() {
|
|
27
11
|
const program = new Command();
|
|
28
12
|
program
|
package/dist/cli/utils/render.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import chalk from 'chalk';
|
|
2
2
|
import figlet from 'figlet';
|
|
3
|
-
export function renderBanner() {
|
|
3
|
+
export function renderBanner(version) {
|
|
4
4
|
const art = figlet.textSync('Morpheus', {
|
|
5
5
|
font: 'Standard',
|
|
6
6
|
horizontalLayout: 'default',
|
|
@@ -8,4 +8,5 @@ export function renderBanner() {
|
|
|
8
8
|
});
|
|
9
9
|
console.log(chalk.cyanBright(art));
|
|
10
10
|
console.log(chalk.gray(' The Local-First AI Agent specialized in Coding\n'));
|
|
11
|
+
console.log(chalk.gray(` v${version || 'unknown'}\n`));
|
|
11
12
|
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { readFileSync } from 'fs';
|
|
2
|
+
import { dirname, join } from 'path';
|
|
3
|
+
import { fileURLToPath } from 'url';
|
|
4
|
+
export const getVersion = () => {
|
|
5
|
+
try {
|
|
6
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
7
|
+
const __dirname = dirname(__filename);
|
|
8
|
+
// Assuming dist/cli/index.js -> package.json is 2 levels up
|
|
9
|
+
const pkgPath = join(__dirname, '../../../package.json');
|
|
10
|
+
const pkg = JSON.parse(readFileSync(pkgPath, 'utf-8'));
|
|
11
|
+
return pkg.version;
|
|
12
|
+
}
|
|
13
|
+
catch (e) {
|
|
14
|
+
return '0.1.0';
|
|
15
|
+
}
|
|
16
|
+
};
|
package/dist/config/manager.js
CHANGED
|
@@ -54,4 +54,20 @@ export class ConfigManager {
|
|
|
54
54
|
await fs.writeFile(PATHS.config, yaml.dump(valid), 'utf8');
|
|
55
55
|
this.config = valid;
|
|
56
56
|
}
|
|
57
|
+
getLLMConfig() {
|
|
58
|
+
return this.config.llm;
|
|
59
|
+
}
|
|
60
|
+
getSantiConfig() {
|
|
61
|
+
if (this.config.santi) {
|
|
62
|
+
return {
|
|
63
|
+
memory_limit: 10, // Default if undefined
|
|
64
|
+
...this.config.santi
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
// Fallback to main LLM config
|
|
68
|
+
return {
|
|
69
|
+
...this.config.llm,
|
|
70
|
+
memory_limit: 10 // Default fallback
|
|
71
|
+
};
|
|
72
|
+
}
|
|
57
73
|
}
|
package/dist/config/schemas.js
CHANGED
|
@@ -7,22 +7,28 @@ export const AudioConfigSchema = z.object({
|
|
|
7
7
|
maxDurationSeconds: z.number().default(DEFAULT_CONFIG.audio.maxDurationSeconds),
|
|
8
8
|
supportedMimeTypes: z.array(z.string()).default(DEFAULT_CONFIG.audio.supportedMimeTypes),
|
|
9
9
|
});
|
|
10
|
+
export const LLMConfigSchema = z.object({
|
|
11
|
+
provider: z.enum(['openai', 'anthropic', 'ollama', 'gemini']).default(DEFAULT_CONFIG.llm.provider),
|
|
12
|
+
model: z.string().min(1).default(DEFAULT_CONFIG.llm.model),
|
|
13
|
+
temperature: z.number().min(0).max(1).default(DEFAULT_CONFIG.llm.temperature),
|
|
14
|
+
max_tokens: z.number().int().positive().optional(),
|
|
15
|
+
api_key: z.string().optional(),
|
|
16
|
+
context_window: z.number().int().positive().optional(),
|
|
17
|
+
});
|
|
18
|
+
export const SantiConfigSchema = LLMConfigSchema.extend({
|
|
19
|
+
memory_limit: z.number().int().positive().optional(),
|
|
20
|
+
});
|
|
10
21
|
// Zod Schema matching MorpheusConfig interface
|
|
11
22
|
export const ConfigSchema = z.object({
|
|
12
23
|
agent: z.object({
|
|
13
24
|
name: z.string().default(DEFAULT_CONFIG.agent.name),
|
|
14
25
|
personality: z.string().default(DEFAULT_CONFIG.agent.personality),
|
|
15
26
|
}).default(DEFAULT_CONFIG.agent),
|
|
16
|
-
llm:
|
|
17
|
-
|
|
18
|
-
model: z.string().min(1).default(DEFAULT_CONFIG.llm.model),
|
|
19
|
-
temperature: z.number().min(0).max(1).default(DEFAULT_CONFIG.llm.temperature),
|
|
20
|
-
max_tokens: z.number().int().positive().optional(),
|
|
21
|
-
api_key: z.string().optional(),
|
|
22
|
-
}).default(DEFAULT_CONFIG.llm),
|
|
27
|
+
llm: LLMConfigSchema.default(DEFAULT_CONFIG.llm),
|
|
28
|
+
santi: SantiConfigSchema.optional(),
|
|
23
29
|
audio: AudioConfigSchema.default(DEFAULT_CONFIG.audio),
|
|
24
30
|
memory: z.object({
|
|
25
|
-
limit: z.number().int().positive().
|
|
31
|
+
limit: z.number().int().positive().optional(),
|
|
26
32
|
}).default(DEFAULT_CONFIG.memory),
|
|
27
33
|
channels: z.object({
|
|
28
34
|
telegram: z.object({
|
|
@@ -56,6 +62,7 @@ export const MCPServerConfigSchema = z.discriminatedUnion('transport', [
|
|
|
56
62
|
z.object({
|
|
57
63
|
transport: z.literal('http'),
|
|
58
64
|
url: z.string().url('Valid URL is required for http transport'),
|
|
65
|
+
headers: z.record(z.string(), z.string()).optional().default({}),
|
|
59
66
|
args: z.array(z.string()).optional().default([]),
|
|
60
67
|
env: z.record(z.string(), z.string()).optional().default({}),
|
|
61
68
|
_comment: z.string().optional(),
|
package/dist/http/api.js
CHANGED
|
@@ -113,6 +113,52 @@ export function createApiRouter() {
|
|
|
113
113
|
}
|
|
114
114
|
}
|
|
115
115
|
});
|
|
116
|
+
// Sati config endpoints
|
|
117
|
+
router.get('/config/sati', (req, res) => {
|
|
118
|
+
try {
|
|
119
|
+
const satiConfig = configManager.getSantiConfig();
|
|
120
|
+
res.json(satiConfig);
|
|
121
|
+
}
|
|
122
|
+
catch (error) {
|
|
123
|
+
res.status(500).json({ error: error.message });
|
|
124
|
+
}
|
|
125
|
+
});
|
|
126
|
+
router.post('/config/sati', async (req, res) => {
|
|
127
|
+
try {
|
|
128
|
+
const config = configManager.get();
|
|
129
|
+
await configManager.save({ ...config, santi: req.body });
|
|
130
|
+
const display = DisplayManager.getInstance();
|
|
131
|
+
display.log('Sati configuration updated via UI', {
|
|
132
|
+
source: 'Zaion',
|
|
133
|
+
level: 'info'
|
|
134
|
+
});
|
|
135
|
+
res.json({ success: true });
|
|
136
|
+
}
|
|
137
|
+
catch (error) {
|
|
138
|
+
if (error.name === 'ZodError') {
|
|
139
|
+
res.status(400).json({ error: 'Validation failed', details: error.errors });
|
|
140
|
+
}
|
|
141
|
+
else {
|
|
142
|
+
res.status(500).json({ error: error.message });
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
});
|
|
146
|
+
router.delete('/config/sati', async (req, res) => {
|
|
147
|
+
try {
|
|
148
|
+
const config = configManager.get();
|
|
149
|
+
const { santi, ...restConfig } = config;
|
|
150
|
+
await configManager.save(restConfig);
|
|
151
|
+
const display = DisplayManager.getInstance();
|
|
152
|
+
display.log('Sati configuration removed via UI (falling back to Oracle config)', {
|
|
153
|
+
source: 'Zaion',
|
|
154
|
+
level: 'info'
|
|
155
|
+
});
|
|
156
|
+
res.json({ success: true });
|
|
157
|
+
}
|
|
158
|
+
catch (error) {
|
|
159
|
+
res.status(500).json({ error: error.message });
|
|
160
|
+
}
|
|
161
|
+
});
|
|
116
162
|
// Keep PUT for backward compatibility if needed, or remove.
|
|
117
163
|
// Tasks says Implement POST. I'll remove PUT to avoid confusion or redirect it.
|
|
118
164
|
router.put('/config', async (req, res) => {
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { Santi } from "../santi/santi.js";
|
|
2
|
+
import { ConfigManager } from "../../config/manager.js";
|
|
3
|
+
import * as path from "path";
|
|
4
|
+
import { homedir } from "os";
|
|
5
|
+
async function main() {
|
|
6
|
+
console.log("Starting Santi Manual Verification...");
|
|
7
|
+
// 1. Check DB isolation
|
|
8
|
+
const santiDbPath = path.join(homedir(), ".morpheus", "memory", "santi-memory.db");
|
|
9
|
+
const shortDbPath = path.join(homedir(), ".morpheus", "memory", "short-memory.db");
|
|
10
|
+
console.log(`Checking DB paths: \n- ${santiDbPath}\n- ${shortDbPath}`);
|
|
11
|
+
// 2. Initialize Santi
|
|
12
|
+
const santi = new Santi();
|
|
13
|
+
console.log("Santi initialized.");
|
|
14
|
+
// 3. Test Recovery (should be empty or have data if prev run)
|
|
15
|
+
let memories = await santi.recover("test");
|
|
16
|
+
console.log(`Initial recovery "test": ${memories.length} items`);
|
|
17
|
+
// 4. Manually add memory via internal store (reflection hack for test)
|
|
18
|
+
console.log("Injecting test memory...");
|
|
19
|
+
// @ts-ignore
|
|
20
|
+
santi.store.addMemory({
|
|
21
|
+
category: 'preference',
|
|
22
|
+
importance: 'high',
|
|
23
|
+
summary: 'The user loves manual testing scripts.',
|
|
24
|
+
hash: 'manual-test-hash',
|
|
25
|
+
source: 'manual_test'
|
|
26
|
+
});
|
|
27
|
+
// 5. Test Recovery again
|
|
28
|
+
memories = await santi.recover("loves manual testing");
|
|
29
|
+
console.log(`Recovery after injection "loves manual testing": ${memories.length} items`);
|
|
30
|
+
if (memories.length > 0 && memories[0].summary.includes("loves manual testing")) {
|
|
31
|
+
console.log("✅ Recovery SUCCESS");
|
|
32
|
+
}
|
|
33
|
+
else {
|
|
34
|
+
console.error("❌ Recovery FAILED");
|
|
35
|
+
}
|
|
36
|
+
// 6. Test Evaluate (Mocking messages)
|
|
37
|
+
// This requires LLM config to be present.
|
|
38
|
+
// We skip this in manual test if no config, but logging will show warning.
|
|
39
|
+
if (ConfigManager.getInstance().get().llm) {
|
|
40
|
+
console.log("Testing Evaluate (dry run with LLM)...");
|
|
41
|
+
// This will assume valid LLM config
|
|
42
|
+
/*
|
|
43
|
+
await santi.evaluate([
|
|
44
|
+
new HumanMessage("My name is Morpheus Tester."),
|
|
45
|
+
new AIMessage("Nice to meet you.")
|
|
46
|
+
]);
|
|
47
|
+
*/
|
|
48
|
+
console.log("Evaluate test skipped to avoid api cost, but code is in place.");
|
|
49
|
+
}
|
|
50
|
+
else {
|
|
51
|
+
console.log("Skipping Evaluate test (no LLM config).");
|
|
52
|
+
}
|
|
53
|
+
console.log("Verification Complete.");
|
|
54
|
+
}
|
|
55
|
+
main().catch(console.error);
|
package/dist/runtime/display.js
CHANGED
|
@@ -80,6 +80,9 @@ export class DisplayManager {
|
|
|
80
80
|
else if (options.source === 'Oracle') {
|
|
81
81
|
color = chalk.hex('#FFA500');
|
|
82
82
|
}
|
|
83
|
+
else if (options.source === 'Sati') {
|
|
84
|
+
color = chalk.hex('#00ff22');
|
|
85
|
+
}
|
|
83
86
|
else if (options.source === 'Telephonist') {
|
|
84
87
|
color = chalk.hex('#b902b9');
|
|
85
88
|
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
|
2
|
+
import { SatiRepository } from '../repository.js';
|
|
3
|
+
import fs from 'fs-extra';
|
|
4
|
+
import path from 'path';
|
|
5
|
+
describe('SatiRepository', () => {
|
|
6
|
+
const dbPath = path.join(process.cwd(), 'test-memory.db');
|
|
7
|
+
let repo;
|
|
8
|
+
beforeEach(() => {
|
|
9
|
+
// Reset singleton instance for testing
|
|
10
|
+
SatiRepository.instance = null;
|
|
11
|
+
repo = SatiRepository.getInstance(dbPath);
|
|
12
|
+
repo.initialize();
|
|
13
|
+
});
|
|
14
|
+
afterEach(() => {
|
|
15
|
+
try {
|
|
16
|
+
repo.close();
|
|
17
|
+
}
|
|
18
|
+
catch (e) { }
|
|
19
|
+
if (fs.existsSync(dbPath)) {
|
|
20
|
+
try {
|
|
21
|
+
fs.unlinkSync(dbPath);
|
|
22
|
+
fs.rmdirSync(path.dirname(dbPath)); // cleanup dir if possible/empty
|
|
23
|
+
}
|
|
24
|
+
catch (e) { }
|
|
25
|
+
}
|
|
26
|
+
});
|
|
27
|
+
it('should save and retrieve memory', async () => {
|
|
28
|
+
const mem = {
|
|
29
|
+
category: 'preference',
|
|
30
|
+
importance: 'high',
|
|
31
|
+
summary: 'Test Memory',
|
|
32
|
+
hash: '123',
|
|
33
|
+
source: 'test'
|
|
34
|
+
};
|
|
35
|
+
await repo.save(mem);
|
|
36
|
+
const result = repo.findByHash('123');
|
|
37
|
+
expect(result).not.toBeNull();
|
|
38
|
+
expect(result?.summary).toBe('Test Memory');
|
|
39
|
+
});
|
|
40
|
+
it('should deduplicate (update) on hash collision', async () => {
|
|
41
|
+
const mem1 = {
|
|
42
|
+
category: 'preference',
|
|
43
|
+
importance: 'medium',
|
|
44
|
+
summary: 'Test Memory v1',
|
|
45
|
+
hash: 'abc',
|
|
46
|
+
source: 'test'
|
|
47
|
+
};
|
|
48
|
+
await repo.save(mem1);
|
|
49
|
+
const mem2 = {
|
|
50
|
+
category: 'preference',
|
|
51
|
+
importance: 'high',
|
|
52
|
+
summary: 'Test Memory v2', // Summary logic might not update summary field in my query
|
|
53
|
+
// Let's check my query in repository.ts:
|
|
54
|
+
// ON CONFLICT(hash) DO UPDATE SET importance..., details... but NOT summary.
|
|
55
|
+
// If hash is same, summary implies same content usually.
|
|
56
|
+
// But here I test that fields ARE updated.
|
|
57
|
+
hash: 'abc',
|
|
58
|
+
source: 'test',
|
|
59
|
+
details: 'updated details'
|
|
60
|
+
};
|
|
61
|
+
await repo.save(mem2);
|
|
62
|
+
const result = repo.findByHash('abc');
|
|
63
|
+
expect(result).not.toBeNull();
|
|
64
|
+
expect(result?.importance).toBe('high');
|
|
65
|
+
expect(result?.details).toBe('updated details');
|
|
66
|
+
// Since my query did not update summary (it assumed hash=summary match), summary should stay v1.
|
|
67
|
+
expect(result?.summary).toBe('Test Memory v1');
|
|
68
|
+
// Access count should increment
|
|
69
|
+
expect(result?.access_count).toBeGreaterThan(0);
|
|
70
|
+
});
|
|
71
|
+
});
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import { vi, describe, it, expect, beforeEach } from 'vitest';
|
|
2
|
+
import { SatiService } from '../service.js';
|
|
3
|
+
import { SatiRepository } from '../repository.js';
|
|
4
|
+
import { ProviderFactory } from '../../../providers/factory.js';
|
|
5
|
+
// Mock ConfigManager
|
|
6
|
+
vi.mock('../../../../config/manager.js', () => ({
|
|
7
|
+
ConfigManager: {
|
|
8
|
+
getInstance: vi.fn().mockReturnValue({
|
|
9
|
+
get: vi.fn().mockReturnValue({ llm: { provider: 'test' } })
|
|
10
|
+
})
|
|
11
|
+
}
|
|
12
|
+
}));
|
|
13
|
+
// Mock ProviderFactory
|
|
14
|
+
vi.mock('../../../providers/factory.js', () => ({
|
|
15
|
+
ProviderFactory: {
|
|
16
|
+
create: vi.fn()
|
|
17
|
+
}
|
|
18
|
+
}));
|
|
19
|
+
// Mock the repository module
|
|
20
|
+
vi.mock('../repository.js', () => {
|
|
21
|
+
const SatiRepositoryMock = {
|
|
22
|
+
getInstance: vi.fn(),
|
|
23
|
+
};
|
|
24
|
+
return { SatiRepository: SatiRepositoryMock };
|
|
25
|
+
});
|
|
26
|
+
describe('SatiService', () => {
|
|
27
|
+
let service;
|
|
28
|
+
let mockRepo;
|
|
29
|
+
let mockAgent;
|
|
30
|
+
beforeEach(() => {
|
|
31
|
+
vi.resetAllMocks();
|
|
32
|
+
// Setup mock repository instance
|
|
33
|
+
mockRepo = {
|
|
34
|
+
initialize: vi.fn(),
|
|
35
|
+
search: vi.fn(),
|
|
36
|
+
save: vi.fn(),
|
|
37
|
+
getAllMemories: vi.fn().mockReturnValue([]),
|
|
38
|
+
};
|
|
39
|
+
SatiRepository.getInstance.mockReturnValue(mockRepo);
|
|
40
|
+
// Setup mock agent
|
|
41
|
+
mockAgent = {
|
|
42
|
+
invoke: vi.fn()
|
|
43
|
+
};
|
|
44
|
+
ProviderFactory.create.mockResolvedValue(mockAgent);
|
|
45
|
+
service = SatiService.getInstance();
|
|
46
|
+
service.repository = mockRepo;
|
|
47
|
+
});
|
|
48
|
+
describe('recover', () => {
|
|
49
|
+
it('should recover memories calling repository with limit', async () => {
|
|
50
|
+
mockRepo.search.mockReturnValue([
|
|
51
|
+
{ summary: 'Memory 1', category: 'preference', importance: 'high' },
|
|
52
|
+
{ summary: 'Memory 2', category: 'project', importance: 'medium' }
|
|
53
|
+
]);
|
|
54
|
+
const result = await service.recover('hello world', []);
|
|
55
|
+
expect(mockRepo.search).toHaveBeenCalledWith('hello world', 5);
|
|
56
|
+
expect(result.relevant_memories).toHaveLength(2);
|
|
57
|
+
expect(result.relevant_memories[0].summary).toBe('Memory 1');
|
|
58
|
+
});
|
|
59
|
+
it('should return empty list when no memories found', async () => {
|
|
60
|
+
mockRepo.search.mockReturnValue([]);
|
|
61
|
+
const result = await service.recover('unknown', []);
|
|
62
|
+
expect(mockRepo.search).toHaveBeenCalledWith('unknown', 5);
|
|
63
|
+
expect(result.relevant_memories).toEqual([]);
|
|
64
|
+
});
|
|
65
|
+
});
|
|
66
|
+
describe('evaluateAndPersist', () => {
|
|
67
|
+
it('should parse LLM response and persist memory', async () => {
|
|
68
|
+
mockAgent.invoke.mockResolvedValue({
|
|
69
|
+
messages: [{
|
|
70
|
+
content: JSON.stringify({
|
|
71
|
+
should_store: true,
|
|
72
|
+
category: 'preference',
|
|
73
|
+
importance: 'high',
|
|
74
|
+
summary: 'User likes TypeScript',
|
|
75
|
+
reason: 'User stated preference'
|
|
76
|
+
})
|
|
77
|
+
}]
|
|
78
|
+
});
|
|
79
|
+
await service.evaluateAndPersist([{ role: 'user', content: 'I like TypeScript' }]);
|
|
80
|
+
expect(mockRepo.save).toHaveBeenCalledWith(expect.objectContaining({
|
|
81
|
+
summary: 'User likes TypeScript',
|
|
82
|
+
category: 'preference',
|
|
83
|
+
importance: 'high'
|
|
84
|
+
}));
|
|
85
|
+
});
|
|
86
|
+
it('should not persist if should_store is false', async () => {
|
|
87
|
+
mockAgent.invoke.mockResolvedValue({
|
|
88
|
+
messages: [{
|
|
89
|
+
content: JSON.stringify({
|
|
90
|
+
should_store: false,
|
|
91
|
+
reason: 'Chit chat'
|
|
92
|
+
})
|
|
93
|
+
}]
|
|
94
|
+
});
|
|
95
|
+
await service.evaluateAndPersist([{ role: 'user', content: 'Hi' }]);
|
|
96
|
+
expect(mockRepo.save).not.toHaveBeenCalled();
|
|
97
|
+
});
|
|
98
|
+
});
|
|
99
|
+
});
|