morpheus-cli 0.6.4 → 0.6.5
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.
|
@@ -811,8 +811,9 @@ export class TelegramAdapter {
|
|
|
811
811
|
return;
|
|
812
812
|
}
|
|
813
813
|
try {
|
|
814
|
+
const globalTz = this.config.getChronosConfig().timezone;
|
|
814
815
|
const { parse: chronoParse } = await import('chrono-node');
|
|
815
|
-
const results = chronoParse(fullText);
|
|
816
|
+
const results = chronoParse(fullText, { timezone: globalTz });
|
|
816
817
|
if (!results.length) {
|
|
817
818
|
await ctx.reply('Could not detect a time expression. Try: `/chronos Check disk space tomorrow at 9am`', { parse_mode: 'Markdown' });
|
|
818
819
|
return;
|
|
@@ -821,7 +822,6 @@ export class TelegramAdapter {
|
|
|
821
822
|
const match = results[0];
|
|
822
823
|
const matchedText = fullText.slice(match.index, match.index + match.text.length);
|
|
823
824
|
const prompt = (fullText.slice(0, match.index) + fullText.slice(match.index + match.text.length)).replace(/\s+/g, ' ').trim() || fullText;
|
|
824
|
-
const globalTz = this.config.getChronosConfig().timezone;
|
|
825
825
|
const { parseScheduleExpression } = await import('../runtime/chronos/parser.js');
|
|
826
826
|
const schedule = parseScheduleExpression(matchedText, 'once', { timezone: globalTz });
|
|
827
827
|
const formatted = new Date(schedule.next_run_at).toLocaleString('en-US', {
|
|
@@ -77,6 +77,64 @@ 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
|
+
* Parses Portuguese time expressions and converts to ISO 8601 format.
|
|
82
|
+
* Handles patterns like "às 15h", "hoje às 15:30", "amanhã às 9h".
|
|
83
|
+
*/
|
|
84
|
+
function parsePortugueseTimeExpression(expression, refDate, timezone) {
|
|
85
|
+
const lower = expression.toLowerCase().trim();
|
|
86
|
+
// Pattern: "às 15h", "as 15h", "às 15:30", "as 15:30"
|
|
87
|
+
const timeOnlyMatch = lower.match(/^(?:à|a)s?\s+(\d{1,2})(?::(\d{2}))?h?$/);
|
|
88
|
+
if (timeOnlyMatch) {
|
|
89
|
+
let hour = parseInt(timeOnlyMatch[1], 10);
|
|
90
|
+
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);
|
|
96
|
+
// If time is in the past today, schedule for tomorrow
|
|
97
|
+
if (tzDate.getTime() <= refDate.getTime()) {
|
|
98
|
+
tzDate.setDate(tzDate.getDate() + 1);
|
|
99
|
+
}
|
|
100
|
+
return tzDate;
|
|
101
|
+
}
|
|
102
|
+
// Pattern: "hoje às 15h", "hoje as 15:30"
|
|
103
|
+
const todayMatch = lower.match(/^hoje\s+(?:à|a)s?\s+(\d{1,2})(?::(\d{2}))?h?$/);
|
|
104
|
+
if (todayMatch) {
|
|
105
|
+
let hour = parseInt(todayMatch[1], 10);
|
|
106
|
+
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);
|
|
109
|
+
// If already passed, return null (can't schedule in the past)
|
|
110
|
+
if (tzDate.getTime() <= refDate.getTime()) {
|
|
111
|
+
return null;
|
|
112
|
+
}
|
|
113
|
+
return tzDate;
|
|
114
|
+
}
|
|
115
|
+
// 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))?$/);
|
|
117
|
+
if (tomorrowMatch) {
|
|
118
|
+
let hour = parseInt(tomorrowMatch[1], 10);
|
|
119
|
+
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;
|
|
124
|
+
}
|
|
125
|
+
// Pattern: "daqui a X minutos/horas/dias"
|
|
126
|
+
const relativeMatch = lower.match(/^daqui\s+a\s+(\d+)\s+(minutos?|horas?|dias?|semanas?)$/);
|
|
127
|
+
if (relativeMatch) {
|
|
128
|
+
const amount = parseInt(relativeMatch[1], 10);
|
|
129
|
+
const unit = relativeMatch[2];
|
|
130
|
+
const ms = unit.startsWith('min') ? amount * 60_000
|
|
131
|
+
: unit.startsWith('hor') ? amount * 3_600_000
|
|
132
|
+
: unit.startsWith('dia') ? amount * 86_400_000
|
|
133
|
+
: amount * 7 * 86_400_000;
|
|
134
|
+
return new Date(refDate.getTime() + ms);
|
|
135
|
+
}
|
|
136
|
+
return null;
|
|
137
|
+
}
|
|
80
138
|
function formatDatetime(date, timezone) {
|
|
81
139
|
try {
|
|
82
140
|
return date.toLocaleString('en-US', {
|
|
@@ -110,13 +168,17 @@ export function parseScheduleExpression(expression, type, opts = {}) {
|
|
|
110
168
|
: amount * 7 * 86_400_000;
|
|
111
169
|
parsed = new Date(refDate.getTime() + ms);
|
|
112
170
|
}
|
|
113
|
-
// 2.
|
|
171
|
+
// 2. Portuguese time expressions: "às 15h", "hoje às 15:30", "amanhã às 9h", "daqui a 30 minutos"
|
|
172
|
+
if (!parsed) {
|
|
173
|
+
parsed = parsePortugueseTimeExpression(expression, refDate, timezone);
|
|
174
|
+
}
|
|
175
|
+
// 3. ISO 8601
|
|
114
176
|
if (!parsed) {
|
|
115
177
|
const isoDate = new Date(expression);
|
|
116
178
|
if (!isNaN(isoDate.getTime()))
|
|
117
179
|
parsed = isoDate;
|
|
118
180
|
}
|
|
119
|
-
//
|
|
181
|
+
// 4. chrono-node NLP fallback ("tomorrow at 9am", "next friday", etc.)
|
|
120
182
|
if (!parsed) {
|
|
121
183
|
const results = chrono.parse(expression, { instant: refDate, timezone });
|
|
122
184
|
if (results.length > 0 && results[0].date()) {
|
|
@@ -125,7 +187,8 @@ export function parseScheduleExpression(expression, type, opts = {}) {
|
|
|
125
187
|
}
|
|
126
188
|
if (!parsed) {
|
|
127
189
|
throw new Error(`Could not parse date/time expression: "${expression}". ` +
|
|
128
|
-
`Try: "in 30 minutes", "in 2 hours", "tomorrow at 9am", "next friday at 3pm",
|
|
190
|
+
`Try: "in 30 minutes", "in 2 hours", "tomorrow at 9am", "next friday at 3pm", ` +
|
|
191
|
+
`"às 15h", "hoje às 15:30", "amanhã às 9h", "daqui a 30 minutos", or an ISO 8601 datetime.`);
|
|
129
192
|
}
|
|
130
193
|
if (parsed.getTime() <= refDate.getTime()) {
|
|
131
194
|
throw new Error(`Scheduled time must be in the future. Got: "${expression}" which resolves to ${parsed.toISOString()}.`);
|
|
@@ -24,6 +24,71 @@ describe('parseScheduleExpression — once type', () => {
|
|
|
24
24
|
expect(result.next_run_at).toBeGreaterThan(REF);
|
|
25
25
|
expect(result.cron_normalized).toBeNull();
|
|
26
26
|
});
|
|
27
|
+
it('parses Portuguese "às 15h" in America/Sao_Paulo timezone', () => {
|
|
28
|
+
const result = parseScheduleExpression('às 15h', 'once', {
|
|
29
|
+
timezone: 'America/Sao_Paulo',
|
|
30
|
+
referenceDate: REF,
|
|
31
|
+
});
|
|
32
|
+
expect(result.type).toBe('once');
|
|
33
|
+
expect(result.next_run_at).toBeGreaterThan(REF);
|
|
34
|
+
// Verify the hour is 15 in Sao Paulo timezone
|
|
35
|
+
const dateInTz = new Date(result.next_run_at).toLocaleString('en-US', {
|
|
36
|
+
timeZone: 'America/Sao_Paulo',
|
|
37
|
+
hour: '2-digit',
|
|
38
|
+
hour12: false,
|
|
39
|
+
});
|
|
40
|
+
expect(dateInTz).toContain('15:00');
|
|
41
|
+
});
|
|
42
|
+
it('parses Portuguese "hoje às 15:30"', () => {
|
|
43
|
+
// Use a reference time before 15:30 to ensure it schedules for today
|
|
44
|
+
const morningRef = new Date().setHours(9, 0, 0, 0);
|
|
45
|
+
const result = parseScheduleExpression('hoje às 15:30', 'once', {
|
|
46
|
+
timezone: 'America/Sao_Paulo',
|
|
47
|
+
referenceDate: morningRef,
|
|
48
|
+
});
|
|
49
|
+
expect(result.type).toBe('once');
|
|
50
|
+
const dateInTz = new Date(result.next_run_at).toLocaleString('en-US', {
|
|
51
|
+
timeZone: 'America/Sao_Paulo',
|
|
52
|
+
hour: '2-digit',
|
|
53
|
+
minute: '2-digit',
|
|
54
|
+
hour12: false,
|
|
55
|
+
});
|
|
56
|
+
expect(dateInTz).toContain('15:30');
|
|
57
|
+
});
|
|
58
|
+
it('parses Portuguese "amanhã às 9h"', () => {
|
|
59
|
+
const result = parseScheduleExpression('amanhã às 9h', 'once', {
|
|
60
|
+
timezone: 'America/Sao_Paulo',
|
|
61
|
+
referenceDate: REF,
|
|
62
|
+
});
|
|
63
|
+
expect(result.type).toBe('once');
|
|
64
|
+
const dateInTz = new Date(result.next_run_at).toLocaleString('en-US', {
|
|
65
|
+
timeZone: 'America/Sao_Paulo',
|
|
66
|
+
hour: '2-digit',
|
|
67
|
+
hour12: false,
|
|
68
|
+
});
|
|
69
|
+
expect(dateInTz).toContain('09:00');
|
|
70
|
+
});
|
|
71
|
+
it('parses Portuguese "daqui a 30 minutos"', () => {
|
|
72
|
+
const result = parseScheduleExpression('daqui a 30 minutos', 'once', {
|
|
73
|
+
timezone: 'America/Sao_Paulo',
|
|
74
|
+
referenceDate: REF,
|
|
75
|
+
});
|
|
76
|
+
expect(result.type).toBe('once');
|
|
77
|
+
// Should be approximately 30 minutes from now
|
|
78
|
+
const diffMinutes = (result.next_run_at - REF) / 1000 / 60;
|
|
79
|
+
expect(diffMinutes).toBeGreaterThanOrEqual(29);
|
|
80
|
+
expect(diffMinutes).toBeLessThanOrEqual(31);
|
|
81
|
+
});
|
|
82
|
+
it('parses Portuguese "daqui a 2 horas"', () => {
|
|
83
|
+
const result = parseScheduleExpression('daqui a 2 horas', 'once', {
|
|
84
|
+
timezone: 'America/Sao_Paulo',
|
|
85
|
+
referenceDate: REF,
|
|
86
|
+
});
|
|
87
|
+
expect(result.type).toBe('once');
|
|
88
|
+
const diffHours = (result.next_run_at - REF) / 1000 / 60 / 60;
|
|
89
|
+
expect(diffHours).toBeGreaterThanOrEqual(1.9);
|
|
90
|
+
expect(diffHours).toBeLessThanOrEqual(2.1);
|
|
91
|
+
});
|
|
27
92
|
});
|
|
28
93
|
describe('parseScheduleExpression — cron type', () => {
|
|
29
94
|
it('parses a valid 5-field cron expression', () => {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "morpheus-cli",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.5",
|
|
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"
|