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.
Files changed (33) hide show
  1. package/README.md +14 -5
  2. package/dist/cli/commands/doctor.js +36 -1
  3. package/dist/cli/commands/init.js +92 -0
  4. package/dist/cli/commands/start.js +2 -1
  5. package/dist/cli/index.js +1 -17
  6. package/dist/cli/utils/render.js +2 -1
  7. package/dist/cli/utils/version.js +16 -0
  8. package/dist/config/manager.js +16 -0
  9. package/dist/config/schemas.js +15 -8
  10. package/dist/http/api.js +46 -0
  11. package/dist/runtime/__tests__/manual_santi_verify.js +55 -0
  12. package/dist/runtime/display.js +3 -0
  13. package/dist/runtime/memory/sati/__tests__/repository.test.js +71 -0
  14. package/dist/runtime/memory/sati/__tests__/service.test.js +99 -0
  15. package/dist/runtime/memory/sati/index.js +59 -0
  16. package/dist/runtime/memory/sati/repository.js +219 -0
  17. package/dist/runtime/memory/sati/service.js +142 -0
  18. package/dist/runtime/memory/sati/system-prompts.js +40 -0
  19. package/dist/runtime/memory/sati/types.js +1 -0
  20. package/dist/runtime/memory/sqlite.js +5 -1
  21. package/dist/runtime/migration.js +53 -1
  22. package/dist/runtime/oracle.js +32 -7
  23. package/dist/runtime/santi/contracts.js +1 -0
  24. package/dist/runtime/santi/middleware.js +61 -0
  25. package/dist/runtime/santi/santi.js +109 -0
  26. package/dist/runtime/santi/store.js +158 -0
  27. package/dist/runtime/tools/factory.js +31 -25
  28. package/dist/types/config.js +1 -0
  29. package/dist/ui/assets/index-Ddyo4FWH.js +50 -0
  30. package/dist/ui/assets/{index-AEbYNHuy.css → index-tz0YVye-.css} +1 -1
  31. package/dist/ui/index.html +2 -2
  32. package/package.json +3 -3
  33. package/dist/ui/assets/index-BjnI8c1U.js +0 -50
