morpheus-cli 0.7.0 → 0.7.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 CHANGED
@@ -312,6 +312,7 @@ neo:
312
312
  model: gpt-4o-mini
313
313
  temperature: 0.2
314
314
  context_window: 100
315
+ personality: analytical_engineer
315
316
 
316
317
  apoc:
317
318
  provider: openai
@@ -319,11 +320,13 @@ apoc:
319
320
  temperature: 0.2
320
321
  working_dir: /home/user/projects
321
322
  timeout_ms: 30000
323
+ personality: pragmatic_dev
322
324
 
323
325
  trinity:
324
326
  provider: openai
325
327
  model: gpt-4o-mini
326
328
  temperature: 0.2
329
+ personality: data_specialist
327
330
 
328
331
  chronos:
329
332
  check_interval_ms: 60000 # polling interval in ms (minimum 60000)
@@ -400,6 +403,7 @@ Generic Morpheus overrides (selected):
400
403
  | `MORPHEUS_NEO_CONTEXT_WINDOW` | `neo.context_window` |
401
404
  | `MORPHEUS_NEO_API_KEY` | `neo.api_key` |
402
405
  | `MORPHEUS_NEO_BASE_URL` | `neo.base_url` |
406
+ | `MORPHEUS_NEO_PERSONALITY` | `neo.personality` |
403
407
  | `MORPHEUS_APOC_PROVIDER` | `apoc.provider` |
404
408
  | `MORPHEUS_APOC_MODEL` | `apoc.model` |
405
409
  | `MORPHEUS_APOC_TEMPERATURE` | `apoc.temperature` |
@@ -408,10 +412,12 @@ Generic Morpheus overrides (selected):
408
412
  | `MORPHEUS_APOC_API_KEY` | `apoc.api_key` |
409
413
  | `MORPHEUS_APOC_WORKING_DIR` | `apoc.working_dir` |
410
414
  | `MORPHEUS_APOC_TIMEOUT_MS` | `apoc.timeout_ms` |
415
+ | `MORPHEUS_APOC_PERSONALITY` | `apoc.personality` |
411
416
  | `MORPHEUS_TRINITY_PROVIDER` | `trinity.provider` |
412
417
  | `MORPHEUS_TRINITY_MODEL` | `trinity.model` |
413
418
  | `MORPHEUS_TRINITY_TEMPERATURE` | `trinity.temperature` |
414
419
  | `MORPHEUS_TRINITY_API_KEY` | `trinity.api_key` |
420
+ | `MORPHEUS_TRINITY_PERSONALITY` | `trinity.personality` |
415
421
  | `MORPHEUS_AUDIO_PROVIDER` | `audio.provider` |
416
422
  | `MORPHEUS_AUDIO_MODEL` | `audio.model` |
417
423
  | `MORPHEUS_AUDIO_ENABLED` | `audio.enabled` |
@@ -77,50 +77,68 @@ function intervalToCron(expression) {
77
77
  `Supported formats: "every N minutes/hours/days/weeks", "every minute/hour/day/week", ` +
78
78
  `"every monday [at 9am]", "every monday and friday at 18:30", "every weekday", "every weekend", "daily", "weekly".`);
79
79
  }
80
+ /**
81
+ * Creates a Date in UTC from a local time in a specific timezone.
82
+ * E.g., createDateInTimezone(2026, 2, 26, 23, 0, 'America/Sao_Paulo') returns
83
+ * a Date representing 23:00 BRT = 02:00 UTC (next day).
84
+ */
85
+ function createDateInTimezone(year, month, day, hour, minute, timezone) {
86
+ // Create a candidate UTC date
87
+ const candidateUtc = Date.UTC(year, month - 1, day, hour, minute, 0, 0);
88
+ // Get the offset at that moment for the timezone
89
+ const offsetMs = ianaToOffsetMinutes(timezone, new Date(candidateUtc)) * 60_000;
90
+ // Subtract offset: if BRT is -180 min (-3h), local 23:00 = UTC 23:00 - (-3h) = UTC 02:00
91
+ return new Date(candidateUtc - offsetMs);
92
+ }
93
+ /**
94
+ * Gets the current date components (year, month, day) in a specific timezone.
95
+ */
96
+ function getDatePartsInTimezone(date, timezone) {
97
+ const formatter = new Intl.DateTimeFormat('en-CA', { timeZone: timezone, year: 'numeric', month: '2-digit', day: '2-digit' });
98
+ const parts = formatter.formatToParts(date);
99
+ return {
100
+ year: parseInt(parts.find(p => p.type === 'year').value, 10),
101
+ month: parseInt(parts.find(p => p.type === 'month').value, 10),
102
+ day: parseInt(parts.find(p => p.type === 'day').value, 10),
103
+ };
104
+ }
80
105
  /**
81
106
  * Parses Portuguese time expressions and converts to ISO 8601 format.
82
107
  * Handles patterns like "às 15h", "hoje às 15:30", "amanhã às 9h".
83
108
  */
