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.
- package/README.md +44 -2
- package/dist/channels/telegram.js +264 -7
- package/dist/cli/commands/start.js +9 -1
- package/dist/config/manager.js +18 -1
- package/dist/config/schemas.js +6 -0
- package/dist/devkit/tools/browser.js +167 -51
- package/dist/http/api.js +12 -2
- package/dist/http/routers/chronos.js +267 -0
- package/dist/http/server.js +4 -2
- package/dist/runtime/apoc.js +164 -141
- package/dist/runtime/chronos/parser.js +215 -0
- package/dist/runtime/chronos/parser.test.js +63 -0
- package/dist/runtime/chronos/repository.js +244 -0
- package/dist/runtime/chronos/worker.js +141 -0
- package/dist/runtime/chronos/worker.test.js +120 -0
- package/dist/runtime/display.js +3 -0
- package/dist/runtime/memory/sati/repository.js +1 -1
- package/dist/runtime/memory/sqlite.js +73 -49
- package/dist/runtime/oracle.js +28 -4
- package/dist/runtime/providers/factory.js +1 -1
- package/dist/runtime/tasks/repository.js +24 -7
- package/dist/runtime/tasks/worker.js +12 -15
- package/dist/runtime/tools/chronos-tools.js +181 -0
- package/dist/runtime/tools/index.js +2 -0
- package/dist/runtime/tools/time-verify-tools.js +70 -0
- package/dist/ui/assets/index-BDWWF6gM.css +1 -0
- package/dist/ui/assets/index-DVQvTlPe.js +111 -0
- package/dist/ui/index.html +2 -2
- package/dist/ui/sw.js +1 -1
- package/package.json +7 -1
- package/dist/ui/assets/index-BC40Mg99.js +0 -112
- package/dist/ui/assets/index-Dgdart9S.css +0 -1
|
@@ -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
|
-
|
|
300
|
-
|
|
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:
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
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:
|
|
311
|
-
const res = await fetch(
|
|
312
|
-
method:
|
|
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
|
-
|
|
315
|
-
|
|
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(
|
|
348
|
+
signal: AbortSignal.timeout(20000),
|
|
320
349
|
});
|
|
321
350
|
if (!res.ok) {
|
|
322
|
-
return JSON.stringify({ success: false,
|
|
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
|
|
329
|
-
const
|
|
330
|
-
|
|
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 <
|
|
333
|
-
const
|
|
334
|
-
|
|
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
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
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
|
|
438
|
+
if (!results.length) {
|
|
344
439
|
return JSON.stringify({
|
|
345
440
|
success: false,
|
|
346
|
-
query,
|
|
347
|
-
error:
|
|
441
|
+
query: refinedQuery,
|
|
442
|
+
error: "No valid results after filtering",
|
|
348
443
|
});
|
|
349
444
|
}
|
|
350
|
-
|
|
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({
|
|
477
|
+
return JSON.stringify({
|
|
478
|
+
success: false,
|
|
479
|
+
error: err.message,
|
|
480
|
+
});
|
|
354
481
|
}
|
|
355
482
|
}, {
|
|
356
|
-
name:
|
|
357
|
-
description:
|
|
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()
|
|
362
|
-
num_results: z
|
|
363
|
-
|
|
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
|
|
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
|
+
}
|
package/dist/http/server.js
CHANGED
|
@@ -15,9 +15,11 @@ export class HttpServer {
|
|
|
15
15
|
app;
|
|
16
16
|
server;
|
|
17
17
|
oracle;
|
|
18
|
-
|
|
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));
|