@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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ducci/jarvis",
3
- "version": "1.0.81",
3
+ "version": "1.0.83",
4
4
  "description": "A fully automated agent system that lives on a server.",
5
5
  "main": "./src/index.js",
6
6
  "type": "module",
@@ -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.
@@ -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 || 100,
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;
@@ -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 {