morpheus-cli 0.4.14 → 0.5.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 (38) hide show
  1. package/README.md +275 -1116
  2. package/dist/channels/telegram.js +210 -73
  3. package/dist/cli/commands/doctor.js +34 -0
  4. package/dist/cli/commands/init.js +128 -0
  5. package/dist/cli/commands/restart.js +17 -0
  6. package/dist/cli/commands/start.js +15 -0
  7. package/dist/config/manager.js +51 -0
  8. package/dist/config/schemas.js +7 -0
  9. package/dist/devkit/tools/network.js +1 -1
  10. package/dist/http/api.js +177 -10
  11. package/dist/runtime/apoc.js +139 -32
  12. package/dist/runtime/memory/sati/repository.js +30 -2
  13. package/dist/runtime/memory/sati/service.js +46 -15
  14. package/dist/runtime/memory/sati/system-prompts.js +71 -29
  15. package/dist/runtime/memory/sqlite.js +24 -0
  16. package/dist/runtime/neo.js +134 -0
  17. package/dist/runtime/oracle.js +244 -133
  18. package/dist/runtime/providers/factory.js +1 -12
  19. package/dist/runtime/tasks/context.js +53 -0
  20. package/dist/runtime/tasks/dispatcher.js +70 -0
  21. package/dist/runtime/tasks/notifier.js +68 -0
  22. package/dist/runtime/tasks/repository.js +370 -0
  23. package/dist/runtime/tasks/types.js +1 -0
  24. package/dist/runtime/tasks/worker.js +96 -0
  25. package/dist/runtime/tools/apoc-tool.js +61 -8
  26. package/dist/runtime/tools/delegation-guard.js +29 -0
  27. package/dist/runtime/tools/index.js +1 -0
  28. package/dist/runtime/tools/neo-tool.js +99 -0
  29. package/dist/runtime/tools/task-query-tool.js +76 -0
  30. package/dist/runtime/webhooks/dispatcher.js +10 -19
  31. package/dist/types/config.js +10 -0
  32. package/dist/ui/assets/index-20lLB1sM.js +112 -0
  33. package/dist/ui/assets/index-BJ56bRfs.css +1 -0
  34. package/dist/ui/index.html +2 -2
  35. package/dist/ui/sw.js +1 -1
  36. package/package.json +1 -1
  37. package/dist/ui/assets/index-LemKVRjC.js +0 -112
  38. package/dist/ui/assets/index-TCQ7VNYO.css +0 -1
