@ducci/jarvis 1.0.80 → 1.0.82
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' },
|
|
@@ -278,15 +328,19 @@ export async function startTelegramChannel(config) {
|
|
|
278
328
|
return;
|
|
279
329
|
}
|
|
280
330
|
|
|
281
|
-
const
|
|
282
|
-
const
|
|
331
|
+
const totalMessages = Math.max(0, session.messages.length - 1); // exclude system prompt
|
|
332
|
+
const windowed = session.messages.length <= config.contextWindow + 1
|
|
333
|
+
? session.messages
|
|
334
|
+
: [session.messages[0], ...session.messages.slice(-config.contextWindow)];
|
|
335
|
+
const inContext = Math.max(0, windowed.length - 1);
|
|
336
|
+
const estimatedTokens = Math.round(JSON.stringify(windowed).length / 4);
|
|
283
337
|
const model = config.selectedModel || 'unknown';
|
|
284
338
|
const contextWindow = config.modelContextWindow || lookupContextWindow(model);
|
|
285
339
|
|
|
286
340
|
let lines = [
|
|
287
341
|
`<b>Context — Slot ${slot}</b>`,
|
|
288
342
|
`Model: <code>${escapeHtml(model)}</code>`,
|
|
289
|
-
`Messages in
|
|
343
|
+
`Messages on disk: ${totalMessages} | in context: ${inContext}`,
|
|
290
344
|
`Estimated tokens: ~${estimatedTokens.toLocaleString()}`,
|
|
291
345
|
];
|
|
292
346
|
|
|
@@ -468,6 +522,57 @@ export async function startTelegramChannel(config) {
|
|
|
468
522
|
await ctx.answerCallbackQuery();
|
|
469
523
|
});
|
|
470
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
|
+
|
|
471
576
|
// Runs one or more batches until the pending queue is drained.
|
|
472
577
|
// Each iteration takes all currently pending messages, merges them into a
|
|
473
578
|
// single user turn, calls handleChat once, and sends one response.
|
|
@@ -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 {
|