morpheus-cli 0.5.6 → 0.6.1

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 ────────────────────────────────────────────────────────────────
package/dist/http/api.js CHANGED
@@ -15,6 +15,9 @@ import { TaskRepository } from '../runtime/tasks/repository.js';
15
15
  import { DatabaseRegistry } from '../runtime/memory/trinity-db.js';
16
16
  import { testConnection, introspectSchema } from '../runtime/trinity-connector.js';
17
17
  import { Trinity } from '../runtime/trinity.js';
18
+ import { ChronosRepository } from '../runtime/chronos/repository.js';
19
+ import { ChronosWorker } from '../runtime/chronos/worker.js';
20
+ import { createChronosJobRouter, createChronosConfigRouter } from './routers/chronos.js';
18
21
  async function readLastLines(filePath, n) {
19
22
  try {
20
23
  const content = await fs.readFile(filePath, 'utf8');
@@ -25,11 +28,18 @@ async function readLastLines(filePath, n) {
25
28
  return [];
26
29
  }
27
30
  }
28
- export function createApiRouter(oracle) {
31
+ export function createApiRouter(oracle, chronosWorker) {
29
32
  const router = Router();
30
33
  const configManager = ConfigManager.getInstance();
31
34
  const history = new SQLiteChatMessageHistory({ sessionId: 'api-reader' });
32
35
  const taskRepository = TaskRepository.getInstance();
36
+ const chronosRepo = ChronosRepository.getInstance();
37
+ const worker = chronosWorker ?? ChronosWorker.getInstance();
38
+ // Mount Chronos routers
39
+ if (worker) {
40
+ router.use('/chronos', createChronosJobRouter(chronosRepo, worker));
41
+ router.use('/config/chronos', createChronosConfigRouter(worker));
42
+ }
33
43
  // --- Session Management ---
34
44
  router.get('/sessions', async (req, res) => {
35
45
  try {
@@ -1011,7 +1021,7 @@ export function createApiRouter(oracle) {
1011
1021
  return res.status(404).json({ error: 'Log file not found' });
1012
1022
  }
1013
1023
  const lines = await readLastLines(filePath, limit);
1014
- res.json({ lines: lines.reverse() });
1024
+ res.json({ lines: lines });
1015
1025
  });
1016
1026
  return router;
1017
1027
  }
@@ -0,0 +1,267 @@
1
+ import { Router } from 'express';
2
+ import { z } from 'zod';
3
+ import { ConfigManager } from '../../config/manager.js';
4
+ import { ChronosConfigSchema } from '../../config/schemas.js';
5
+ import { DisplayManager } from '../../runtime/display.js';
6
+ import { ChronosError } from '../../runtime/chronos/repository.js';
7
+ import { parseScheduleExpression, getNextOccurrences, } from '../../runtime/chronos/parser.js';
8
+ const ScheduleTypeSchema = z.enum(['once', 'cron', 'interval']);
9
+ const CreateJobSchema = z.object({
10
+ prompt: z.string().min(1).max(10000),
11
+ schedule_type: ScheduleTypeSchema,
12
+ schedule_expression: z.string().min(1),
13
+ timezone: z.string().optional(),
14
+ });
15
+ const UpdateJobSchema = z.object({
16
+ prompt: z.string().min(1).max(10000).optional(),
17
+ schedule_expression: z.string().min(1).optional(),
18
+ timezone: z.string().optional(),
19
+ enabled: z.boolean().optional(),
20
+ });
21
+ const PreviewSchema = z.object({
22
+ expression: z.string().min(1),
23
+ schedule_type: ScheduleTypeSchema,
24
+ timezone: z.string().optional(),
25
+ });
26
+ const ExecutionsQuerySchema = z.object({
27
+ limit: z.coerce.number().int().min(1).max(100).default(50),
28
+ });
29
+ // ─── Job Router ───────────────────────────────────────────────────────────────
30
+ export function createChronosJobRouter(repo, _worker) {
31
+ const router = Router();
32
+ const configManager = ConfigManager.getInstance();
33
+ // POST /api/chronos/preview — must be before /:id routes
34
+ router.post('/preview', (req, res) => {
35
+ const parsed = PreviewSchema.safeParse(req.body);
36
+ if (!parsed.success) {
37
+ return res.status(400).json({ error: 'Invalid input', details: parsed.error.issues });
38
+ }
39
+ const { expression, schedule_type, timezone } = parsed.data;
40
+ const globalTz = configManager.getChronosConfig().timezone;
41
+ const opts = { timezone: timezone ?? globalTz };
42
+ try {
43
+ const result = parseScheduleExpression(expression, schedule_type, opts);
44
+ const next_occurrences = [];
45
+ if (result.cron_normalized) {
46
+ const timestamps = getNextOccurrences(result.cron_normalized, opts.timezone ?? 'UTC', 3);
47
+ for (const ts of timestamps) {
48
+ next_occurrences.push(new Date(ts).toLocaleString('en-US', {
49
+ timeZone: opts.timezone ?? 'UTC',
50
+ year: 'numeric', month: 'short', day: 'numeric',
51
+ hour: '2-digit', minute: '2-digit', timeZoneName: 'short',
52
+ }));
53
+ }
54
+ }
55
+ res.json({
56
+ next_run_at: result.next_run_at,
57
+ human_readable: result.human_readable,
58
+ next_occurrences,
59
+ });
60
+ }
61
+ catch (err) {
62
+ res.status(400).json({ error: err.message });
63
+ }
64
+ });
65
+ // GET /api/chronos — list jobs
66
+ router.get('/', (req, res) => {
67
+ try {
68
+ const enabled = req.query.enabled;
69
+ const created_by = req.query.created_by;
70
+ const jobs = repo.listJobs({
71
+ enabled: enabled === 'true' ? true : enabled === 'false' ? false : undefined,
72
+ created_by,
73
+ });
74
+ res.json(jobs);
75
+ }
76
+ catch (err) {
77
+ res.status(500).json({ error: err.message });
78
+ }
79
+ });
80
+ // POST /api/chronos — create job
81
+ router.post('/', (req, res) => {
82
+ const parsed = CreateJobSchema.safeParse(req.body);
83
+ if (!parsed.success) {
84
+ return res.status(400).json({ error: 'Invalid input', details: parsed.error.issues });
85
+ }
86
+ const { prompt, schedule_type, schedule_expression, timezone } = parsed.data;
87
+ const globalTz = configManager.getChronosConfig().timezone;
88
+ const tz = timezone ?? globalTz;
89
+ const opts = { timezone: tz };
90
+ try {
91
+ const schedule = parseScheduleExpression(schedule_expression, schedule_type, opts);
92
+ const job = repo.createJob({
93
+ prompt,
94
+ schedule_type,
95
+ schedule_expression,
96
+ cron_normalized: schedule.cron_normalized,
97
+ timezone: tz,
98
+ next_run_at: schedule.next_run_at,
99
+ created_by: 'ui',
100
+ });
101
+ const display = DisplayManager.getInstance();
102
+ display.log(`Job ${job.id} created — ${schedule.human_readable}`, { source: 'Chronos' });
103
+ res.status(201).json({
104
+ job,
105
+ human_readable: schedule.human_readable,
106
+ next_run_formatted: new Date(schedule.next_run_at).toLocaleString('en-US', {
107
+ timeZone: tz,
108
+ year: 'numeric', month: 'short', day: 'numeric',
109
+ hour: '2-digit', minute: '2-digit', timeZoneName: 'short',
110
+ }),
111
+ });
112
+ }
113
+ catch (err) {
114
+ if (err instanceof ChronosError) {
115
+ return res.status(429).json({ error: err.message });
116
+ }
117
+ res.status(400).json({ error: err.message });
118
+ }
119
+ });
120
+ // GET /api/chronos/:id
121
+ router.get('/:id', (req, res) => {
122
+ try {
123
+ const job = repo.getJob(req.params.id);
124
+ if (!job)
125
+ return res.status(404).json({ error: 'Job not found' });
126
+ res.json(job);
127
+ }
128
+ catch (err) {
129
+ res.status(500).json({ error: err.message });
130
+ }
131
+ });
132
+ // PUT /api/chronos/:id — update job
133
+ router.put('/:id', (req, res) => {
134
+ const parsed = UpdateJobSchema.safeParse(req.body);
135
+ if (!parsed.success) {
136
+ return res.status(400).json({ error: 'Invalid input', details: parsed.error.issues });
137
+ }
138
+ try {
139
+ const existing = repo.getJob(req.params.id);
140
+ if (!existing)
141
+ return res.status(404).json({ error: 'Job not found' });
142
+ const patch = parsed.data;
143
+ const tz = patch.timezone ?? existing.timezone;
144
+ let updatedSchedule = undefined;
145
+ if (patch.schedule_expression) {
146
+ updatedSchedule = parseScheduleExpression(patch.schedule_expression, existing.schedule_type, {
147
+ timezone: tz,
148
+ });
149
+ }
150
+ const job = repo.updateJob(req.params.id, {
151
+ prompt: patch.prompt,
152
+ schedule_expression: patch.schedule_expression,
153
+ cron_normalized: updatedSchedule?.cron_normalized,
154
+ timezone: patch.timezone,
155
+ next_run_at: updatedSchedule?.next_run_at,
156
+ enabled: patch.enabled,
157
+ });
158
+ res.json(job);
159
+ }
160
+ catch (err) {
161
+ res.status(400).json({ error: err.message });
162
+ }
163
+ });
164
+ // DELETE /api/chronos/:id
165
+ router.delete('/:id', (req, res) => {
166
+ try {
167
+ const deleted = repo.deleteJob(req.params.id);
168
+ if (!deleted)
169
+ return res.status(404).json({ error: 'Job not found' });
170
+ res.json({ success: true, deleted_id: req.params.id });
171
+ }
172
+ catch (err) {
173
+ res.status(500).json({ error: err.message });
174
+ }
175
+ });
176
+ // PATCH /api/chronos/:id/enable
177
+ router.patch('/:id/enable', (req, res) => {
178
+ try {
179
+ const existing = repo.getJob(req.params.id);
180
+ if (!existing)
181
+ return res.status(404).json({ error: 'Job not found' });
182
+ // Recompute next_run_at
183
+ let nextRunAt;
184
+ if (existing.cron_normalized) {
185
+ // cron_normalized is always a 5-field cron string regardless of the original schedule_type,
186
+ // so always parse it as 'cron' (not 'interval' or 'once').
187
+ const schedule = parseScheduleExpression(existing.cron_normalized, 'cron', {
188
+ timezone: existing.timezone,
189
+ });
190
+ nextRunAt = schedule.next_run_at;
191
+ }
192
+ else if (existing.schedule_type === 'once' && existing.next_run_at && existing.next_run_at > Date.now()) {
193
+ nextRunAt = existing.next_run_at;
194
+ }
195
+ repo.updateJob(req.params.id, { enabled: true, next_run_at: nextRunAt ?? undefined });
196
+ const job = repo.getJob(req.params.id);
197
+ res.json(job);
198
+ }
199
+ catch (err) {
200
+ res.status(400).json({ error: err.message });
201
+ }
202
+ });
203
+ // PATCH /api/chronos/:id/disable
204
+ router.patch('/:id/disable', (req, res) => {
205
+ try {
206
+ const job = repo.disableJob(req.params.id);
207
+ if (!job)
208
+ return res.status(404).json({ error: 'Job not found' });
209
+ res.json(job);
210
+ }
211
+ catch (err) {
212
+ res.status(500).json({ error: err.message });
213
+ }
214
+ });
215
+ // GET /api/chronos/:id/executions
216
+ router.get('/:id/executions', (req, res) => {
217
+ try {
218
+ const query = ExecutionsQuerySchema.safeParse(req.query);
219
+ const limit = query.success ? query.data.limit : 50;
220
+ const job = repo.getJob(req.params.id);
221
+ if (!job)
222
+ return res.status(404).json({ error: 'Job not found' });
223
+ const executions = repo.listExecutions(req.params.id, limit);
224
+ res.json(executions);
225
+ }
226
+ catch (err) {
227
+ res.status(500).json({ error: err.message });
228
+ }
229
+ });
230
+ return router;
231
+ }
232
+ // ─── Config Router ────────────────────────────────────────────────────────────
233
+ export function createChronosConfigRouter(worker) {
234
+ const router = Router();
235
+ const configManager = ConfigManager.getInstance();
236
+ // GET /api/config/chronos
237
+ router.get('/', (req, res) => {
238
+ try {
239
+ res.json(configManager.getChronosConfig());
240
+ }
241
+ catch (err) {
242
+ res.status(500).json({ error: err.message });
243
+ }
244
+ });
245
+ // POST /api/config/chronos
246
+ router.post('/', async (req, res) => {
247
+ const parsed = ChronosConfigSchema.partial().safeParse(req.body);
248
+ if (!parsed.success) {
249
+ return res.status(400).json({ error: 'Invalid input', details: parsed.error.issues });
250
+ }
251
+ try {
252
+ const current = configManager.get();
253
+ const newChronos = { ...configManager.getChronosConfig(), ...parsed.data };
254
+ await configManager.save({ ...current, chronos: newChronos });
255
+ if (parsed.data.check_interval_ms) {
256
+ worker.updateInterval(parsed.data.check_interval_ms);
257
+ }
258
+ const display = DisplayManager.getInstance();
259
+ display.log('Chronos configuration updated via UI', { source: 'Zaion', level: 'info' });
260
+ res.json(configManager.getChronosConfig());
261
+ }
262
+ catch (err) {
263
+ res.status(500).json({ error: err.message });
264
+ }
265
+ });
266
+ return router;
267
+ }
@@ -15,9 +15,11 @@ export class HttpServer {
15
15
  app;
16
16
  server;
17
17
  oracle;
18
- constructor(oracle) {
18
+ chronosWorker;
19
+ constructor(oracle, chronosWorker) {
19
20
  this.app = express();
20
21
  this.oracle = oracle;
22
+ this.chronosWorker = chronosWorker;
21
23
  // Wire Oracle into the webhook dispatcher so triggers use the full agent
22
24
  WebhookDispatcher.setOracle(oracle);
23
25
  this.setupMiddleware();
@@ -53,7 +55,7 @@ export class HttpServer {
53
55
  // The trigger endpoint is public (validated via x-api-key header internally).
54
56
  // All other webhook management endpoints apply authMiddleware internally.
55
57
  this.app.use('/api/webhooks', createWebhooksRouter());
56
- this.app.use('/api', authMiddleware, createApiRouter(this.oracle));
58
+ this.app.use('/api', authMiddleware, createApiRouter(this.oracle, this.chronosWorker));
57
59
  // Serve static frontend from compiled output
58
60
  const uiPath = path.resolve(__dirname, '../ui');
59
61
  this.app.use(express.static(uiPath));