morpheus-cli 0.6.8 → 0.7.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/dist/config/manager.js +4 -1
- package/dist/config/precedence.js +6 -0
- package/dist/runtime/apoc.js +4 -2
- package/dist/runtime/neo.js +4 -2
- package/dist/runtime/oracle.js +29 -0
- package/dist/runtime/tools/factory.js +13 -3
- package/dist/runtime/tools/time-verify-tools.js +49 -14
- package/dist/runtime/trinity.js +4 -2
- package/dist/types/config.js +8 -0
- package/package.json +1 -1
package/dist/config/manager.js
CHANGED
|
@@ -167,7 +167,8 @@ export class ConfigManager {
|
|
|
167
167
|
base_url: config.apoc.base_url || config.llm.base_url,
|
|
168
168
|
context_window: config.apoc.context_window !== undefined ? resolveNumeric('MORPHEUS_APOC_CONTEXT_WINDOW', config.apoc.context_window, config.apoc.context_window) : llmConfig.context_window,
|
|
169
169
|
working_dir: resolveString('MORPHEUS_APOC_WORKING_DIR', config.apoc.working_dir, process.cwd()),
|
|
170
|
-
timeout_ms: config.apoc.timeout_ms !== undefined ? resolveNumeric('MORPHEUS_APOC_TIMEOUT_MS', config.apoc.timeout_ms, 30_000) : 30_000
|
|
170
|
+
timeout_ms: config.apoc.timeout_ms !== undefined ? resolveNumeric('MORPHEUS_APOC_TIMEOUT_MS', config.apoc.timeout_ms, 30_000) : 30_000,
|
|
171
|
+
personality: resolveString('MORPHEUS_APOC_PERSONALITY', config.apoc.personality, 'pragmatic_dev'),
|
|
171
172
|
};
|
|
172
173
|
}
|
|
173
174
|
// Apply precedence to Neo config
|
|
@@ -207,6 +208,7 @@ export class ConfigManager {
|
|
|
207
208
|
api_key: resolveApiKey(neoProvider, 'MORPHEUS_NEO_API_KEY', config.neo?.api_key || llmConfig.api_key),
|
|
208
209
|
base_url: neoBaseUrl || undefined,
|
|
209
210
|
context_window: resolveOptionalNumeric('MORPHEUS_NEO_CONTEXT_WINDOW', config.neo?.context_window, neoContextWindowFallback),
|
|
211
|
+
personality: resolveString('MORPHEUS_NEO_PERSONALITY', config.neo?.personality, 'analytical_engineer'),
|
|
210
212
|
};
|
|
211
213
|
}
|
|
212
214
|
// Apply precedence to Trinity config
|
|
@@ -230,6 +232,7 @@ export class ConfigManager {
|
|
|
230
232
|
api_key: resolveApiKey(trinityProvider, 'MORPHEUS_TRINITY_API_KEY', config.trinity?.api_key || llmConfig.api_key),
|
|
231
233
|
base_url: config.trinity?.base_url || config.llm.base_url,
|
|
232
234
|
context_window: resolveOptionalNumeric('MORPHEUS_TRINITY_CONTEXT_WINDOW', config.trinity?.context_window, trinityContextWindowFallback),
|
|
235
|
+
personality: resolveString('MORPHEUS_TRINITY_PERSONALITY', config.trinity?.personality, 'data_specialist'),
|
|
233
236
|
};
|
|
234
237
|
}
|
|
235
238
|
// Apply precedence to audio config
|
|
@@ -210,6 +210,8 @@ export function getActiveEnvOverrides() {
|
|
|
210
210
|
overrides['neo.temperature'] = true;
|
|
211
211
|
if (isEnvVarSet('MORPHEUS_NEO_API_KEY'))
|
|
212
212
|
overrides['neo.api_key'] = true;
|
|
213
|
+
if (isEnvVarSet('MORPHEUS_NEO_PERSONALITY'))
|
|
214
|
+
overrides['neo.personality'] = true;
|
|
213
215
|
// Apoc
|
|
214
216
|
if (isEnvVarSet('MORPHEUS_APOC_PROVIDER'))
|
|
215
217
|
overrides['apoc.provider'] = true;
|
|
@@ -223,6 +225,8 @@ export function getActiveEnvOverrides() {
|
|
|
223
225
|
overrides['apoc.working_dir'] = true;
|
|
224
226
|
if (isEnvVarSet('MORPHEUS_APOC_TIMEOUT_MS'))
|
|
225
227
|
overrides['apoc.timeout_ms'] = true;
|
|
228
|
+
if (isEnvVarSet('MORPHEUS_APOC_PERSONALITY'))
|
|
229
|
+
overrides['apoc.personality'] = true;
|
|
226
230
|
// Trinity
|
|
227
231
|
if (isEnvVarSet('MORPHEUS_TRINITY_PROVIDER'))
|
|
228
232
|
overrides['trinity.provider'] = true;
|
|
@@ -232,6 +236,8 @@ export function getActiveEnvOverrides() {
|
|
|
232
236
|
overrides['trinity.temperature'] = true;
|
|
233
237
|
if (isEnvVarSet('MORPHEUS_TRINITY_API_KEY'))
|
|
234
238
|
overrides['trinity.api_key'] = true;
|
|
239
|
+
if (isEnvVarSet('MORPHEUS_TRINITY_PERSONALITY'))
|
|
240
|
+
overrides['trinity.personality'] = true;
|
|
235
241
|
// Audio
|
|
236
242
|
if (isEnvVarSet('MORPHEUS_AUDIO_PROVIDER'))
|
|
237
243
|
overrides['audio.provider'] = true;
|
package/dist/runtime/apoc.js
CHANGED
|
@@ -44,6 +44,7 @@ export class Apoc {
|
|
|
44
44
|
// console.log(`Apoc configuration: ${JSON.stringify(apocConfig)}`);
|
|
45
45
|
const working_dir = this.config.apoc?.working_dir || process.cwd();
|
|
46
46
|
const timeout_ms = this.config.apoc?.timeout_ms || 30_000;
|
|
47
|
+
const personality = this.config.apoc?.personality || 'pragmatic_dev';
|
|
47
48
|
// Import all devkit tool factories (side-effect registration)
|
|
48
49
|
await import("../devkit/index.js");
|
|
49
50
|
const tools = buildDevKit({
|
|
@@ -51,7 +52,7 @@ export class Apoc {
|
|
|
51
52
|
allowed_commands: [], // no restriction — Oracle is trusted orchestrator
|
|
52
53
|
timeout_ms,
|
|
53
54
|
});
|
|
54
|
-
this.display.log(`Apoc initialized with ${tools.length} DevKit tools (working_dir: ${working_dir})`, { source: "Apoc" });
|
|
55
|
+
this.display.log(`Apoc initialized with ${tools.length} DevKit tools (working_dir: ${working_dir}, personality: ${personality})`, { source: "Apoc" });
|
|
55
56
|
try {
|
|
56
57
|
this.agent = await ProviderFactory.createBare(apocConfig, tools);
|
|
57
58
|
}
|
|
@@ -72,8 +73,9 @@ export class Apoc {
|
|
|
72
73
|
this.display.log(`Executing delegated task: ${task.slice(0, 80)}...`, {
|
|
73
74
|
source: "Apoc",
|
|
74
75
|
});
|
|
76
|
+
const personality = this.config.apoc?.personality || 'pragmatic_dev';
|
|
75
77
|
const systemMessage = new SystemMessage(`
|
|
76
|
-
You are Apoc, a high-reliability execution and verification subagent inside the Morpheus system.
|
|
78
|
+
You are Apoc, ${personality === 'pragmatic_dev' ? 'a pragmatic and methodical developer' : personality}, a high-reliability execution and verification subagent inside the Morpheus system.
|
|
77
79
|
|
|
78
80
|
You are NOT a conversational assistant.
|
|
79
81
|
You are a task executor, evidence collector, and autonomous verifier.
|
package/dist/runtime/neo.js
CHANGED
|
@@ -35,10 +35,11 @@ export class Neo {
|
|
|
35
35
|
}
|
|
36
36
|
async initialize() {
|
|
37
37
|
const neoConfig = this.config.neo || this.config.llm;
|
|
38
|
+
const personality = this.config.neo?.personality || 'analytical_engineer';
|
|
38
39
|
const mcpTools = await Construtor.create();
|
|
39
40
|
const tools = [...mcpTools, ...morpheusTools];
|
|
40
41
|
updateNeoDelegateToolDescription(mcpTools);
|
|
41
|
-
this.display.log(`Neo initialized with ${tools.length} tools.`, { source: "Neo" });
|
|
42
|
+
this.display.log(`Neo initialized with ${tools.length} tools (personality: ${personality}).`, { source: "Neo" });
|
|
42
43
|
try {
|
|
43
44
|
this.agent = await ProviderFactory.create(neoConfig, tools);
|
|
44
45
|
}
|
|
@@ -54,8 +55,9 @@ export class Neo {
|
|
|
54
55
|
this.display.log(`Executing delegated task in Neo: ${task.slice(0, 80)}...`, {
|
|
55
56
|
source: "Neo",
|
|
56
57
|
});
|
|
58
|
+
const personality = this.config.neo?.personality || 'analytical_engineer';
|
|
57
59
|
const systemMessage = new SystemMessage(`
|
|
58
|
-
You are Neo, an execution subagent in Morpheus.
|
|
60
|
+
You are Neo, ${personality === 'analytical_engineer' ? 'an analytical and precise engineer' : personality}, an execution subagent in Morpheus.
|
|
59
61
|
|
|
60
62
|
You execute tasks using MCP and internal tools.
|
|
61
63
|
Focus on verifiable execution and return objective results.
|
package/dist/runtime/oracle.js
CHANGED
|
@@ -223,6 +223,35 @@ Examples:
|
|
|
223
223
|
- "lembre em todos os canais" → notify_channels: []
|
|
224
224
|
- "lembre em 1 hora" (sem canal) → omit notify_channels
|
|
225
225
|
|
|
226
|
+
## Chronos Schedule Type Selection
|
|
227
|
+
**CRITICAL**: Choose the correct schedule_type based on user intent:
|
|
228
|
+
|
|
229
|
+
### schedule_type: "once" (ONE-TIME reminder)
|
|
230
|
+
Use when user says:
|
|
231
|
+
- "daqui a X minutos/horas/dias" (in X minutes/hours/days)
|
|
232
|
+
- "em X minutos" (in X minutes)
|
|
233
|
+
- "às HH:MM" (at HH:MM)
|
|
234
|
+
- "hoje às...", "amanhã às...", "na próxima segunda"
|
|
235
|
+
- "me lembre de..." (remind me to...)
|
|
236
|
+
- "avise-me..." (notify me...)
|
|
237
|
+
|
|
238
|
+
Example: "me lembre de tomar remédio daqui a 10 minutos" → once, "in 10 minutes"
|
|
239
|
+
|
|
240
|
+
### schedule_type: "interval" (RECURRING reminder)
|
|
241
|
+
Use ONLY when user explicitly says:
|
|
242
|
+
- "a cada X minutos/horas/dias" (every X minutes/hours/days)
|
|
243
|
+
- "todo dia às...", "toda semana...", "todo mês..."
|
|
244
|
+
- "diariamente", "semanalmente", "mensalmente"
|
|
245
|
+
|
|
246
|
+
Example: "me lembre de beber água a cada 2 horas" → interval, "every 2 hours"
|
|
247
|
+
|
|
248
|
+
### schedule_type: "cron" (SPECIFIC schedule)
|
|
249
|
+
Use ONLY when user provides a cron expression or very specific recurring pattern:
|
|
250
|
+
- "todo dia útil às 9am" → cron, "0 9 * * 1-5"
|
|
251
|
+
- "toda segunda e quarta às 3pm" → cron, "0 15 * * 1,3"
|
|
252
|
+
|
|
253
|
+
**IMPORTANT**: Default to "once" for reminders unless user explicitly indicates recurrence with "a cada", "todo", "diariamente", etc.
|
|
254
|
+
|
|
226
255
|
## Chronos Scheduled Execution
|
|
227
256
|
When the current user message starts with [CHRONOS EXECUTION], it means a Chronos scheduled job has just fired. The content after the prefix is the **job's saved prompt**, not a new live request from the user.
|
|
228
257
|
|
|
@@ -40,14 +40,18 @@ function wrapToolWithSanitizedSchema(tool) {
|
|
|
40
40
|
}
|
|
41
41
|
return tool;
|
|
42
42
|
}
|
|
43
|
-
/**
|
|
44
|
-
|
|
43
|
+
/**
|
|
44
|
+
* Timeout (ms) for connecting to each MCP server and fetching its tools list.
|
|
45
|
+
* Increased to 60s to allow time for npx to download and install packages.
|
|
46
|
+
* First connection may take longer as npx downloads the package.
|
|
47
|
+
*/
|
|
48
|
+
const MCP_CONNECT_TIMEOUT_MS = 60_000;
|
|
45
49
|
/**
|
|
46
50
|
* Returns a promise that rejects after `ms` milliseconds with a timeout error.
|
|
47
51
|
* Used to guard `client.getTools()` against servers that never respond.
|
|
48
52
|
*/
|
|
49
53
|
function connectTimeout(serverName, ms) {
|
|
50
|
-
return new Promise((_, reject) => setTimeout(() => reject(new Error(`MCP server '${serverName}' timed out after ${ms}ms
|
|
54
|
+
return new Promise((_, reject) => setTimeout(() => reject(new Error(`MCP server '${serverName}' timed out after ${ms}ms. If using 'npx', first run may take longer to download packages.`)), ms));
|
|
51
55
|
}
|
|
52
56
|
export class Construtor {
|
|
53
57
|
static async probe() {
|
|
@@ -89,6 +93,11 @@ export class Construtor {
|
|
|
89
93
|
onConnectionError: "ignore",
|
|
90
94
|
});
|
|
91
95
|
try {
|
|
96
|
+
display.log(`Connecting to MCP server '${serverName}'... (timeout: ${MCP_CONNECT_TIMEOUT_MS / 1000}s)`, {
|
|
97
|
+
level: 'info',
|
|
98
|
+
source: 'Construtor',
|
|
99
|
+
meta: { server: serverName, transport: serverConfig.transport }
|
|
100
|
+
});
|
|
92
101
|
const tools = await Promise.race([
|
|
93
102
|
client.getTools(),
|
|
94
103
|
connectTimeout(serverName, MCP_CONNECT_TIMEOUT_MS),
|
|
@@ -102,6 +111,7 @@ export class Construtor {
|
|
|
102
111
|
// Sanitize tool schemas to remove fields not supported by Gemini
|
|
103
112
|
const sanitizedTools = tools.map(tool => wrapToolWithSanitizedSchema(tool));
|
|
104
113
|
allTools.push(...sanitizedTools);
|
|
114
|
+
display.log(`Successfully loaded ${tools.length} tools from MCP server '${serverName}'`, { level: 'info', source: 'Construtor' });
|
|
105
115
|
}
|
|
106
116
|
catch (error) {
|
|
107
117
|
display.log(`Failed to initialize MCP tools for server '${serverName}': ${error}`, { level: 'warning', source: 'Construtor' });
|
|
@@ -17,25 +17,60 @@ const casualChrono = new chrono.Chrono({
|
|
|
17
17
|
],
|
|
18
18
|
});
|
|
19
19
|
/**
|
|
20
|
-
* Formats a Date as ISO string with timezone offset
|
|
21
|
-
*
|
|
20
|
+
* Formats a Date as ISO string with timezone offset, preserving the local time.
|
|
21
|
+
*
|
|
22
|
+
* When user says "20:00" in America/Sao_Paulo, we want 20:00 in that timezone,
|
|
23
|
+
* not a converted time. This function extracts the local time components and
|
|
24
|
+
* creates an ISO string with the proper timezone offset.
|
|
25
|
+
*
|
|
26
|
+
* Example: "2026-02-25T20:00:00-03:00" instead of "2026-02-25T20:00:00.000Z"
|
|
22
27
|
*/
|
|
23
28
|
function formatDateWithTimezone(date, timezone) {
|
|
24
|
-
//
|
|
25
|
-
const
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
29
|
+
// Extract the local time components in the target timezone
|
|
30
|
+
const localTimeStr = date.toLocaleString('en-US', {
|
|
31
|
+
timeZone: timezone,
|
|
32
|
+
year: 'numeric',
|
|
33
|
+
month: '2-digit',
|
|
34
|
+
day: '2-digit',
|
|
35
|
+
hour: '2-digit',
|
|
36
|
+
minute: '2-digit',
|
|
37
|
+
second: '2-digit',
|
|
38
|
+
hour12: false,
|
|
39
|
+
});
|
|
40
|
+
// Parse the local time string (format: "MM/DD/YYYY, HH:MM:SS")
|
|
41
|
+
const [datePart, timePart] = localTimeStr.split(', ');
|
|
42
|
+
const [month, day, year] = datePart.split('/');
|
|
43
|
+
const [hours, minutes, seconds] = timePart.split(':');
|
|
44
|
+
// Create a date object representing this local time in the target timezone
|
|
45
|
+
// We need to find the offset for this specific date/time
|
|
46
|
+
const testDate = new Date(Date.UTC(parseInt(year), parseInt(month) - 1, parseInt(day), parseInt(hours), parseInt(minutes), parseInt(seconds)));
|
|
47
|
+
// Get the offset for this timezone at this specific date
|
|
48
|
+
const offsetStr = getOffsetString(testDate, timezone);
|
|
49
|
+
// Format as ISO with offset
|
|
50
|
+
return `${year}-${month}-${day}T${hours}:${minutes}:${seconds}${offsetStr}`;
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Gets the timezone offset string for a given date and timezone.
|
|
54
|
+
* Example: "-03:00" for America/Sao_Paulo
|
|
55
|
+
*/
|
|
56
|
+
function getOffsetString(date, timezone) {
|
|
57
|
+
// Get the timezone offset by comparing UTC and local time
|
|
58
|
+
const utcStr = date.toLocaleString('en-US', { timeZone: 'UTC', timeZoneName: 'short' });
|
|
59
|
+
const tzStr = date.toLocaleString('en-US', { timeZone: timezone, timeZoneName: 'longOffset' });
|
|
60
|
+
// Extract offset from timezone name (e.g., "GMT-03:00" → "-03:00")
|
|
61
|
+
const match = tzStr.match(/GMT([+-]\d{2}):?(\d{2})?/);
|
|
62
|
+
if (match) {
|
|
63
|
+
const sign = match[1].charAt(0);
|
|
64
|
+
const hours = match[1].slice(1);
|
|
65
|
+
const mins = match[2] || '00';
|
|
66
|
+
return `${sign}${hours}:${mins}`;
|
|
67
|
+
}
|
|
68
|
+
// Fallback: calculate offset from Date
|
|
69
|
+
const offsetMinutes = -date.getTimezoneOffset();
|
|
34
70
|
const offsetHours = Math.floor(Math.abs(offsetMinutes) / 60);
|
|
35
71
|
const offsetMins = Math.abs(offsetMinutes) % 60;
|
|
36
72
|
const offsetSign = offsetMinutes >= 0 ? '+' : '-';
|
|
37
|
-
|
|
38
|
-
return `${year}-${month}-${day}T${hours}:${minutes}:${seconds}${offsetStr}`;
|
|
73
|
+
return `${offsetSign}${String(offsetHours).padStart(2, '0')}:${String(offsetMins).padStart(2, '0')}`;
|
|
39
74
|
}
|
|
40
75
|
export const timeVerifierTool = tool(async ({ text, timezone }) => {
|
|
41
76
|
// If a timezone is provided, use it for parsing context.
|
package/dist/runtime/trinity.js
CHANGED
|
@@ -159,8 +159,9 @@ export class Trinity {
|
|
|
159
159
|
}
|
|
160
160
|
async initialize() {
|
|
161
161
|
const trinityConfig = this.config.trinity || this.config.llm;
|
|
162
|
+
const personality = this.config.trinity?.personality || 'data_specialist';
|
|
162
163
|
const tools = this.buildTrinityTools();
|
|
163
|
-
this.display.log(`Trinity initialized with ${tools.length} tools.`, { source: 'Trinity' });
|
|
164
|
+
this.display.log(`Trinity initialized with ${tools.length} tools (personality: ${personality}).`, { source: 'Trinity' });
|
|
164
165
|
try {
|
|
165
166
|
this.agent = await ProviderFactory.createBare(trinityConfig, tools);
|
|
166
167
|
}
|
|
@@ -183,8 +184,9 @@ export class Trinity {
|
|
|
183
184
|
return `- [${db.id}] ${db.name} (${db.type}): ${tables}`;
|
|
184
185
|
}).join('\n')
|
|
185
186
|
: ' (no databases registered)';
|
|
187
|
+
const personality = this.config.trinity?.personality || 'data_specialist';
|
|
186
188
|
const systemMessage = new SystemMessage(`
|
|
187
|
-
You are Trinity, a specialized database subagent within the Morpheus system.
|
|
189
|
+
You are Trinity, ${personality === 'data_specialist' ? 'a meticulous data specialist' : personality}, a specialized database subagent within the Morpheus system.
|
|
188
190
|
|
|
189
191
|
You receive natural-language database tasks from Oracle and execute them using your available tools.
|
|
190
192
|
|
package/dist/types/config.js
CHANGED
|
@@ -50,10 +50,18 @@ export const DEFAULT_CONFIG = {
|
|
|
50
50
|
model: 'gpt-4',
|
|
51
51
|
temperature: 0.2,
|
|
52
52
|
timeout_ms: 30000,
|
|
53
|
+
personality: 'pragmatic_dev',
|
|
53
54
|
},
|
|
54
55
|
neo: {
|
|
55
56
|
provider: 'openai',
|
|
56
57
|
model: 'gpt-4',
|
|
57
58
|
temperature: 0.2,
|
|
59
|
+
personality: 'analytical_engineer',
|
|
60
|
+
},
|
|
61
|
+
trinity: {
|
|
62
|
+
provider: 'openai',
|
|
63
|
+
model: 'gpt-4',
|
|
64
|
+
temperature: 0.2,
|
|
65
|
+
personality: 'data_specialist',
|
|
58
66
|
}
|
|
59
67
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "morpheus-cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.7.0",
|
|
4
4
|
"description": "Morpheus is a local AI agent for developers, running as a CLI daemon that connects to LLMs, local tools, and MCPs, enabling interaction via Terminal, Telegram, and Discord. Inspired by the character Morpheus from *The Matrix*, the project acts as an intelligent orchestrator, bridging the gap between the developer and complex systems.",
|
|
5
5
|
"bin": {
|
|
6
6
|
"morpheus": "./bin/morpheus.js"
|