@@ -0,0 +1,61 @@
1
+ import { createMiddleware } from "langchain";
2
+ import { SystemMessage } from "@langchain/core/messages";
3
+ import { DisplayManager } from "../display.js";
4
+ // T009: Create skeleton
5
+ export const createSantiMiddleware = (santi) => {
6
+ const display = DisplayManager.getInstance();
7
+ return createMiddleware({
8
+ name: "SatiMemoryMiddleware",
9
+ // T010: beforeAgent hook
10
+ async beforeAgent(state) {
11
+ try {
12
+ const messages = state.messages;
13
+ if (!messages || messages.length === 0)
14
+ return;
15
+ // Extract last user message
16
+ const lastMessage = messages[messages.length - 1];
17
+ if (lastMessage._getType() !== "human")
18
+ return;
19
+ // Recover memories
20
+ const memories = await santi.recover(lastMessage.content);
21
+ if (memories.length > 0) {
22
+ const memoryText = memories.map(m => `- [${m.category.toUpperCase()}] ${m.summary}`).join("\n");
23
+ const systemMsg = new SystemMessage(`Relevant long-term memory about the Architect:\n${memoryText}`);
24
+ // Inject into state logic - typically by appending to messages or inserting
25
+ // Since this is "beforeAgent", modifying state.messages usually propagates to the agent input
26
+ // We need to check if 'state' is mutable or if we return a diff.
27
+ // In standard LangGraph/LangChain middleware, we might return an update.
28
+ // Spec example: "return;" implies mutation of 'state' or side-effect?
29
+ // Spec: "The injection... should occur adding a SystemMessage to the array state.messages"
30
+ // Assuming direct mutation as per common JS middleware patterns if not specified otherwise
31
+ state.messages.push(systemMsg);
32
+ display.log('Injected long-term memories into context', { source: 'SatiMiddleware' });
33
+ }
34
+ }
35
+ catch (error) {
36
+ display.log(`Error in beforeAgent: ${error}`, { level: "error", source: 'SatiMiddleware' });
37
+ }
38
+ },
39
+ // T013: afterAgent hook
40
+ async afterAgent(state) {
41
+ try {
42
+ const messages = state.messages;
43
+ // We interact with the *result* of the agent.
44
+ // 'state' usually contains the full history at this point.
45
+ // We pass the full recent history to evaluate.
46
+ if (messages && messages.length > 0) {
47
+ // We fire and forget this to not block the response?
48
+ // Or await it? Middleware in LangGraph might be blocking.
49
+ // Given performance goals (<2s), and that this is post-generation, blocking is safer to ensure data consistency
50
+ // but user might perceive latency.
51
+ // Spec says "afterAgent... Persist if necessary".
52
+ // We'll await it.
53
+ await santi.evaluate(messages);
54
+ }
55
+ }
56
+ catch (error) {
57
+ display.log(`Error in afterAgent: ${error}`, { level: "error", source: 'SatiMiddleware' });
58
+ }
59
+ }
60
+ });
61
+ };
@@ -0,0 +1,109 @@
1
+ import { HumanMessage } from "@langchain/core/messages";
2
+ import { SantiStore } from "./store.js";
3
+ import { DisplayManager } from "../display.js";
4
+ import { ConfigManager } from "../../config/manager.js";
5
+ import { ProviderFactory } from "../providers/factory.js";
6
+ import * as crypto from 'crypto';
7
+ export class Santi {
8
+ store;
9
+ display = DisplayManager.getInstance();
10
+ constructor() {
11
+ this.store = new SantiStore();
12
+ }
13
+ // T007 & T008: Recover implementation
14
+ async recover(message, limit = 5) {
15
+ this.display.log('Recovering memories...', { source: 'Sati' });
16
+ // 1. Keyword extraction (simple heuristic: words > 3 chars)
17
+ // Ideally this could be an LLM call to generate search queries, but for now we stick to the plan/store logic.
18
+ // If we want to be smarter, we could ask the LLM "Extract search keywords from this message".
19
+ // For MVP, direct search is faster and meets the "textual search" constraint.
20
+ // T008: Fetch from store
21
+ const memories = this.store.searchMemories(message, limit);
22
+ if (memories.length > 0) {
23
+ this.display.log(`Recovered ${memories.length} memories`, { source: 'Sati' });
24
+ }
25
+ return memories;
26
+ }
27
+ // T012: Evaluate implementation
28
+ async evaluate(recentMessages) {
29
+ if (recentMessages.length < 2)
30
+ return; // Need at least user + AI response to evaluate context
31
+ this.display.log('Evaluating interaction for memory persistence...', { source: 'Sati' });
32
+ try {
33
+ const config = ConfigManager.getInstance().get().llm;
34
+ if (!config) {
35
+ this.display.log('LLM config missing, skipping memory evaluation.', { level: "warning", source: 'Sati' });
36
+ return;
37
+ }
38
+ const model = ProviderFactory.createModel(config);
39
+ // Format conversation for prompt
40
+ const conversationText = recentMessages.map(m => {
41
+ const role = m._getType() === 'human' ? 'User' : 'Assistant';
42
+ const content = typeof m.content === 'string' ? m.content : JSON.stringify(m.content);
43
+ return `${role}: ${content}`;
44
+ }).join('\n');
45
+ const prompt = `
46
+ You are Sati, guardian of the long-term memory system.
47
+ Analyze the interaction below between the Architect (User) and the System.
48
+
49
+ Decide if there is any information that should be stored as long-term memory.
50
+ Store ONLY:
51
+ - Persistent preferences
52
+ - Architectural decisions
53
+ - Permanent constraints
54
+ - Project context
55
+ - Personal identity (non-sensitive)
56
+ - Favorite languages/tech
57
+ - Relationships/Pets/Names
58
+
59
+ IGNORE:
60
+ - One-off questions
61
+ - Temporary context
62
+ - Redundant info
63
+ - SENSITIVE DATA (API Keys, Secrets, Passwords) - NEVER STORE THESE.
64
+
65
+ Format output as JSON:
66
+ {
67
+ "should_store": boolean,
68
+ "category": "preference" | "project" | "identity" | "constraint" | "context" | "personal_data" | "languages" | "favorite_things" | "relationships" | "pets" | "naming" | "professional_profile",
69
+ "importance": "low" | "medium" | "high",
70
+ "summary": "Concise summary",
71
+ "reason": "Why this is important"
72
+ }
73
+
74
+ Interaction:
75
+ ${conversationText}
76
+ `;
77
+ const response = await model.invoke([new HumanMessage(prompt)]);
78
+ const content = typeof response.content === 'string' ? response.content : JSON.stringify(response.content);
79
+ // Parse JSON (handle potential markdown code blocks)
80
+ const jsonMatch = content.match(/\{[\s\S]*\}/);
81
+ if (!jsonMatch) {
82
+ return; // No JSON found
83
+ }
84
+ const result = JSON.parse(jsonMatch[0]);
85
+ if (result.should_store && result.summary && result.category && result.importance) {
86
+ // T015: Privacy Filter (Regex check)
87
+ const sensitiveRegex = /(sk-[a-zA-Z0-9]{20,}|eyJ[a-zA-Z0-9]{20,}|password|secret|[0-9a-f]{32,})/;
88
+ if (sensitiveRegex.test(result.summary)) {
89
+ this.display.log('Sensitive data detected in memory summary. Aborting persistence.', { level: 'warning', source: 'Sati' });
90
+ return;
91
+ }
92
+ // Generate hash for deduplication
93
+ const hash = crypto.createHash('sha256').update(result.summary.toLowerCase().trim()).digest('hex');
94
+ // T014: Persist
95
+ const memory = this.store.addMemory({
96
+ category: result.category,
97
+ importance: result.importance,
98
+ summary: result.summary,
99
+ hash: hash,
100
+ source: 'conversation_evaluation'
101
+ });
102
+ this.display.log(`Persisted new memory: ${memory.summary.substring(0, 50)}...`, { source: 'Sati' });
103
+ }
104
+ }
105
+ catch (err) {
106
+ this.display.log(`Evaluation failed: ${err}`, { level: "error", source: 'Sati' });
107
+ }
108
+ }
109
+ }
@@ -0,0 +1,158 @@
1
+ import Database from "better-sqlite3";
2
+ import * as fs from "fs-extra";
3
+ import * as path from "path";
4
+ import { homedir } from "os";
5
+ import { v4 as uuidv4 } from 'uuid';
6
+ export class SantiStore {
7
+ db;
8
+ constructor(databasePath) {
9
+ const dbPath = databasePath || path.join(homedir(), ".morpheus", "memory", "santi-memory.db");
10
+ this.ensureDirectory(dbPath);
11
+ this.db = new Database(dbPath, { timeout: 5000 });
12
+ this.init();
13
+ }
14
+ ensureDirectory(filePath) {
15
+ const dir = path.dirname(filePath);
16
+ fs.ensureDirSync(dir);
17
+ }
18
+ init() {
19
+ // T003: Table creation
20
+ const schema = `
21
+ CREATE TABLE IF NOT EXISTS long_term_memory (
22
+ id TEXT PRIMARY KEY,
23
+ category TEXT NOT NULL CHECK(category IN ('preference','project','identity','constraint','context','personal_data','languages','favorite_things','relationships','pets','naming','professional_profile')),
24
+ importance TEXT NOT NULL CHECK(importance IN ('low','medium','high')),
25
+ summary TEXT NOT NULL,
26
+ details TEXT,
27
+ hash TEXT NOT NULL UNIQUE,
28
+ source TEXT,
29
+ created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
30
+ updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
31
+ last_accessed_at DATETIME,
32
+ access_count INTEGER DEFAULT 0,
33
+ version INTEGER DEFAULT 1,
34
+ archived BOOLEAN DEFAULT 0
35
+ );
36
+
37
+ CREATE INDEX IF NOT EXISTS idx_memory_category ON long_term_memory(category);
38
+ CREATE INDEX IF NOT EXISTS idx_memory_importance ON long_term_memory(importance);
39
+ CREATE INDEX IF NOT EXISTS idx_memory_archived ON long_term_memory(archived);
40
+ `;
41
+ this.db.exec(schema);
42
+ }
43
+ // T004: addMemory
44
+ addMemory(memory) {
45
+ // Check for existing hash
46
+ const existing = this.db.prepare('SELECT * FROM long_term_memory WHERE hash = ?').get(memory.hash);
47
+ if (existing) {
48
+ // If exact hash match, return existing (idempotent)
49
+ return existing;
50
+ }
51
+ const id = uuidv4();
52
+ const now = new Date().toISOString();
53
+ const newMemory = {
54
+ ...memory,
55
+ id,
56
+ details: memory.details || null, // Ensure explicit null if undefined
57
+ source: memory.source || null,
58
+ created_at: now,
59
+ updated_at: now,
60
+ access_count: 0,
61
+ version: 1,
62
+ archived: false
63
+ };
64
+ const stmt = this.db.prepare(`
65
+ INSERT INTO long_term_memory (
66
+ id, category, importance, summary, details, hash, source,
67
+ created_at, updated_at, access_count, version, archived
68
+ ) VALUES (
69
+ @id, @category, @importance, @summary, @details, @hash, @source,
70
+ @created_at, @updated_at, @access_count, @version, @archived
71
+ )
72
+ `);
73
+ const bindParams = {
74
+ ...newMemory,
75
+ archived: newMemory.archived ? 1 : 0
76
+ };
77
+ stmt.run(bindParams);
78
+ return newMemory;
79
+ }
80
+ // T005: searchMemories
81
+ searchMemories(query, limit = 5) {
82
+ // Basic text search using LIKE for now as per requirements (no vectors)
83
+ // We split query into keywords and search for any match
84
+ // Prioritizing importance and recency
85
+ const keywords = query.split(' ').filter(word => word.length > 3).map(w => `%${w}%`);
86
+ if (keywords.length === 0)
87
+ return [];
88
+ // Construct dynamic query for multiple keywords (OR logic for broader retrieval, refined by importance)
89
+ const conditions = keywords.map((_, i) => `summary LIKE ?`).join(' OR ');
90
+ const stmt = this.db.prepare(`
91
+ SELECT * FROM long_term_memory
92
+ WHERE archived = 0 AND (${conditions})
93
+ ORDER BY
94
+ CASE importance
95
+ WHEN 'high' THEN 1
96
+ WHEN 'medium' THEN 2
97
+ WHEN 'low' THEN 3
98
+ END ASC,
99
+ updated_at DESC
100
+ LIMIT ?
101
+ `);
102
+ const results = stmt.all(...keywords, limit);
103
+ // Update access stats
104
+ if (results.length > 0) {
105
+ this.updateAccessStats(results.map(r => r.id));
106
+ }
107
+ return results;
108
+ }
109
+ updateAccessStats(ids) {
110
+ const now = new Date().toISOString();
111
+ const stmt = this.db.prepare(`
112
+ UPDATE long_term_memory
113
+ SET access_count = access_count + 1, last_accessed_at = ?
114
+ WHERE id = ?
115
+ `);
116
+ const transaction = this.db.transaction((timestamp, memoryIds) => {
117
+ for (const id of memoryIds) {
118
+ stmt.run(timestamp, id);
119
+ }
120
+ });
121
+ transaction(now, ids);
122
+ }
123
+ // T006: updateMemory
124
+ updateMemory(id, data) {
125
+ const setClauses = [];
126
+ const params = [];
127
+ if (data.summary) {
128
+ setClauses.push("summary = ?");
129
+ params.push(data.summary);
130
+ }
131
+ if (data.details) {
132
+ setClauses.push("details = ?");
133
+ params.push(data.details);
134
+ }
135
+ if (data.importance) {
136
+ setClauses.push("importance = ?");
137
+ params.push(data.importance);
138
+ }
139
+ if (data.hash) {
140
+ setClauses.push("hash = ?");
141
+ params.push(data.hash);
142
+ }
143
+ if (setClauses.length === 0)
144
+ return;
145
+ setClauses.push("updated_at = CURRENT_TIMESTAMP");
146
+ setClauses.push("version = version + 1");
147
+ params.push(id); // Where ID
148
+ const stmt = this.db.prepare(`
149
+ UPDATE long_term_memory
150
+ SET ${setClauses.join(', ')}
151
+ WHERE id = ?
152
+ `);
153
+ stmt.run(...params);
154
+ }
155
+ getByHash(hash) {
156
+ return this.db.prepare('SELECT * FROM long_term_memory WHERE hash = ?').get(hash);
157
+ }
158
+ }
@@ -29,7 +29,7 @@ function sanitizeSchema(obj) {
29
29
  * Creates a proxy that intercepts schema access and sanitizes the output.
30
30
  */
31
31
  function wrapToolWithSanitizedSchema(tool) {
32
- display.log('Tool loaded: - ' + tool.name, { source: 'Construtor' });
32
+ // display.log('Tool loaded: - '+ tool.name, { source: 'Construtor' });
33
33
  // The MCP tools have a schema property that returns JSON Schema
34
34
  // We need to intercept and sanitize it
35
35
  const originalSchema = tool.schema;
@@ -45,34 +45,40 @@ export class Construtor {
45
45
  const display = DisplayManager.getInstance();
46
46
  const mcpServers = await loadMCPConfig();
47
47
  const serverCount = Object.keys(mcpServers).length;
48
+ // console.log(mcpServers);
48
49
  if (serverCount === 0) {
49
50
  display.log('No MCP servers configured in mcps.json', { level: 'info', source: 'Construtor' });
50
51
  return [];
51
52
  }
52
- const client = new MultiServerMCPClient({
53
- mcpServers: mcpServers,
54
- onConnectionError: "ignore",
55
- // log the MCP client's internal events
56
- // beforeToolCall: ({ serverName, name, args }) => {
57
- // display.log(`MCP Tool Call - Server: ${serverName}, Tool: ${name}, Args: ${JSON.stringify(args)}`, { source: 'MCPServer' });
58
- // return;
59
- // },
60
- // // log the results of tool calls
61
- // afterToolCall: (res) => {
62
- // display.log(`MCP Tool Result - ${JSON.stringify(res)}`, { source: 'MCPServer' });
63
- // return;
64
- // }
65
- });
66
- try {
67
- const tools = await client.getTools();
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: 'Construtor' });
71
- return sanitizedTools;
72
- }
73
- catch (error) {
74
- display.log(`Failed to initialize MCP tools: ${error}`, { level: 'warning', source: 'Construtor' });
75
- return []; // Return empty tools on failure to allow agent to start
53
+ const allTools = [];
54
+ // Create a client for each server to handle tool naming conflicts
55
+ for (const [serverName, serverConfig] of Object.entries(mcpServers)) {
56
+ const client = new MultiServerMCPClient({
57
+ mcpServers: {
58
+ [serverName]: serverConfig
59
+ },
60
+ onConnectionError: "ignore",
61
+ });
62
+ try {
63
+ const tools = await client.getTools();
64
+ // Rename tools to include server prefix to avoid collisions
65
+ tools.forEach(tool => {
66
+ const originalName = tool.name;
67
+ const newName = `${serverName}_${originalName}`;
68
+ Object.defineProperty(tool, "name", { value: newName });
69
+ const shortDesc = tool.description && typeof tool.description === 'string' ? tool.description.slice(0, 100) + '...' : '';
70
+ display.log(`\nLoaded MCP tool: ${tool.name} (from ${serverName})\n ${shortDesc}`, { level: 'info', source: 'Construtor' });
71
+ });
72
+ // Sanitize tool schemas to remove fields not supported by Gemini
73
+ const sanitizedTools = tools.map(tool => wrapToolWithSanitizedSchema(tool));
74
+ allTools.push(...sanitizedTools);
75
+ }
76
+ catch (error) {
77
+ display.log(`Failed to initialize MCP tools for server '${serverName}': ${error}`, { level: 'warning', source: 'Construtor' });
78
+ // Continue to other servers even if one fails
79
+ }
76
80
  }
81
+ display.log(`Loaded ${allTools.length} total MCP tools (schemas sanitized for Gemini compatibility)`, { level: 'info', source: 'Construtor' });
82
+ return allTools;
77
83
  }
78
84
  }
@@ -21,6 +21,7 @@ export const DEFAULT_CONFIG = {
21
21
  provider: 'openai',
22
22
  model: 'gpt-4',
23
23
  temperature: 0.7,
24
+ context_window: 100,
24
25
  },
25
26
  channels: {
26
27
  telegram: { enabled: false, allowedUsers: [] },