@ducci/jarvis 1.0.81 → 1.0.83
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/package.json
CHANGED
|
@@ -11,6 +11,7 @@ import { run } from '@grammyjs/runner';
|
|
|
11
11
|
import { handleChat, requestAbort } from '../../server/agent.js';
|
|
12
12
|
import { loadSession } from '../../server/sessions.js';
|
|
13
13
|
import { PATHS } from '../../server/config.js';
|
|
14
|
+
import { isRunningCron, getRunningCrons } from '../../server/cron-scheduler.js';
|
|
14
15
|
import { load, save } from './sessions.js';
|
|
15
16
|
import { describeImage } from '../../server/vision.js';
|
|
16
17
|
|
|
@@ -123,6 +124,54 @@ function lookupContextWindow(model) {
|
|
|
123
124
|
return null;
|
|
124
125
|
}
|
|
125
126
|
|
|
127
|
+
function nextCronDate(expression) {
|
|
128
|
+
try {
|
|
129
|
+
const parts = expression.trim().split(/\s+/);
|
|
130
|
+
if (parts.length !== 5) return null;
|
|
131
|
+
const [minE, hourE, domE, monE, dowE] = parts;
|
|
132
|
+
|
|
133
|
+
function matchField(expr, val) {
|
|
134
|
+
if (expr === '*') return true;
|
|
135
|
+
if (expr.includes(',')) return expr.split(',').some(p => matchField(p, val));
|
|
136
|
+
if (expr.includes('/')) {
|
|
137
|
+
const [range, step] = expr.split('/');
|
|
138
|
+
const s = parseInt(step, 10);
|
|
139
|
+
if (range === '*') return val % s === 0;
|
|
140
|
+
const [lo, hi] = range.split('-').map(Number);
|
|
141
|
+
return val >= lo && val <= hi && (val - lo) % s === 0;
|
|
142
|
+
}
|
|
143
|
+
if (expr.includes('-')) {
|
|
144
|
+
const [lo, hi] = expr.split('-').map(Number);
|
|
145
|
+
return val >= lo && val <= hi;
|
|
146
|
+
}
|
|
147
|
+
return parseInt(expr, 10) === val;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
const candidate = new Date();
|
|
151
|
+
candidate.setSeconds(0, 0);
|
|
152
|
+
candidate.setMinutes(candidate.getMinutes() + 1);
|
|
153
|
+
|
|
154
|
+
for (let i = 0; i < 10080; i++) {
|
|
155
|
+
if (matchField(minE, candidate.getMinutes()) &&
|
|
156
|
+
matchField(hourE, candidate.getHours()) &&
|
|
157
|
+
matchField(domE, candidate.getDate()) &&
|
|
158
|
+
matchField(monE, candidate.getMonth() + 1) &&
|
|
159
|
+
matchField(dowE, candidate.getDay())) {
|
|
160
|
+
return candidate;
|
|
161
|
+
}
|
|
162
|
+
candidate.setMinutes(candidate.getMinutes() + 1);
|
|
163
|
+
}
|
|
164
|
+
return null;
|
|
165
|
+
} catch { return null; }
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
function formatMinutes(mins) {
|
|
169
|
+
if (mins < 60) return `${mins}m`;
|
|
170
|
+
const h = Math.floor(mins / 60);
|
|
171
|
+
const m = mins % 60;
|
|
172
|
+
return m > 0 ? `${h}h ${m}m` : `${h}h`;
|
|
173
|
+
}
|
|
174
|
+
|
|
126
175
|
export async function startTelegramChannel(config) {
|
|
127
176
|
const { token, allowedUserIds } = config.telegram;
|
|
128
177
|
|
|
@@ -198,6 +247,7 @@ export async function startTelegramChannel(config) {
|
|
|
198
247
|
{ command: 'context', description: 'Estimated context size vs model limit' },
|
|
199
248
|
{ command: 'stop', description: 'Stop the running agent on the active slot' },
|
|
200
249
|
{ command: 'slots', description: 'Show all slots and their status' },
|
|
250
|
+
{ command: 'crons', description: 'Show all crons, running status and next run' },
|
|
201
251
|
{ command: 'version', description: 'Show Jarvis version' },
|
|
202
252
|
{ command: 'update', description: 'Update Jarvis to the latest version' },
|
|
203
253
|
{ command: 'restart', description: 'Restart Jarvis' },
|
|
@@ -472,6 +522,57 @@ export async function startTelegramChannel(config) {
|
|
|
472
522
|
await ctx.answerCallbackQuery();
|
|
473
523
|
});
|
|
474
524
|
|
|
525
|
+
bot.command('crons', async (ctx) => {
|
|
526
|
+
const userId = ctx.from?.id;
|
|
527
|
+
if (!allowedUserIds.includes(userId)) return;
|
|
528
|
+
|
|
529
|
+
let entries = [];
|
|
530
|
+
try {
|
|
531
|
+
entries = JSON.parse(fs.readFileSync(PATHS.cronsFile, 'utf8'));
|
|
532
|
+
} catch { /* no crons file */ }
|
|
533
|
+
|
|
534
|
+
if (entries.length === 0) {
|
|
535
|
+
await ctx.reply('Keine Crons konfiguriert.');
|
|
536
|
+
return;
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
const runningMap = getRunningCrons(); // id -> startDate
|
|
540
|
+
const now = Date.now();
|
|
541
|
+
const lines = ['<b>Crons:</b>'];
|
|
542
|
+
|
|
543
|
+
for (const entry of entries) {
|
|
544
|
+
const running = isRunningCron(entry.id);
|
|
545
|
+
let statusLine;
|
|
546
|
+
|
|
547
|
+
if (running) {
|
|
548
|
+
const startDate = runningMap.get(entry.id);
|
|
549
|
+
let elapsed = '';
|
|
550
|
+
if (startDate) {
|
|
551
|
+
const secs = Math.floor((now - startDate.getTime()) / 1000);
|
|
552
|
+
const m = Math.floor(secs / 60);
|
|
553
|
+
const s = secs % 60;
|
|
554
|
+
elapsed = m > 0 ? ` (seit ${m}m ${s}s)` : ` (seit ${s}s)`;
|
|
555
|
+
}
|
|
556
|
+
statusLine = `🟢 läuft${elapsed}`;
|
|
557
|
+
} else {
|
|
558
|
+
const nextDate = nextCronDate(entry.schedule);
|
|
559
|
+
if (nextDate) {
|
|
560
|
+
const diffMins = Math.round((nextDate.getTime() - now) / 60000);
|
|
561
|
+
statusLine = `⏰ in ${formatMinutes(diffMins)}`;
|
|
562
|
+
} else {
|
|
563
|
+
statusLine = `⏰ unbekannt`;
|
|
564
|
+
}
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
const onceMark = entry.once ? ' <i>(einmalig)</i>' : '';
|
|
568
|
+
lines.push(`\n<b>${escapeHtml(entry.name)}</b>${onceMark}`);
|
|
569
|
+
lines.push(`${statusLine}`);
|
|
570
|
+
lines.push(`<code>${escapeHtml(entry.schedule)}</code>`);
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
await ctx.reply(lines.join('\n'), { parse_mode: 'HTML' });
|
|
574
|
+
});
|
|
575
|
+
|
|
475
576
|
// Runs one or more batches until the pending queue is drained.
|
|
476
577
|
// Each iteration takes all currently pending messages, merges them into a
|
|
477
578
|
// single user turn, calls handleChat once, and sends one response.
|
package/src/server/config.js
CHANGED
|
@@ -71,7 +71,7 @@ export function loadConfig() {
|
|
|
71
71
|
fallbackModel: settings.fallbackModel || (provider === 'anthropic' ? 'claude-haiku-4-5-20251001' : 'openrouter/free'),
|
|
72
72
|
maxIterations: settings.maxIterations || 20,
|
|
73
73
|
maxHandoffs: settings.maxHandoffs || 3,
|
|
74
|
-
contextWindow: settings.contextWindow ||
|
|
74
|
+
contextWindow: settings.contextWindow || 300,
|
|
75
75
|
modelContextWindow: settings.modelContextWindow || null,
|
|
76
76
|
port: settings.port || 18008,
|
|
77
77
|
telegram: {
|
|
@@ -3,9 +3,32 @@ import cron from 'node-cron';
|
|
|
3
3
|
// Maps cron id -> active node-cron task
|
|
4
4
|
const tasks = new Map();
|
|
5
5
|
|
|
6
|
+
// Tracks which cron ids are currently executing
|
|
7
|
+
const runningCrons = new Set();
|
|
8
|
+
// Maps cron id -> Date when it started running
|
|
9
|
+
const cronStartTimes = new Map();
|
|
10
|
+
|
|
6
11
|
let _runCron = null;
|
|
7
12
|
let _config = null;
|
|
8
13
|
|
|
14
|
+
export function setRunning(id) {
|
|
15
|
+
runningCrons.add(id);
|
|
16
|
+
cronStartTimes.set(id, new Date());
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function clearRunning(id) {
|
|
20
|
+
runningCrons.delete(id);
|
|
21
|
+
cronStartTimes.delete(id);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function isRunningCron(id) {
|
|
25
|
+
return runningCrons.has(id);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function getRunningCrons() {
|
|
29
|
+
return new Map(cronStartTimes);
|
|
30
|
+
}
|
|
31
|
+
|
|
9
32
|
export function init(runCronFn, config) {
|
|
10
33
|
_runCron = runCronFn;
|
|
11
34
|
_config = config;
|
package/src/server/crons.js
CHANGED
|
@@ -22,6 +22,7 @@ async function appendCronLog(cronId, entry) {
|
|
|
22
22
|
|
|
23
23
|
export async function runCron(entry, config) {
|
|
24
24
|
console.log(`[cron] running "${entry.name}"`);
|
|
25
|
+
cronScheduler.setRunning(entry.id);
|
|
25
26
|
|
|
26
27
|
const systemPromptTemplate = loadSystemPrompt();
|
|
27
28
|
const session = createSession(systemPromptTemplate);
|
|
@@ -128,6 +129,8 @@ export async function runCron(entry, config) {
|
|
|
128
129
|
run = { status: 'error', response: e.message, logSummary: e.message, runToolCalls: [] };
|
|
129
130
|
}
|
|
130
131
|
|
|
132
|
+
cronScheduler.clearRunning(entry.id);
|
|
133
|
+
|
|
131
134
|
// once: true — delete after firing
|
|
132
135
|
if (entry.once) {
|
|
133
136
|
try {
|