@@ -60,7 +60,6 @@ export class SatiService {
60
60
  const agent = await ProviderFactory.create(satiConfig, []);
61
61
  // Get existing memories for context (Simulated "Working Memory" or full list if small)
62
62
  const allMemories = this.repository.getAllMemories();
63
- const existingSummaries = allMemories.slice(0, 50).map(m => m.summary);
64
63
  // Map conversation to strict types and sanitize
65
64
  const recentConversation = conversation.map(c => ({
66
65
  role: (c.role === 'human' ? 'user' : c.role),
@@ -68,7 +67,12 @@ export class SatiService {
68
67
  }));
69
68
  const inputPayload = {
70
69
  recent_conversation: recentConversation,
71
- existing_memory_summaries: existingSummaries
70
+ existing_memories: allMemories.map(m => ({
71
+ id: m.id,
72
+ category: m.category,
73
+ importance: m.importance,
74
+ summary: m.summary
75
+ }))
72
76
  };
73
77
  const messages = [
74
78
  new SystemMessage(SATI_EVALUATION_PROMPT),
@@ -114,7 +118,7 @@ export class SatiService {
114
118
  }
115
119
  // Safe JSON parsing (handle markdown blocks if LLM wraps output)
116
120
  content = content.replace(/```json/g, '').replace(/```/g, '').trim();
117
- let result = [];
121
+ let result = { inclusions: [], edits: [], deletions: [] };
118
122
  try {
119
123
  result = JSON.parse(content);
120
124
  }
@@ -122,8 +126,10 @@ export class SatiService {
122
126
  console.warn('[SatiService] Failed to parse JSON response:', content);
123
127
  return;
124
128
  }
125
- for (const item of result) {
126
- if (item.should_store && item.summary && item.category && item.importance) {
129
+ const embeddingService = await EmbeddingService.getInstance();
130
+ // Process inclusions (new memories)
131
+ for (const item of (result.inclusions ?? [])) {
132
+ if (item.summary && item.category && item.importance) {
127
133
  display.log(`Persisting new memory: [${item.category.toUpperCase()}] ${item.summary}`, { source: 'Sati' });
128
134
  try {
129
135
  const savedMemory = await this.repository.save({
@@ -134,22 +140,14 @@ export class SatiService {
134
140
  hash: this.generateHash(item.summary),
135
141
  source: 'conversation'
136
142
  });
137
- // 🔥 GERAR EMBEDDING
138
- const embeddingService = await EmbeddingService.getInstance();
139
- const textForEmbedding = [
140
- savedMemory.summary,
141
- savedMemory.details ?? ''
142
- ].join(' ');
143
+ const textForEmbedding = [savedMemory.summary, savedMemory.details ?? ''].join(' ');
143
144
  const embedding = await embeddingService.generate(textForEmbedding);
144
145
  display.log(`Generated embedding for memory ID ${savedMemory.id}`, { source: 'Sati', level: 'debug' });
145
- // 🔥 SALVAR EMBEDDING NO SQLITE_VEC
146
146
  this.repository.upsertEmbedding(savedMemory.id, embedding);
147
- // Quiet success - logging handled by repository/middleware if needed, or verbose debug
148
147
  }
149
148
  catch (saveError) {
150
149
  if (saveError.message && saveError.message.includes('UNIQUE constraint failed')) {
151
- // Duplicate detected by DB (Hash collision)
152
- // This is expected given T012 logic
150
+ // Duplicate detected by DB (hash collision) — expected
153
151
  }
154
152
  else {
155
153
  throw saveError;
@@ -157,6 +155,39 @@ export class SatiService {
157
155
  }
158
156
  }
159
157
  }
158
+ // Process edits (update existing memories)
159
+ for (const edit of (result.edits ?? [])) {
160
+ if (!edit.id)
161
+ continue;
162
+ const updated = this.repository.update(edit.id, {
163
+ importance: edit.importance,
164
+ summary: edit.summary,
165
+ details: edit.details,
166
+ });
167
+ if (updated) {
168
+ display.log(`Updated memory ${edit.id}: ${edit.reason ?? ''}`, { source: 'Sati' });
169
+ if (edit.summary || edit.details) {
170
+ const text = [updated.summary, updated.details ?? ''].join(' ');
171
+ const embedding = await embeddingService.generate(text);
172
+ this.repository.upsertEmbedding(updated.id, embedding);
173
+ }
174
+ }
175
+ else {
176
+ display.log(`Edit skipped — memory not found: ${edit.id}`, { source: 'Sati', level: 'warning' });
177
+ }
178
+ }
179
+ // Process deletions (archive memories)
180
+ for (const deletion of (result.deletions ?? [])) {
181
+ if (!deletion.id)
182
+ continue;
183
+ const archived = this.repository.archiveMemory(deletion.id);
184
+ if (archived) {
185
+ display.log(`Archived memory ${deletion.id}: ${deletion.reason ?? ''}`, { source: 'Sati' });
186
+ }
187
+ else {
188
+ display.log(`Deletion skipped — memory not found: ${deletion.id}`, { source: 'Sati', level: 'warning' });
189
+ }
190
+ }
160
191
  }
161
192
  catch (error) {
162
193
  console.error('[SatiService] Evaluation failed:', error);
@@ -1,13 +1,27 @@
1
1
  export const SATI_EVALUATION_PROMPT = `You are **Sati**, an autonomous background memory manager for an AI assistant.
2
- Your goal is to analyze the conversation interaction and decide if any **Persistent Long-Term Memory** should be stored.
2
+ Your goal is to analyze the conversation and decide what changes to make to the long-term memory store.
3
+ You have full CRUD power: you can create new memories, edit existing ones, or delete memories that are no longer valid.
3
4
 
4
5
  ### INPUT DATA
5
- You will receive:
6
- 1. A list of recent messages (USER and ASSISTANT).
7
- 2. A list of ALREADY EXISTING memory summaries (to avoid duplicates).
6
+ You will receive a JSON object with:
7
+ 1. \`recent_conversation\` list of recent messages (user and assistant).
8
+ 2. \`existing_memories\` full objects for all current memories (with IDs), so you can reference them for edits or deletions.
9
+
10
+ Example input:
11
+ \`\`\`json
12
+ {
13
+ "recent_conversation": [
14
+ { "role": "user", "content": "..." },
15
+ { "role": "assistant", "content": "..." }
16
+ ],
17
+ "existing_memories": [
18
+ { "id": "uuid", "category": "preference", "importance": "medium", "summary": "..." }
19
+ ]
20
+ }
21
+ \`\`\`
8
22
 
9
23
  ### MEMORY CATEGORIES
10
- Classify any new memory into one of these types:
24
+ Classify memories into one of these types:
11
25
  - **preference**: User preferences (e.g., "I like dark mode", "Use TypeScript").
12
26
  - **project**: Details about the user's projects, architecture, or tech stack.
13
27
  - **identity**: Facts about the user's identity, role, or background.
@@ -22,32 +36,60 @@ Classify any new memory into one of these types:
22
36
  - **professional_profile**: Job title, industry, skills.
23
37
 
24
38
  ### CRITICAL RULES
25
- 0. **SAVE ON SUMMARY and REASONING IN ENGLISH AND NATIVE LANGUAGE**: Always generate a concise summary in English and, if the original information is in another language, also provide a summary in the original language. This ensures the memory is accessible and useful for future interactions, regardless of the language used.
26
- 1. **NO SECRETS**: NEVER store API keys, passwords, credit cards, or private tokens. If found, ignore them explicitly.
27
- 2. **NO DUPLICATES**: If the information is already covered by the \`existing_memory_summaries\`, DO NOT store it again.
28
- 3. **NO CHIT-CHAT**: Do not store trivial conversation like "Hello", "Thanks", "How are you?".
29
- 4. **IMPORTANCE**: Assign 'low', 'medium', or 'high' importance. Store only 'medium' or 'high' unless it's a specific user preference (which is always important).
39
+ 0. **SUMMARIES IN ENGLISH AND NATIVE LANGUAGE**: Always write summaries in English. If the original content is in another language, also include a summary in that language (e.g., "Prefers TypeScript | Prefere TypeScript").
40
+ 1. **NO SECRETS**: NEVER store API keys, passwords, credit cards, or private tokens.
41
+ 2. **NO CHIT-CHAT**: Do not store trivial conversation like "Hello", "Thanks", "How are you?".
42
+ 3. **NO PERSONAL FINANCIAL**: Avoid storing sensitive financial information (income, debts, expenses).
43
+ 4. **IMPORTANCE**: Assign 'low', 'medium', or 'high'. Prefer 'medium' or 'high' only use 'low' for minor context.
44
+ 5. **OBEY THE USER**: If the user explicitly states something should be remembered, store it with at least 'medium' importance.
45
+
46
+ ### WHEN TO INCLUDE (create new memory)
47
+ - The information is **genuinely new** and not covered by any entry in \`existing_memories\`.
48
+ - Do NOT create a new entry if an existing memory already captures the same fact.
30
49
 
31
- ### TOP IMPORTANT GUIDELINES
32
- 5. **OBEY THE USER**: If the user explicitly states something should be remembered, it must be stored with at least 'medium' importance.
50
+ ### WHEN TO EDIT an existing memory
51
+ - The user **confirmed or reinforced** an existing preference/fact elevate importance (e.g., medium high).
52
+ - The existing memory is **slightly incorrect or outdated** → correct the summary.
53
+ - The user provided **additional detail** about something already stored → update details field.
54
+ - Use the **exact \`id\`** from \`existing_memories\`. Only include fields that actually change.
55
+ - **Never duplicate** — if the info hasn't changed, do nothing.
56
+
57
+ ### WHEN TO DELETE an existing memory
58
+ - The user **explicitly denied** a stored fact ("No, I don't prefer X", "That's wrong").
59
+ - A memory was **proven false** during this conversation.
60
+ - The user **explicitly asked to forget** something specific.
61
+ - Use the **exact \`id\`** from \`existing_memories\`.
33
62
 
34
63
  ### OUTPUT FORMAT
35
- You MUST respond with a valid JSON object ARRAY matching the \`ISatiEvaluationOutputArray\` interface:
36
- [
37
- {
38
- "should_store": boolean,
39
- "category": "category_name" | null,
40
- "importance": "low" | "medium" | "high" | null,
41
- "summary": "Concise factual statement | Summary in native language" | null,
42
- "reason": "Why you decided to store or not store | Reason in native language"
43
- },
44
- {
45
- "should_store": boolean,
46
- "category": "category_name" | null,
47
- "importance": "low" | "medium" | "high" | null,
48
- "summary": "Concise factual statement | Summary in native language" | null,
49
- "reason": "Why you decided to store or not store | Reason in native language"
50
- },
51
- ]
64
+ Respond with a valid JSON object matching this structure. All three arrays are required (use empty arrays if nothing applies):
65
+
66
+ \`\`\`json
67
+ {
68
+ "inclusions": [
69
+ {
70
+ "category": "preference",
71
+ "importance": "high",
72
+ "summary": "Concise factual statement | Summary in native language",
73
+ "reason": "Why this is being stored"
74
+ }
75
+ ],
76
+ "edits": [
77
+ {
78
+ "id": "uuid-of-existing-memory",
79
+ "importance": "high",
80
+ "summary": "Updated summary if changed",
81
+ "details": "Updated details if changed",
82
+ "reason": "Why this is being edited"
83
+ }
84
+ ],
85
+ "deletions": [
86
+ {
87
+ "id": "uuid-of-existing-memory",
88
+ "reason": "Why this memory is being deleted"
89
+ }
90
+ ]
91
+ }
92
+ \`\`\`
52
93
 
94
+ Empty arrays are perfectly valid. Only include fields in edits that actually need to change.
53
95
  `;
@@ -290,6 +290,30 @@ export class SQLiteChatMessageHistory extends BaseListChatMessageHistory {
290
290
  throw new Error(`Failed to retrieve messages: ${error}`);
291
291
  }
292
292
  }
293
+ /**
294
+ * Retrieves raw stored messages for one or more session IDs.
295
+ * Useful when the caller needs metadata like session_id and created_at.
296
+ */
297
+ async getRawMessagesBySessionIds(sessionIds, limit = this.limit) {
298
+ if (sessionIds.length === 0) {
299
+ return [];
300
+ }
301
+ try {
302
+ const placeholders = sessionIds.map(() => '?').join(', ');
303
+ const stmt = this.db.prepare(`SELECT id, session_id, type, content, created_at, input_tokens, output_tokens, total_tokens, cache_read_tokens, provider, model
304
+ FROM messages
305
+ WHERE session_id IN (${placeholders})
306
+ ORDER BY id DESC
307
+ LIMIT ?`);
308
+ return stmt.all(...sessionIds, limit);
309
+ }
310
+ catch (error) {
311
+ if (error instanceof Error && error.message.includes('SQLITE_BUSY')) {
312
+ throw new Error(`Database is locked. Please try again. Original error: ${error.message}`);
313
+ }
314
+ throw new Error(`Failed to retrieve raw messages: ${error}`);
315
+ }
316
+ }
293
317
  /**
294
318
  * Adds a message to the database.
295
319
  * @param message The message to add
@@ -0,0 +1,134 @@
1
+ import { HumanMessage, SystemMessage, AIMessage } from "@langchain/core/messages";
2
+ import { ConfigManager } from "../config/manager.js";
3
+ import { ProviderFactory } from "./providers/factory.js";
4
+ import { ProviderError } from "./errors.js";
5
+ import { DisplayManager } from "./display.js";
6
+ import { Construtor } from "./tools/factory.js";
7
+ import { ConfigQueryTool, ConfigUpdateTool, DiagnosticTool, MessageCountTool, TokenUsageTool, ProviderModelUsageTool, } from "./tools/index.js";
8
+ import { SQLiteChatMessageHistory } from "./memory/sqlite.js";
9
+ import { TaskRequestContext } from "./tasks/context.js";
10
+ import { updateNeoDelegateToolDescription } from "./tools/neo-tool.js";
11
+ export class Neo {
12
+ static instance = null;
13
+ static currentSessionId = undefined;
14
+ agent;
15
+ config;
16
+ display = DisplayManager.getInstance();
17
+ constructor(config) {
18
+ this.config = config || ConfigManager.getInstance().get();
19
+ }
20
+ static setSessionId(sessionId) {
21
+ Neo.currentSessionId = sessionId;
22
+ }
23
+ static getInstance(config) {
24
+ if (!Neo.instance) {
25
+ Neo.instance = new Neo(config);
26
+ }
27
+ return Neo.instance;
28
+ }
29
+ static resetInstance() {
30
+ Neo.instance = null;
31
+ }
32
+ static async refreshDelegateCatalog() {
33
+ const mcpTools = await Construtor.create();
34
+ const catalogTools = [
35
+ ...mcpTools,
36
+ ConfigQueryTool,
37
+ ConfigUpdateTool,
38
+ DiagnosticTool,
39
+ MessageCountTool,
40
+ TokenUsageTool,
41
+ ProviderModelUsageTool
42
+ ];
43
+ updateNeoDelegateToolDescription(catalogTools);
44
+ }
45
+ async initialize() {
46
+ const neoConfig = this.config.neo || this.config.llm;
47
+ const mcpTools = await Construtor.create();
48
+ const tools = [
49
+ ...mcpTools,
50
+ ConfigQueryTool,
51
+ ConfigUpdateTool,
52
+ DiagnosticTool,
53
+ MessageCountTool,
54
+ TokenUsageTool,
55
+ ProviderModelUsageTool
56
+ ];
57
+ updateNeoDelegateToolDescription(tools);
58
+ this.display.log(`Neo initialized with ${tools.length} tools.`, { source: "Neo" });
59
+ try {
60
+ this.agent = await ProviderFactory.create(neoConfig, tools);
61
+ }
62
+ catch (err) {
63
+ throw new ProviderError(neoConfig.provider, err, "Neo subagent initialization failed");
64
+ }
65
+ }
66
+ async execute(task, context, sessionId, taskContext) {
67
+ const neoConfig = this.config.neo || this.config.llm;
68
+ if (!this.agent) {
69
+ await this.initialize();
70
+ }
71
+ this.display.log(`Executing delegated task in Neo: ${task.slice(0, 80)}...`, {
72
+ source: "Neo",
73
+ });
74
+ const systemMessage = new SystemMessage(`
75
+ You are Neo, an execution subagent in Morpheus.
76
+
77
+ You execute tasks using MCP and internal tools.
78
+ Focus on verifiable execution and return objective results.
79
+
80
+ Rules:
81
+ 1. Use tools whenever task depends on external/system state.
82
+ 2. Validate outputs before final answer.
83
+ 3. If blocked, explain what is missing.
84
+ 4. Keep output concise and actionable.
85
+ 5. Respond in the language requested by the user. If not explicit, use the dominant language of the task/context.
86
+ 6. For connectivity checks, prefer the dedicated network "ping" tool semantics (reachability) and avoid forcing shell flags.
87
+ 7. If delegating shell ping to Apoc is explicitly required, include OS-aware guidance: Windows uses "-n", Linux/macOS uses "-c".
88
+
89
+ ${context ? `Context:\n${context}` : ""}
90
+ `);
91
+ const userMessage = new HumanMessage(task);
92
+ const messages = [systemMessage, userMessage];
93
+ try {
94
+ const invokeContext = {
95
+ origin_channel: taskContext?.origin_channel ?? "api",
96
+ session_id: taskContext?.session_id ?? sessionId ?? "default",
97
+ origin_message_id: taskContext?.origin_message_id,
98
+ origin_user_id: taskContext?.origin_user_id,
99
+ };
100
+ const response = await TaskRequestContext.run(invokeContext, () => this.agent.invoke({ messages }));
101
+ const lastMessage = response.messages[response.messages.length - 1];
102
+ const content = typeof lastMessage.content === "string"
103
+ ? lastMessage.content
104
+ : JSON.stringify(lastMessage.content);
105
+ const targetSession = sessionId ?? Neo.currentSessionId ?? "neo";
106
+ const history = new SQLiteChatMessageHistory({ sessionId: targetSession });
107
+ try {
108
+ const persisted = new AIMessage(content);
109
+ persisted.usage_metadata = lastMessage.usage_metadata
110
+ ?? lastMessage.response_metadata?.usage
111
+ ?? lastMessage.response_metadata?.tokenUsage
112
+ ?? lastMessage.usage;
113
+ persisted.provider_metadata = {
114
+ provider: neoConfig.provider,
115
+ model: neoConfig.model,
116
+ };
117
+ await history.addMessage(persisted);
118
+ }
119
+ finally {
120
+ history.close();
121
+ }
122
+ this.display.log("Neo task completed.", { source: "Neo" });
123
+ return content;
124
+ }
125
+ catch (err) {
126
+ throw new ProviderError(neoConfig.provider, err, "Neo task execution failed");
127
+ }
128
+ }
129
+ async reload() {
130
+ this.config = ConfigManager.getInstance().get();
131
+ this.agent = undefined;
132
+ await this.initialize();
133
+ }
134
+ }