morpheus-cli 0.6.3 → 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', {
@@ -11,7 +11,7 @@ const CreateWebhookSchema = z.object({
11
11
  .max(100)
12
12
  .regex(/^[a-z0-9-_]+$/, 'Name must be a slug: lowercase letters, numbers, hyphens, underscores only'),
13
13
  prompt: z.string().min(1).max(10_000),
14
- notification_channels: z.array(z.enum(['ui', 'telegram'])).min(1).default(['ui']),
14
+ notification_channels: z.array(z.enum(['ui', 'telegram', 'discord'])).min(1).default(['ui']),
15
15
  });
16
16
  const UpdateWebhookSchema = z.object({
17
17
  name: z
@@ -22,7 +22,7 @@ const UpdateWebhookSchema = z.object({
22
22
  .optional(),
23
23
  prompt: z.string().min(1).max(10_000).optional(),
24
24
  enabled: z.boolean().optional(),
25
- notification_channels: z.array(z.enum(['ui', 'telegram'])).min(1).optional(),
25
+ notification_channels: z.array(z.enum(['ui', 'telegram', 'discord'])).min(1).optional(),
26
26
  });
27
27
  const MarkReadSchema = z.object({
28
28
  ids: z.array(z.string().uuid()).min(1),
@@ -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. ISO 8601
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
- // 3. chrono-node NLP fallback ("tomorrow at 9am", "next friday", etc.)
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", or an ISO 8601 datetime.`);
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",
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"