morpheus-cli 0.6.0 → 0.6.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.
@@ -295,81 +295,197 @@ const browserFillTool = tool(async ({ selector, value, press_enter, timeout_ms }
295
295
  */
296
296
  const browserSearchTool = tool(async ({ query, num_results, language }) => {
297
297
  try {
298
- const max = num_results ?? 10;
299
- // DDG region codes: "br-pt" for Brazil/Portuguese, "us-en" for US/English, etc.
300
- // Map from simple lang code to DDG kl param
298
+ const max = Math.min(num_results ?? 10, 20);
299
+ const year = new Date().getFullYear().toString();
300
+ const lang = language ?? "pt";
301
+ // ─────────────────────────────────────────────
302
+ // 1️⃣ Intent Classification (heurístico leve)
303
+ // ─────────────────────────────────────────────
304
+ const qLower = query.toLowerCase();
305
+ let intent = "general";
306
+ if (/(hoje|último|resultado|placar|próximos|futebol|202\d)/.test(qLower))
307
+ intent = "news";
308
+ if (/(site oficial|gov|receita federal|ministério)/.test(qLower))
309
+ intent = "official";
310
+ if (/(api|sdk|npm|docs|documentação)/.test(qLower))
311
+ intent = "documentation";
312
+ if (/(preço|valor|quanto custa)/.test(qLower))
313
+ intent = "price";
314
+ // ─────────────────────────────────────────────
315
+ // 2️⃣ Query Refinement
316
+ // ─────────────────────────────────────────────
317
+ let refinedQuery = query;
318
+ if (intent === "news") {
319
+ refinedQuery = `${query} ${year}`;
320
+ }
321
+ if (intent === "official") {
322
+ refinedQuery = `${query} site:gov.br OR site:org`;
323
+ }
324
+ if (intent === "documentation") {
325
+ refinedQuery = `${query} documentation OR docs OR github`;
326
+ }
327
+ if (intent === "price") {
328
+ refinedQuery = `${query} preço ${year} Brasil`;
329
+ }
330
+ // ─────────────────────────────────────────────
331
+ // 3️⃣ DuckDuckGo Lite Fetch
332
+ // ─────────────────────────────────────────────
301
333
  const regionMap = {
302
- pt: 'br-pt', br: 'br-pt',
303
- en: 'us-en', us: 'us-en',
304
- es: 'es-es', fr: 'fr-fr',
305
- de: 'de-de', it: 'it-it',
306
- jp: 'jp-jp', ar: 'ar-es',
334
+ pt: "br-pt",
335
+ br: "br-pt",
336
+ en: "us-en",
337
+ us: "us-en",
307
338
  };
308
- const lang = language ?? 'pt';
309
339
  const kl = regionMap[lang] ?? lang;
310
- const body = new URLSearchParams({ q: query, kl }).toString();
311
- const res = await fetch('https://lite.duckduckgo.com/lite/', {
312
- method: 'POST',
340
+ const body = new URLSearchParams({ q: refinedQuery, kl }).toString();
341
+ const res = await fetch("https://lite.duckduckgo.com/lite/", {
342
+ method: "POST",
313
343
  headers: {
314
- 'Content-Type': 'application/x-www-form-urlencoded',
315
- 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 ' +
316
- '(KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36',
344
+ "Content-Type": "application/x-www-form-urlencoded",
345
+ "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36",
317
346
  },
318
347
  body,
319
- signal: AbortSignal.timeout(20_000),
348
+ signal: AbortSignal.timeout(20000),
320
349
  });
321
350
  if (!res.ok) {
322
- return JSON.stringify({ success: false, query, error: `HTTP ${res.status}` });
351
+ return JSON.stringify({ success: false, error: `HTTP ${res.status}` });
323
352
  }
324
353
  const html = await res.text();
325
- // Extract all result-link anchors (href uses double quotes, class uses single quotes)
326
354
  const linkPattern = /href="(https?:\/\/[^"]+)"[^>]*class='result-link'>([^<]+)<\/a>/g;
327
355
  const snippetPattern = /class='result-snippet'>([\s\S]*?)<\/td>/g;
328
- const allLinks = [...html.matchAll(linkPattern)];
329
- const allSnippets = [...html.matchAll(snippetPattern)];
330
- // Pair links with snippets by index, filtering sponsored (DDG y.js redirect URLs)
356
+ const links = [...html.matchAll(linkPattern)];
357
+ const snippets = [...html.matchAll(snippetPattern)];
358
+ if (!links.length) {
359
+ return JSON.stringify({
360
+ success: false,
361
+ query: refinedQuery,
362
+ error: "No results found",
363
+ });
364
+ }
365
+ // ─────────────────────────────────────────────
366
+ // 4️⃣ Helpers
367
+ // ─────────────────────────────────────────────
368
+ function normalizeUrl(url) {
369
+ try {
370
+ const u = new URL(url);
371
+ u.search = ""; // remove tracking params
372
+ return u.toString();
373
+ }
374
+ catch {
375
+ return url;
376
+ }
377
+ }
378
+ function getDomain(url) {
379
+ try {
380
+ return new URL(url).hostname.replace("www.", "");
381
+ }
382
+ catch {
383
+ return "";
384
+ }
385
+ }
386
+ const trustedDomains = [
387
+ "gov.br",
388
+ "bbc.com",
389
+ "reuters.com",
390
+ "globo.com",
391
+ "uol.com",
392
+ "cnn.com",
393
+ "github.com",
394
+ "npmjs.com",
395
+ "com.br"
396
+ ];
397
+ function scoreResult(result) {
398
+ let score = 0;
399
+ const domain = getDomain(result.url);
400
+ if (trustedDomains.some((d) => domain.includes(d)))
401
+ score += 5;
402
+ if (intent === "official" && domain.includes("gov"))
403
+ score += 5;
404
+ if (intent === "documentation" && domain.includes("github"))
405
+ score += 4;
406
+ if (intent === "news" && /(globo|uol|cnn|bbc)/.test(domain))
407
+ score += 3;
408
+ if (result.title.toLowerCase().includes(query.toLowerCase()))
409
+ score += 2;
410
+ if (result.snippet.length > 120)
411
+ score += 1;
412
+ if (/login|assine|subscribe|paywall/i.test(result.snippet))
413
+ score -= 3;
414
+ return score;
415
+ }
416
+ // ─────────────────────────────────────────────
417
+ // 5️⃣ Build Results + Deduplicate Domain
418
+ // ─────────────────────────────────────────────
419
+ const domainSeen = new Set();
331
420
  const results = [];
332
- for (let i = 0; i < allLinks.length && results.length < max; i++) {
333
- const url = allLinks[i][1];
334
- const title = allLinks[i][2].trim();
335
- // Skip sponsored ads (redirected through duckduckgo.com/y.js)
336
- if (url.startsWith('https://duckduckgo.com/'))
421
+ for (let i = 0; i < links.length; i++) {
422
+ const rawUrl = links[i][1];
423
+ if (rawUrl.startsWith("https://duckduckgo.com/"))
337
424
  continue;
338
- const snippet = allSnippets[i]
339
- ? allSnippets[i][1].replace(/<[^>]+>/g, '').trim()
340
- : '';
341
- results.push({ title, url, snippet });
425
+ const url = normalizeUrl(rawUrl);
426
+ const domain = getDomain(url);
427
+ if (domainSeen.has(domain))
428
+ continue;
429
+ domainSeen.add(domain);
430
+ const title = links[i][2].trim();
431
+ const snippet = snippets[i]
432
+ ? snippets[i][1].replace(/<[^>]+>/g, "").trim()
433
+ : "";
434
+ const result = { title, url, snippet };
435
+ const score = scoreResult(result);
436
+ results.push({ ...result, domain, score });
342
437
  }
343
- if (results.length === 0) {
438
+ if (!results.length) {
344
439
  return JSON.stringify({
345
440
  success: false,
346
- query,
347
- error: 'No results found. The query may be too specific or DDG returned an unexpected response.',
441
+ query: refinedQuery,
442
+ error: "No valid results after filtering",
348
443
  });
349
444
  }
350
- return JSON.stringify({ success: true, query, results });
445
+ // ─────────────────────────────────────────────
446
+ // 6️⃣ Ranking
447
+ // ─────────────────────────────────────────────
448
+ results.sort((a, b) => b.score - a.score);
449
+ const topResults = results.slice(0, max);
450
+ const avgScore = topResults.reduce((acc, r) => acc + r.score, 0) /
451
+ topResults.length;
452
+ // ─────────────────────────────────────────────
453
+ // 7️⃣ Low-Confidence Auto Retry
454
+ // ─────────────────────────────────────────────
455
+ if (avgScore < 2 && intent !== "general") {
456
+ return JSON.stringify({
457
+ success: false,
458
+ query: refinedQuery,
459
+ warning: "Low confidence results. Consider refining query further.",
460
+ results: topResults,
461
+ });
462
+ }
463
+ return JSON.stringify({
464
+ success: true,
465
+ original_query: query,
466
+ refined_query: refinedQuery,
467
+ intent,
468
+ results: topResults.map((r) => ({
469
+ title: r.title,
470
+ url: r.url,
471
+ snippet: r.snippet,
472
+ score: r.score,
473
+ })),
474
+ });
351
475
  }
352
476
  catch (err) {
353
- return JSON.stringify({ success: false, query, error: err.message });
477
+ return JSON.stringify({
478
+ success: false,
479
+ error: err.message,
480
+ });
354
481
  }
355
482
  }, {
356
- name: 'browser_search',
357
- description: 'Search the internet using DuckDuckGo and return structured results (title, URL, snippet). ' +
358
- 'Use this when you need to find current information, news, articles, documentation, or any web content. ' +
359
- 'Returns up to 10 results by default. Does NOT require browser_navigate first — it is self-contained and fast.',
483
+ name: "browser_search",
484
+ description: "Enhanced internet search with query refinement, ranking, deduplication, and confidence scoring. Uses DuckDuckGo Lite.",
360
485
  schema: z.object({
361
- query: z.string().describe('Search query'),
362
- num_results: z
363
- .number()
364
- .int()
365
- .min(1)
366
- .max(20)
367
- .optional()
368
- .describe('Number of results to return. Default: 10, max: 20'),
369
- language: z
370
- .string()
371
- .optional()
372
- .describe('Language/region code (e.g. "pt" for Portuguese/Brazil, "en" for English). Default: "pt"'),
486
+ query: z.string(),
487
+ num_results: z.number().int().min(1).max(20).optional(),
488
+ language: z.string().optional(),
373
489
  }),
374
490
  });
375
491
  // ─── Factory ────────────────────────────────────────────────────────────────
@@ -73,151 +73,174 @@ export class Apoc {
73
73
  source: "Apoc",
74
74
  });
75
75
  const systemMessage = new SystemMessage(`
76
- You are Apoc, a specialized devtools subagent within the Morpheus system.
77
-
78
- You are called by Oracle when the user needs dev operations performed.
79
- Your job is to execute the requested task accurately using your available tools.
80
-
81
- Available capabilities:
82
- - Read, write, append, and delete files
83
- - Execute shell commands
84
- - Inspect and manage processes
85
- - Run git operations (status, log, diff, clone, commit, etc.)
86
- - Perform network operations (curl, DNS, ping)
87
- - Manage packages (npm, yarn)
88
- - Inspect system information
89
- - Navigate websites, inspect DOM, click elements, fill forms using a real browser (for JS-heavy pages and SPAs)
90
- - Search the internet with browser_search (DuckDuckGo, returns structured results)
91
-
92
- OPERATING RULES:
93
- 1. Use tools to accomplish the task. Do not speculate.
94
- 2. Always verify results after execution.
95
- 3. Report clearly what was done and what the result was.
96
- 4. If something fails, report the error and what you tried.
97
- 5. Stay focused on the delegated task only.
98
- 6. Respond in the language requested by the user. If not explicit, use the dominant language of the task/context.
99
- 7. For connectivity checks, prefer the dedicated network tool "ping" (TCP reachability) instead of shell "ping".
100
- 8. Only use shell ping when explicitly required by the user. If shell ping is needed, detect OS first:
101
- - Windows: use "-n" (never use "-c")
102
- - Linux/macOS: use "-c"
103
-
104
-
105
- ────────────────────────────────────────
106
- BROWSER AUTOMATION PROTOCOL
107
- ────────────────────────────────────────
108
-
109
- When using browser tools (browser_navigate, browser_get_dom, browser_click, browser_fill), follow this protocol exactly.
110
-
111
- GENERAL PRINCIPLES
112
- - Never guess selectors.
113
- - Never assume page state.
114
- - Always verify page transitions.
115
- - Always extract evidence of success.
116
- - If required user data is missing, STOP and return to Oracle immediately.
117
-
118
- PHASE 1 — Navigation
119
- 1. ALWAYS call browser_navigate first.
120
- 2. Use:
121
- - wait_until: "networkidle0" for SPAs or JS-heavy pages.
122
- - wait_until: "domcontentloaded" for simple pages.
123
- 3. After navigation, confirm current_url and title.
124
- 4. If navigation fails, report the error and stop.
125
-
126
- PHASE 2 — DOM Inspection (MANDATORY BEFORE ACTION)
127
- 1. ALWAYS call browser_get_dom before browser_click or browser_fill.
128
- 2. Identify stable selectors (prefer id > name > role > unique class).
129
- 3. Understand page structure and expected flow before interacting.
130
- 4. Never click or fill blindly.
131
-
132
- PHASE 3 — Interaction
133
- When clicking:
134
- - Prefer stable selectors.
135
- - If ambiguous, refine selector.
136
- - Use visible text only if selector is unstable.
137
-
138
- When filling:
139
- - Confirm correct input field via DOM.
140
- - Fill field.
141
- - Submit using press_enter OR clicking submit button.
142
-
143
- If login or personal data is required:
144
- STOP and return required fields clearly.
145
-
146
- PHASE 4 — State Verification (MANDATORY)
147
- After ANY interaction:
148
- 1. Call browser_get_dom again.
149
- 2. Verify URL change or content change.
150
- 3. Confirm success or detect error message.
151
-
152
- If expected change did not occur:
153
- - Reinspect DOM.
154
- - Attempt one justified alternative.
155
- - If still failing, report failure clearly.
156
-
157
- Maximum 2 attempts per step.
158
- Never assume success.
159
-
160
- PHASE 5 — Reporting
161
- Include:
162
- - Step-by-step actions
163
- - Final URL
164
- - Evidence of success
165
- - Errors encountered
166
- - Completion status (true/false)
167
-
168
-
169
- ────────────────────────────────────────
170
- WEB RESEARCH PROTOCOL
171
- ────────────────────────────────────────
172
-
173
- When using browser_search for factual verification, follow this protocol strictly.
174
-
175
- PHASE 1 — Query Design
176
- 1. Identify core entity, information type, and time constraint.
177
- 2. Build a precise search query.
178
- 3. If time-sensitive, include the current year.
179
-
180
- PHASE 2 — Source Discovery
181
- 1. Call browser_search.
182
- 2. Collect results.
183
- 3. Prioritize official sources and major publications.
184
- 4. Reformulate query if necessary.
185
- 5. IMMEDIATELY save the search result titles and snippets — you will need them as fallback.
186
-
187
- PHASE 3 — Source Validation
188
- 1. Try to open up to 3 distinct URLs with browser_navigate.
189
- - For news/sports/media sites (GE, Globo, UOL, Terra, ESPN, etc.): ALWAYS use wait_until: "networkidle0" — these are SPAs that require JavaScript to load content.
190
- - For simple/static pages: use wait_until: "domcontentloaded".
191
- 2. Read actual page content from accessible pages.
192
- 3. Ignore inaccessible pages (timeouts, bot blocks, errors).
193
- 4. If ALL navigations fail OR page content does not contain useful information:
194
- - DO NOT attempt further workarounds (wget, curl, python scripts, http_request).
195
- - Use the search snippets from Phase 2 as your source and proceed to Phase 5.
196
-
197
- PHASE 4 — Cross-Verification
198
- 1. Extract relevant information from each accessible source.
199
- 2. Compare findings across sources when possible.
200
- 3. If content came from snippets only, state clearly:
201
- "Source: DuckDuckGo search snippets (direct page access unavailable)."
202
-
203
- PHASE 5 — Structured Report
204
- Include:
205
- - Direct answer based ONLY on what was found online
206
- - Source URLs (from search results or navigated pages)
207
- - Confidence level (High / Medium / Low)
208
-
209
- ABSOLUTE RULES — NEVER VIOLATE
210
- 1. NEVER use prior knowledge to fill gaps when online tools failed to find information.
211
- 2. NEVER fabricate, invent, or speculate about news, facts, prices, results, or events.
212
- 3. If browser_search returned results: ALWAYS report those results — never say "no results found".
213
- 4. If content could not be extracted from pages: report the search snippets verbatim.
214
- 5. If both search and navigation failed: say exactly "I was unable to retrieve this information online at this time." Stop there. Do not continue with "based on general knowledge...".
215
- 6. Do NOT attempt more than 2 workaround approaches (wget, curl, python) — if the primary tools fail, move immediately to fallback (snippets) or honest failure report.
76
+ You are Apoc, a high-reliability execution and verification subagent inside the Morpheus system.
216
77
 
78
+ You are NOT a conversational assistant.
79
+ You are a task executor, evidence collector, and autonomous verifier.
217
80
 
81
+ Accuracy is more important than speed.
82
+ If verification fails, you must state it clearly.
83
+
84
+ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
85
+ CORE PRINCIPLES
86
+ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
87
+
88
+ • Never fabricate.
89
+ • Never rely on prior knowledge when online tools are available.
90
+ • Prefer authoritative sources over secondary commentary.
91
+ • Prefer verification over assumption.
92
+ • Explicitly measure and report confidence.
93
+
94
+ If reliable evidence cannot be obtained:
95
+ State clearly:
96
+ "I was unable to retrieve this information online at this time."
97
+
98
+ Stop there.
99
+
100
+ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
101
+ TASK CLASSIFICATION
102
+ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
103
+
104
+ Before using tools:
105
+
106
+ 1. Identify task type:
107
+ - Dev operation
108
+ - Web research
109
+ - Browser automation
110
+ - System inspection
111
+ - Network verification
112
+
113
+ 2. Determine whether external verification is required.
114
+ If yes → use tools.
115
+ If no → respond directly.
116
+
117
+ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
118
+ WEB RESEARCH STRATEGY (QUALITY-FIRST)
119
+ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
120
+
121
+ You operate in iterative cycles.
122
+
123
+ Maximum cycles: 2
124
+
125
+ ━━━━━━━━━━━━━━
126
+ CYCLE 1
127
+ ━━━━━━━━━━━━━━
128
+
129
+ PHASE 1 — Intelligent Query Design
130
+ • Identify intent: news, official, documentation, price, general.
131
+ • Add year if time-sensitive.
132
+ • Add region if relevant.
133
+ • Make query precise and focused.
134
+
135
+ PHASE 2 — Search
136
+ • Use browser_search.
137
+ • Immediately store titles and snippets.
138
+
139
+ PHASE 3 — Source Selection
140
+ Select up to 3 URLs.
141
+ Prefer:
142
+ - One official source
143
+ - One major publication
144
+ - One independent alternative
145
+ Avoid:
146
+ - Multiple links from same domain group
147
+ - Obvious paywalls or login walls
148
+
149
+ PHASE 4 — Navigation & Extraction
150
+ • Use browser_navigate.
151
+ • For news/media → wait_until: "networkidle0"
152
+ • Extract content from:
153
+ article > main > body
154
+ • Remove navigation noise.
155
+
156
+ PHASE 5 — Cross Verification
157
+ • Compare findings across sources.
158
+ • Detect inconsistencies.
159
+ • Identify strongest source.
160
+
161
+ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
162
+ AUTO-REFINEMENT LOOP
163
+ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
164
+
165
+ After completing Cycle 1, evaluate:
166
+
167
+ Trigger refinement if ANY condition is true:
168
+
169
+ • No authoritative source was successfully opened.
170
+ • Only snippets were available.
171
+ • Extracted content did not contain concrete answer.
172
+ • Sources contradict each other.
173
+ • Confidence would be LOW.
174
+ • Search results appear irrelevant or weak.
175
+
176
+ If refinement is triggered:
177
+
178
+ 1. Reformulate query:
179
+ - Add year
180
+ - Add country
181
+ - Add "official"
182
+ - Add domain filters (gov, org, major media)
183
+ - Remove ambiguous words
184
+
185
+ 2. Execute a second search cycle (Cycle 2).
186
+ 3. Repeat selection, navigation, extraction, verification.
187
+ 4. Choose the stronger cycle’s evidence.
188
+ 5. Do NOT perform more than 2 cycles.
189
+
190
+ If Cycle 2 also fails:
191
+ Report inability clearly.
192
+
193
+ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
194
+ SELF-CRITIQUE (MANDATORY BEFORE OUTPUT)
195
+ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
196
+
197
+ Internally evaluate:
198
+
199
+ 1. Did I use at least one authoritative source when available?
200
+ 2. Did I rely only on snippets unnecessarily?
201
+ 3. Did I merge conflicting data incorrectly?
202
+ 4. Did I verify the page actually contained the requested information?
203
+ 5. Did I introduce any information not explicitly found online?
204
+ 6. Is my confidence level justified?
205
+
206
+ If issues are found:
207
+ Correct them.
208
+ If correction is not possible:
209
+ Lower confidence explicitly.
210
+
211
+ Do NOT expose this checklist.
212
+
213
+ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
214
+ CONFIDENCE CRITERIA
215
+ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
216
+
217
+ HIGH:
218
+ • Multiple independent authoritative sources agree
219
+ • Full page extraction used
220
+
221
+ MEDIUM:
222
+ • One strong source OR minor inconsistencies
223
+ • Partial verification
224
+
225
+ LOW:
226
+ • Snippets only OR weak sources OR incomplete confirmation
227
+
228
+ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
229
+ OUTPUT FORMAT (STRICT)
230
+ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
231
+
232
+ 1. Direct Answer
233
+ 2. Evidence Summary
234
+ 3. Sources (URLs)
235
+ 4. Confidence Level (HIGH / MEDIUM / LOW)
236
+ 5. Completion Status (true / false)
237
+
238
+ No conversational filler.
239
+ No reasoning trace.
240
+ Only structured output.
218
241
 
219
242
  ${context ? `CONTEXT FROM ORACLE:\n${context}` : ""}
220
- `);
243
+ `);
221
244
  const userMessage = new HumanMessage(task);
222
245
  const messages = [systemMessage, userMessage];
223
246
  try {
@@ -88,12 +88,11 @@ export class ChronosWorker {
88
88
  session_id: activeSessionId,
89
89
  });
90
90
  try {
91
- // Inject execution context as an AI message so it appears naturally in the
92
- // conversation history without triggering an extra LLM response.
93
- const contextMessage = `[CHRONOS EXECUTION job_id: ${job.id}]\n` +
94
- `Executing scheduled job. Do NOT call chronos_cancel, chronos_schedule, ` +
95
- `or any Chronos management tools during this execution.`;
96
- await this.oracle.injectAIMessage(contextMessage);
91
+ // Prefix the job prompt with the Chronos execution context marker so the
92
+ // Oracle system prompt can detect it in the current HumanMessage.
93
+ // This avoids persisting an AIMessage with the marker in conversation history,
94
+ // which would cause the LLM to reproduce the format in future scheduling responses.
95
+ const promptWithContext = `[CHRONOS EXECUTION job_id: ${job.id}]\n${job.prompt}`;
97
96
  // If a Telegram notify function is registered, tag delegated tasks with
98
97
  // origin_channel: 'telegram' so the TaskDispatcher broadcasts their result.
99
98
  const taskContext = ChronosWorker.notifyFn
@@ -101,7 +100,7 @@ export class ChronosWorker {
101
100
  : undefined;
102
101
  // Hard-block Chronos management tools during execution.
103
102
  ChronosWorker.isExecuting = true;
104
- const response = await this.oracle.chat(job.prompt, undefined, false, taskContext);
103
+ const response = await this.oracle.chat(promptWithContext, undefined, false, taskContext);
105
104
  this.repo.completeExecution(execId, 'success');
106
105
  display.log(`Job ${job.id} completed — status: success`, { source: 'Chronos' });
107
106
  // Deliver Oracle response to notification channels.
@@ -208,7 +208,7 @@ export class SatiRepository {
208
208
  searchUnifiedVector(embedding, limit) {
209
209
  if (!this.db)
210
210
  return [];
211
- const SIMILARITY_THRESHOLD = 0.8;
211
+ const SIMILARITY_THRESHOLD = 0.9;
212
212
  const stmt = this.db.prepare(`
213
213
  SELECT *
214
214
  FROM (
@@ -9,8 +9,8 @@ import { DisplayManager } from "../display.js";
9
9
  export class SQLiteChatMessageHistory extends BaseListChatMessageHistory {
10
10
  lc_namespace = ["langchain", "stores", "message", "sqlite"];
11
11
  display = DisplayManager.getInstance();
12
+ static migrationDone = false; // run migrations only once per process
12
13
  db;
13
- dbSati; // Optional separate DB for Sati memory, if needed in the future
14
14
  sessionId;
15
15
  limit;
16
16
  titleSet = false; // cache: skip setSessionTitleIfNeeded after title is set
@@ -23,20 +23,16 @@ export class SQLiteChatMessageHistory extends BaseListChatMessageHistory {
23
23
  this.limit = fields.limit ? fields.limit : 20;
24
24
  // Default path: ~/.morpheus/memory/short-memory.db
25
25
  const dbPath = fields.databasePath || path.join(homedir(), ".morpheus", "memory", "short-memory.db");
26
- const dbSatiPath = path.join(homedir(), '.morpheus', 'memory', 'sati-memory.db');
27
26
  // Ensure the directory exists
28
27
  this.ensureDirectory(dbPath);
29
- this.ensureDirectory(dbSatiPath);
30
28
  // Initialize database with retry logic for locked databases
31
29
  try {
32
30
  this.db = new Database(dbPath, {
33
31
  ...fields.config,
34
32
  timeout: 5000, // 5 second timeout for locks
35
33
  });
36
- this.dbSati = new Database(dbSatiPath, {
37
- ...fields.config,
38
- timeout: 5000,
39
- });
34
+ this.db.pragma('journal_mode = WAL');
35
+ this.db.pragma('synchronous = NORMAL');
40
36
  try {
41
37
  this.ensureTable();
42
38
  }
@@ -172,6 +168,9 @@ export class SQLiteChatMessageHistory extends BaseListChatMessageHistory {
172
168
  * Checks for missing columns and adds them if necessary.
173
169
  */
174
170
  migrateTable() {
171
+ if (SQLiteChatMessageHistory.migrationDone)
172
+ return;
173
+ SQLiteChatMessageHistory.migrationDone = true;
175
174
  try {
176
175
  // Migrate messages table
177
176
  const tableInfo = this.db.pragma('table_info(messages)');
@@ -669,6 +668,27 @@ export class SQLiteChatMessageHistory extends BaseListChatMessageHistory {
669
668
  tx(); // Executar a transação
670
669
  this.display.log('✅ Nova sessão iniciada e sessão anterior pausada', { source: 'Sati' });
671
670
  }
671
+ chunkText(text, chunkSize = 500, overlap = 50) {
672
+ if (!text || text.length === 0)
673
+ return [];
674
+ const chunks = [];
675
+ let start = 0;
676
+ while (start < text.length) {
677
+ let end = start + chunkSize;
678
+ if (end < text.length) {
679
+ const lastSpace = text.lastIndexOf(' ', end);
680
+ if (lastSpace > start)
681
+ end = lastSpace;
682
+ }
683
+ const chunk = text.slice(start, end).trim();
684
+ if (chunk.length > 0)
685
+ chunks.push(chunk);
686
+ start = end - overlap;
687
+ if (start < 0)
688
+ start = 0;
689
+ }
690
+ return chunks;
691
+ }
672
692
  /**
673
693
  * Encerrar uma sessão e transformá-la em memória do Sati.
674
694
  * Validar sessão existe e está em active ou paused.
@@ -711,30 +731,49 @@ export class SQLiteChatMessageHistory extends BaseListChatMessageHistory {
711
731
  const sessionText = messages
712
732
  .map(m => `[${m.type}] ${m.content}`)
713
733
  .join('\n\n');
714
- // Criar chunks (session_chunks) usando dbSati
715
- if (this.dbSati) {
716
- const chunks = this.chunkText(sessionText);
717
- for (let i = 0; i < chunks.length; i++) {
718
- this.dbSati.prepare(`
719
- INSERT INTO session_chunks (
720
- id,
721
- session_id,
722
- chunk_index,
723
- content,
724
- created_at
725
- ) VALUES (?, ?, ?, ?, ?)
726
- `).run(randomUUID(), sessionId, i, chunks[i], now);
727
- }
728
- this.display.log(`🧩 ${chunks.length} chunks criados para sessão ${sessionId}`, { source: 'Sati' });
729
- }
730
734
  // Remover mensagens da sessão após criar os chunks
731
735
  this.db.prepare(`
732
736
  DELETE FROM messages
733
737
  WHERE session_id = ?
734
738
  `).run(sessionId);
739
+ return sessionText;
735
740
  }
741
+ return null;
736
742
  });
737
- tx(); // Executar a transação
743
+ const sessionText = tx(); // Executar a transação
744
+ // Criar chunks no banco Sati — conexão aberta localmente e fechada ao fim
745
+ if (sessionText) {
746
+ const dbSatiPath = path.join(homedir(), '.morpheus', 'memory', 'sati-memory.db');
747
+ this.ensureDirectory(dbSatiPath);
748
+ const dbSati = new Database(dbSatiPath, { timeout: 5000 });
749
+ dbSati.pragma('journal_mode = WAL');
750
+ try {
751
+ dbSati.exec(`
752
+ CREATE TABLE IF NOT EXISTS session_chunks (
753
+ id TEXT PRIMARY KEY,
754
+ session_id TEXT NOT NULL,
755
+ chunk_index INTEGER NOT NULL,
756
+ content TEXT NOT NULL,
757
+ created_at INTEGER NOT NULL
758
+ );
759
+ CREATE INDEX IF NOT EXISTS idx_session_chunks_session_id ON session_chunks(session_id);
760
+ `);
761
+ const chunks = this.chunkText(sessionText);
762
+ const now = Date.now();
763
+ const insert = dbSati.prepare(`
764
+ INSERT INTO session_chunks (id, session_id, chunk_index, content, created_at)
765
+ VALUES (?, ?, ?, ?, ?)
766
+ `);
767
+ const insertMany = dbSati.transaction((items) => {
768
+ items.forEach((chunk, i) => insert.run(randomUUID(), sessionId, i, chunk, now));
769
+ });
770
+ insertMany(chunks);
771
+ this.display.log(`${chunks.length} chunks criados para sessão ${sessionId}`, { source: 'Sati' });
772
+ }
773
+ finally {
774
+ dbSati.close();
775
+ }
776
+ }
738
777
  }
739
778
  /**
740
779
  * Descartar completamente uma sessão sem gerar memória.
@@ -919,31 +958,6 @@ export class SQLiteChatMessageHistory extends BaseListChatMessageHistory {
919
958
  `).run(newId, now);
920
959
  return newId;
921
960
  }
922
- chunkText(text, chunkSize = 500, overlap = 50) {
923
- if (!text || text.length === 0) {
924
- return [];
925
- }
926
- const chunks = [];
927
- let start = 0;
928
- while (start < text.length) {
929
- let end = start + chunkSize;
930
- // Evita cortar no meio da palavra
931
- if (end < text.length) {
932
- const lastSpace = text.lastIndexOf(' ', end);
933
- if (lastSpace > start) {
934
- end = lastSpace;
935
- }
936
- }
937
- const chunk = text.slice(start, end).trim();
938
- if (chunk.length > 0) {
939
- chunks.push(chunk);
940
- }
941
- start = end - overlap;
942
- if (start < 0)
943
- start = 0;
944
- }
945
- return chunks;
946
- }
947
961
  /**
948
962
  * Lists all active and paused sessions with their basic information.
949
963
  * Returns an array of session objects containing id, title, status, and started_at.
@@ -13,7 +13,7 @@ import { Trinity } from "./trinity.js";
13
13
  import { NeoDelegateTool } from "./tools/neo-tool.js";
14
14
  import { ApocDelegateTool } from "./tools/apoc-tool.js";
15
15
  import { TrinityDelegateTool } from "./tools/trinity-tool.js";
16
- import { TaskQueryTool, chronosTools } from "./tools/index.js";
16
+ import { TaskQueryTool, chronosTools, timeVerifierTool } from "./tools/index.js";
17
17
  import { MCPManager } from "../config/mcp-manager.js";
18
18
  export class Oracle {
19
19
  provider;
@@ -114,6 +114,20 @@ export class Oracle {
114
114
  }
115
115
  return false;
116
116
  }
117
+ hasChronosToolCall(messages) {
118
+ const chronosToolNames = new Set(["chronos_schedule", "chronos_list", "chronos_cancel", "chronos_preview"]);
119
+ for (const msg of messages) {
120
+ if (!(msg instanceof AIMessage))
121
+ continue;
122
+ const toolCalls = msg.tool_calls ?? [];
123
+ if (!Array.isArray(toolCalls))
124
+ continue;
125
+ if (toolCalls.some((tc) => chronosToolNames.has(tc?.name))) {
126
+ return true;
127
+ }
128
+ }
129
+ return false;
130
+ }
117
131
  async initialize() {
118
132
  if (!this.config.llm) {
119
133
  throw new Error("LLM configuration missing in config object.");
@@ -129,7 +143,7 @@ export class Oracle {
129
143
  // Fail-open: Oracle can still initialize even if catalog refresh fails.
130
144
  await Neo.refreshDelegateCatalog().catch(() => { });
131
145
  await Trinity.refreshDelegateCatalog().catch(() => { });
132
- this.provider = await ProviderFactory.create(this.config.llm, [TaskQueryTool, NeoDelegateTool, ApocDelegateTool, TrinityDelegateTool, ...chronosTools]);
146
+ this.provider = await ProviderFactory.create(this.config.llm, [TaskQueryTool, NeoDelegateTool, ApocDelegateTool, TrinityDelegateTool, timeVerifierTool, ...chronosTools]);
133
147
  if (!this.provider) {
134
148
  throw new Error("Provider factory returned undefined");
135
149
  }
@@ -175,6 +189,14 @@ You are ${this.config.agent.name}, ${this.config.agent.personality}, the Oracle.
175
189
 
176
190
  You are an orchestrator and task router.
177
191
 
192
+ If the user request contains ANY time-related expression
193
+ (today, tomorrow, this week, next month, in 3 days, etc),
194
+ you **MUST** call the tool "time_verifier" before answering or call another tool **ALWAYS**.
195
+
196
+ With the time_verify, you remake the user prompt.
197
+
198
+ Never assume dates.
199
+ Always resolve temporal expressions using the tool.
178
200
 
179
201
  Rules:
180
202
  1. For conversation-only requests (greetings, conceptual explanation, memory follow-up, statements of fact, sharing personal information), answer directly. DO NOT create tasks or delegate for simple statements like "I have two cats" or "My name is John". Sati will automatically memorize facts in the background ( **ALWAYS** use SATI Memories to review or retrieve these facts if needed).
@@ -190,6 +212,17 @@ Rules:
190
212
  10. After enqueuing all required delegated tasks for the current message, stop calling tools and return a concise acknowledgement.
191
213
  11. If a delegation is rejected as "not atomic", immediately split into smaller delegations and retry.
192
214
 
215
+ ## Chronos Scheduled Execution
216
+ 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.
217
+
218
+ Behavior rules for Chronos execution context:
219
+ - **Reminder / notification prompts** (e.g., "me lembre de beber água", "lembre de tomar remédio", "avise que é hora de X", "lembrete: reunião às 15h"): respond with ONLY a short, direct notification message. Keep it to 1–2 sentences max. Do NOT use any tools. Do NOT delegate. Do NOT create tasks. Do NOT add motivational commentary or ask follow-up questions.
220
+ - Good: "Hora de beber água! 💧"
221
+ - Good: "Lembrete: reunião em 5 minutos."
222
+ - Bad: "Combinado! Vou beber agora. Você também deveria se hidratar!" (adds unnecessary commentary)
223
+ - **Action / task prompts** (e.g., "executar npm build", "verificar se o servidor está online", "enviar relatório"): execute normally using the appropriate tools.
224
+ - NEVER re-schedule or create new Chronos jobs from within a Chronos execution.
225
+
193
226
  Delegation quality:
194
227
  - Write delegation input in the same language requested by the user.
195
228
  - Include clear objective and constraints.
@@ -286,6 +319,7 @@ Use it to inform your response and tool selection (if needed), but do not assume
286
319
  let responseContent;
287
320
  const toolDelegationAcks = this.extractDelegationAcksFromMessages(newGeneratedMessages);
288
321
  const hadDelegationToolCall = this.hasDelegationToolCall(newGeneratedMessages);
322
+ const hadChronosToolCall = this.hasChronosToolCall(newGeneratedMessages);
289
323
  const mergedDelegationAcks = [
290
324
  ...contextDelegationAcks.map((ack) => ({ task_id: ack.task_id, agent: ack.agent })),
291
325
  ...toolDelegationAcks,
@@ -310,6 +344,9 @@ Use it to inform your response and tool selection (if needed), but do not assume
310
344
  // Persist with addMessage so ack-provider usage is tracked per message row.
311
345
  await this.history.addMessage(userMessage);
312
346
  await this.history.addMessage(ackMessage);
347
+ // Unblock tasks for execution: the ack message is now persisted and will be
348
+ // returned to the caller (Telegram / UI) immediately after this point.
349
+ this.taskRepository.markAckSent(validDelegationAcks.map(a => a.task_id));
313
350
  }
314
351
  else if (mergedDelegationAcks.length > 0 || hadDelegationToolCall) {
315
352
  this.display.log(`Delegation attempted but no valid task id was confirmed (context=${contextDelegationAcks.length}, tool_messages=${toolDelegationAcks.length}, had_tool_call=${hadDelegationToolCall}).`, { source: "Oracle", level: "error" });
@@ -325,7 +362,7 @@ Use it to inform your response and tool selection (if needed), but do not assume
325
362
  else {
326
363
  const lastMessage = response.messages[response.messages.length - 1];
327
364
  responseContent = (typeof lastMessage.content === 'string') ? lastMessage.content : JSON.stringify(lastMessage.content);
328
- if (this.looksLikeSyntheticDelegationAck(responseContent)) {
365
+ if (!hadChronosToolCall && this.looksLikeSyntheticDelegationAck(responseContent)) {
329
366
  blockedSyntheticDelegationAck = true;
330
367
  this.display.log("Blocked synthetic delegation acknowledgement without validated task creation.", { source: "Oracle", level: "error", meta: { preview: responseContent.slice(0, 200) } });
331
368
  const usage = lastMessage.usage_metadata
@@ -15,7 +15,7 @@ export class ProviderFactory {
15
15
  display.log(`Arguments: ${JSON.stringify(request.toolCall.args)}`, { level: "info", source: 'ConstructLoad' });
16
16
  try {
17
17
  const result = handler(request);
18
- display.log("Tool completed successfully", { level: "info", source: 'ConstructLoad' });
18
+ display.log(`Tool completed successfully. Result: ${JSON.stringify(result)}`, { level: "info", source: 'ConstructLoad' });
19
19
  return result;
20
20
  }
21
21
  catch (e) {
@@ -61,6 +61,7 @@ export class TaskRepository {
61
61
  addColumn(`ALTER TABLE tasks ADD COLUMN notify_last_error TEXT`, 'notify_last_error');
62
62
  addColumn(`ALTER TABLE tasks ADD COLUMN notified_at INTEGER`, 'notified_at');
63
63
  addColumn(`ALTER TABLE tasks ADD COLUMN notify_after_at INTEGER`, 'notify_after_at');
64
+ addColumn(`ALTER TABLE tasks ADD COLUMN ack_sent INTEGER NOT NULL DEFAULT 0`, 'ack_sent');
64
65
  this.db.exec(`
65
66
  UPDATE tasks
66
67
  SET
@@ -108,6 +109,7 @@ export class TaskRepository {
108
109
  notify_last_error: row.notify_last_error ?? null,
109
110
  notified_at: row.notified_at ?? null,
110
111
  notify_after_at: row.notify_after_at ?? null,
112
+ ack_sent: row.ack_sent === 1,
111
113
  };
112
114
  }
113
115
  /**
@@ -115,16 +117,20 @@ export class TaskRepository {
115
117
  * acknowledgement and the task result share the same delivery path (e.g. Telegram).
116
118
  * Channels with a synchronous ack (ui, api, cli, webhook) don't need this delay.
117
119
  */
118
- static DEFAULT_NOTIFY_AFTER_MS = 10_000;
120
+ static DEFAULT_NOTIFY_AFTER_MS = 1_000;
119
121
  static CHANNELS_NEEDING_ACK_GRACE = new Set(['telegram', 'discord']);
120
122
  createTask(input) {
121
123
  const now = Date.now();
122
124
  const id = randomUUID();
125
+ const needsAck = TaskRepository.CHANNELS_NEEDING_ACK_GRACE.has(input.origin_channel);
123
126
  const notify_after_at = input.notify_after_at !== undefined
124
127
  ? input.notify_after_at
125
- : TaskRepository.CHANNELS_NEEDING_ACK_GRACE.has(input.origin_channel)
128
+ : needsAck
126
129
  ? now + TaskRepository.DEFAULT_NOTIFY_AFTER_MS
127
130
  : null;
131
+ // ack_sent starts as 0 (blocked) for channels that send an ack message (telegram, discord).
132
+ // For other channels (ui, api, webhook, cli) there is no ack to wait for, so start as 1 (free).
133
+ const ack_sent = needsAck ? 0 : 1;
128
134
  this.db.prepare(`
129
135
  INSERT INTO tasks (
130
136
  id, agent, status, input, context, output, error,
@@ -132,16 +138,16 @@ export class TaskRepository {
132
138
  attempt_count, max_attempts, available_at,
133
139
  created_at, started_at, finished_at, updated_at, worker_id,
134
140
  notify_status, notify_attempts, notify_last_error, notified_at,
135
- notify_after_at
141
+ notify_after_at, ack_sent
136
142
  ) VALUES (
137
143
  ?, ?, 'pending', ?, ?, NULL, NULL,
138
144
  ?, ?, ?, ?,
139
145
  0, ?, ?,
140
146
  ?, NULL, NULL, ?, NULL,
141
147
  'pending', 0, NULL, NULL,
142
- ?
148
+ ?, ?
143
149
  )
144
- `).run(id, input.agent, input.input, input.context ?? null, input.origin_channel, input.session_id, input.origin_message_id ?? null, input.origin_user_id ?? null, input.max_attempts ?? 3, now, now, now, notify_after_at);
150
+ `).run(id, input.agent, input.input, input.context ?? null, input.origin_channel, input.session_id, input.origin_message_id ?? null, input.origin_user_id ?? null, input.max_attempts ?? 3, now, now, now, notify_after_at, ack_sent);
145
151
  return this.getTaskById(id);
146
152
  }
147
153
  getTaskById(id) {
@@ -199,16 +205,27 @@ export class TaskRepository {
199
205
  }
200
206
  return stats;
201
207
  }
208
+ /** Mark ack as sent for a list of task IDs, unblocking them for execution. */
209
+ markAckSent(ids) {
210
+ if (ids.length === 0)
211
+ return;
212
+ const placeholders = ids.map(() => '?').join(', ');
213
+ this.db.prepare(`UPDATE tasks SET ack_sent = 1, updated_at = ? WHERE id IN (${placeholders})`).run(Date.now(), ...ids);
214
+ }
215
+ /** Fallback grace period (ms): tasks older than this run even without ack_sent. */
216
+ static ACK_FALLBACK_MS = 60_000;
202
217
  claimNextPending(workerId) {
203
218
  const now = Date.now();
204
219
  const tx = this.db.transaction(() => {
205
220
  const row = this.db.prepare(`
206
221
  SELECT id
207
222
  FROM tasks
208
- WHERE status = 'pending' AND available_at <= ?
223
+ WHERE status = 'pending'
224
+ AND available_at <= ?
225
+ AND (ack_sent = 1 OR created_at <= ?)
209
226
  ORDER BY created_at ASC
210
227
  LIMIT 1
211
- `).get(now);
228
+ `).get(now, now - TaskRepository.ACK_FALLBACK_MS);
212
229
  if (!row)
213
230
  return null;
214
231
  const result = this.db.prepare(`
@@ -8,14 +8,16 @@ export class TaskWorker {
8
8
  workerId;
9
9
  pollIntervalMs;
10
10
  staleRunningMs;
11
+ maxConcurrent;
11
12
  repository = TaskRepository.getInstance();
12
13
  display = DisplayManager.getInstance();
13
14
  timer = null;
14
- running = false;
15
+ activeTasks = new Set(); // task IDs currently executing
15
16
  constructor(opts) {
16
17
  this.workerId = `task-worker-${randomUUID().slice(0, 8)}`;
17
- this.pollIntervalMs = opts?.pollIntervalMs ?? 1000;
18
+ this.pollIntervalMs = opts?.pollIntervalMs ?? 300;
18
19
  this.staleRunningMs = opts?.staleRunningMs ?? 5 * 60 * 1000;
20
+ this.maxConcurrent = opts?.maxConcurrent ?? parseInt(process.env.MORPHEUS_TASK_CONCURRENCY ?? '3', 10);
19
21
  }
20
22
  start() {
21
23
  if (this.timer)
@@ -25,7 +27,7 @@ export class TaskWorker {
25
27
  this.display.log(`Recovered ${recovered} stale running task(s).`, { source: 'TaskWorker', level: 'warning' });
26
28
  }
27
29
  this.timer = setInterval(() => {
28
- void this.tick();
30
+ this.tick();
29
31
  }, this.pollIntervalMs);
30
32
  this.display.log(`Task worker started (${this.workerId}).`, { source: 'TaskWorker' });
31
33
  }
@@ -36,19 +38,14 @@ export class TaskWorker {
36
38
  this.display.log(`Task worker stopped (${this.workerId}).`, { source: 'TaskWorker' });
37
39
  }
38
40
  }
39
- async tick() {
40
- if (this.running)
41
+ tick() {
42
+ if (this.activeTasks.size >= this.maxConcurrent)
41
43
  return;
42
- this.running = true;
43
- try {
44
- const task = this.repository.claimNextPending(this.workerId);
45
- if (!task)
46
- return;
47
- await this.executeTask(task);
48
- }
49
- finally {
50
- this.running = false;
51
- }
44
+ const task = this.repository.claimNextPending(this.workerId);
45
+ if (!task)
46
+ return;
47
+ this.activeTasks.add(task.id);
48
+ this.executeTask(task).finally(() => this.activeTasks.delete(task.id));
52
49
  }
53
50
  async executeTask(task) {
54
51
  try {
@@ -3,3 +3,4 @@ export * from './morpheus-tools.js';
3
3
  export * from './apoc-tool.js';
4
4
  export * from './neo-tool.js';
5
5
  export * from './chronos-tools.js';
6
+ export * from './time-verify-tools.js';
@@ -11,8 +11,7 @@ Neo built-in capabilities (always available — no MCP required):
11
11
  • Analytics: message_count, token_usage, provider_model_usage — message counts and token/cost usage stats
12
12
  • Tasks: task_query — look up task status by id or session
13
13
  • MCP Management: mcp_list, mcp_manage — list/add/update/delete/enable/disable MCP servers; use action "reload" to reload tools across all agents after config changes
14
- • Webhooks: webhook_list, webhook_manage — create/update/delete webhooks; create returns api_key
15
- • Trinity DB: trinity_db_list, trinity_db_manage — register/update/delete/test connection/refresh schema for databases`.trim();
14
+ • Webhooks: webhook_list, webhook_manage — create/update/delete webhooks; create returns api_key`.trim();
16
15
  const NEO_BASE_DESCRIPTION = `Delegate execution to Neo asynchronously.
17
16
 
18
17
  This tool creates a background task and returns an acknowledgement with task id.
@@ -29,7 +28,7 @@ function buildCatalogSection(mcpTools) {
29
28
  if (mcpTools.length === 0) {
30
29
  return "\n\nRuntime MCP tools: none currently loaded.";
31
30
  }
32
- const maxItems = 32;
31
+ const maxItems = 500;
33
32
  const lines = mcpTools.slice(0, maxItems).map((t) => {
34
33
  const desc = normalizeDescription(t.description).slice(0, 120);
35
34
  return `- ${t.name}: ${desc}`;
@@ -0,0 +1,70 @@
1
+ // tools/timeVerifier.tool.ts
2
+ import { tool } from "@langchain/core/tools";
3
+ import { z } from "zod";
4
+ import * as chrono from "chrono-node";
5
+ import { ConfigManager } from "../../config/manager.js";
6
+ // Create a custom instance that supports English, Portuguese, and Spanish
7
+ const casualChrono = new chrono.Chrono({
8
+ parsers: [
9
+ ...chrono.en.casual.parsers,
10
+ ...chrono.pt.casual.parsers,
11
+ ...chrono.es.casual.parsers,
12
+ ],
13
+ refiners: [
14
+ ...chrono.en.casual.refiners,
15
+ ...chrono.pt.casual.refiners,
16
+ ...chrono.es.casual.refiners,
17
+ ],
18
+ });
19
+ export const timeVerifierTool = tool(async ({ text, timezone }) => {
20
+ // If a timezone is provided, use it for parsing context.
21
+ // Otherwise, use the configured system timezone from Chronos.
22
+ const configTimezone = ConfigManager.getInstance().getChronosConfig()?.timezone || 'UTC';
23
+ const effectiveTimezone = timezone || configTimezone;
24
+ const referenceDate = { instant: new Date(), timezone: effectiveTimezone };
25
+ // Parse using our multi-lingual casual parser
26
+ // We pass forwardDate: true to prefer future dates for ambiguous expressions (e.g. "Friday" -> next Friday)
27
+ // If timezone is provided, we could use it, but chrono-node's timezone support is complex.
28
+ // For now, we rely on relative parsing from the system's current time.
29
+ const results = casualChrono.parse(text, referenceDate, { forwardDate: true });
30
+ if (!results.length) {
31
+ return {
32
+ detected: false,
33
+ message: "No temporal expression detected.",
34
+ };
35
+ }
36
+ const parsed = results.map((result) => {
37
+ const startDate = result.start.date();
38
+ const endDate = result.end?.date();
39
+ return {
40
+ expression: result.text,
41
+ isoStart: startDate.toISOString(),
42
+ isoEnd: endDate ? endDate.toISOString() : null,
43
+ // Format the date in the user's timezone for clarity
44
+ formatted: startDate.toLocaleString('pt-BR', { timeZone: effectiveTimezone, timeZoneName: 'short' }),
45
+ isRange: !!endDate,
46
+ };
47
+ });
48
+ return {
49
+ detected: true,
50
+ timezone: effectiveTimezone,
51
+ referenceDate: referenceDate instanceof Date ? referenceDate.toISOString() : referenceDate.instant.toISOString(),
52
+ parsed,
53
+ };
54
+ }, {
55
+ name: "time_verifier",
56
+ description: `
57
+ Detects and resolves relative time expressions in user input.
58
+ Supports multiple languages (English, Portuguese, Spanish).
59
+ Use this tool whenever the user mentions words like:
60
+ today, tomorrow, yesterday, this week, next week,
61
+ hoje, amanhã, ontem, próxima semana,
62
+ hoy, mañana, ayer, la próxima semana, etc.
63
+
64
+ Returns resolved ISO dates based on the current time and timezone configuration.
65
+ `,
66
+ schema: z.object({
67
+ text: z.string().describe("User input text containing time expressions"),
68
+ timezone: z.string().optional().describe("Optional timezone override. Defaults to system configuration."),
69
+ }),
70
+ });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "morpheus-cli",
3
- "version": "0.6.0",
3
+ "version": "0.6.2",
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"
@@ -46,6 +46,7 @@
46
46
  "cors": "^2.8.6",
47
47
  "cron-parser": "^4.9.0",
48
48
  "cronstrue": "^2.59.0",
49
+ "date-fns": "^4.1.0",
49
50
  "date-fns-tz": "^3.2.0",
50
51
  "express": "^5.2.1",
51
52
  "figlet": "^1.10.0",