84
109
  function parsePortugueseTimeExpression(expression, refDate, timezone) {
85
110
  const lower = expression.toLowerCase().trim();
111
+ const { year, month, day } = getDatePartsInTimezone(refDate, timezone);
86
112
  // Pattern: "às 15h", "as 15h", "às 15:30", "as 15:30"
87
113
  const timeOnlyMatch = lower.match(/^(?:à|a)s?\s+(\d{1,2})(?::(\d{2}))?h?$/);
88
114
  if (timeOnlyMatch) {
89
- let hour = parseInt(timeOnlyMatch[1], 10);
115
+ const hour = parseInt(timeOnlyMatch[1], 10);
90
116
  const minute = timeOnlyMatch[2] ? parseInt(timeOnlyMatch[2], 10) : 0;
91
- // Create date by setting hours in the target timezone
92
- // We use Intl.DateTimeFormat to properly handle timezone
93
- const targetDate = new Date();
94
- const tzDate = new Date(targetDate.toLocaleString('en-US', { timeZone: timezone }));
95
- tzDate.setHours(hour, minute, 0, 0);
117
+ let result = createDateInTimezone(year, month, day, hour, minute, timezone);
96
118
  // If time is in the past today, schedule for tomorrow
97
- if (tzDate.getTime() <= refDate.getTime()) {
98
- tzDate.setDate(tzDate.getDate() + 1);
119
+ if (result.getTime() <= refDate.getTime()) {
120
+ result = createDateInTimezone(year, month, day + 1, hour, minute, timezone);
99
121
  }
100
- return tzDate;
122
+ return result;
101
123
  }
102
124
  // Pattern: "hoje às 15h", "hoje as 15:30"
103
125
  const todayMatch = lower.match(/^hoje\s+(?:à|a)s?\s+(\d{1,2})(?::(\d{2}))?h?$/);
104
126
  if (todayMatch) {
105
- let hour = parseInt(todayMatch[1], 10);
127
+ const hour = parseInt(todayMatch[1], 10);
106
128
  const minute = todayMatch[2] ? parseInt(todayMatch[2], 10) : 0;
107
- const tzDate = new Date(new Date().toLocaleString('en-US', { timeZone: timezone }));
108
- tzDate.setHours(hour, minute, 0, 0);
129
+ const result = createDateInTimezone(year, month, day, hour, minute, timezone);
109
130
  // If already passed, return null (can't schedule in the past)
110
- if (tzDate.getTime() <= refDate.getTime()) {
131
+ if (result.getTime() <= refDate.getTime()) {
111
132
  return null;
112
133
  }
113
- return tzDate;
134
+ return result;
114
135
  }
115
136
  // Pattern: "amanhã às 15h", "amanha as 15:30", "amanhã às 15h da tarde"
116
- const tomorrowMatch = lower.match(/^amanhã(?:ã)?\s+(?:à|a)s?\s+(\d{1,2})(?::(\d{2}))?h?(?:\s+(?:da|do)\s+(?:manhã|tarde|noite))?$/);
137
+ const tomorrowMatch = lower.match(/^amanh[aã]\s+(?:à|a)s?\s+(\d{1,2})(?::(\d{2}))?h?(?:\s+(?:da|do)\s+(?:manh[aã]|tarde|noite))?$/);
117
138
  if (tomorrowMatch) {
118
- let hour = parseInt(tomorrowMatch[1], 10);
139
+ const hour = parseInt(tomorrowMatch[1], 10);
119
140
  const minute = tomorrowMatch[2] ? parseInt(tomorrowMatch[2], 10) : 0;
120
- const tzDate = new Date(new Date().toLocaleString('en-US', { timeZone: timezone }));
121
- tzDate.setDate(tzDate.getDate() + 1);
122
- tzDate.setHours(hour, minute, 0, 0);
123
- return tzDate;
141
+ return createDateInTimezone(year, month, day + 1, hour, minute, timezone);
124
142
  }
125
143
  // Pattern: "daqui a X minutos/horas/dias"
126
144
  const relativeMatch = lower.match(/^daqui\s+a\s+(\d+)\s+(minutos?|horas?|dias?|semanas?)$/);
@@ -135,6 +153,22 @@ function parsePortugueseTimeExpression(expression, refDate, timezone) {
135
153
  }
136
154
  return null;
137
155
  }
156
+ /**
157
+ * Converts an IANA timezone name (e.g. "America/Sao_Paulo") to a UTC offset in minutes.
158
+ * chrono-node only understands abbreviations (EST, BRT) and numeric offsets,
159
+ * NOT IANA names — passing an unrecognised string makes it silently fall back
160
+ * to the system timezone, which breaks on servers running in UTC.
161
+ */
162
+ function ianaToOffsetMinutes(timezone, refDate) {
163
+ try {
164
+ const utcStr = refDate.toLocaleString('en-US', { timeZone: 'UTC' });
165
+ const tzStr = refDate.toLocaleString('en-US', { timeZone: timezone });
166
+ return Math.round((new Date(tzStr).getTime() - new Date(utcStr).getTime()) / 60_000);
167
+ }
168
+ catch {
169
+ return 0; // fall back to UTC on invalid timezone
170
+ }
171
+ }
138
172
  function formatDatetime(date, timezone) {
139
173
  try {
140
174
  return date.toLocaleString('en-US', {
@@ -180,7 +214,9 @@ export function parseScheduleExpression(expression, type, opts = {}) {
180
214
  }
181
215
  // 4. chrono-node NLP fallback ("tomorrow at 9am", "next friday", etc.)
182
216
  if (!parsed) {
183
- const results = chrono.parse(expression, { instant: refDate, timezone });
217
+ // chrono-node does NOT support IANA timezone names — convert to numeric offset
218
+ const tzOffset = ianaToOffsetMinutes(timezone, refDate);
219
+ const results = chrono.parse(expression, { instant: refDate, timezone: tzOffset });
184
220
  if (results.length > 0 && results[0].date()) {
185
221
  parsed = results[0].date();
186
222
  }