@ducci/jarvis 1.0.44 → 1.0.46
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/docs/crons.md +5 -4
- package/docs/system-prompt.md +2 -2
- package/package.json +1 -1
- package/src/server/agent.js +3 -0
- package/src/server/tools.js +59 -53
package/docs/crons.md
CHANGED
|
@@ -51,7 +51,7 @@ For one-time tasks specified as relative times ("in 2 hours", "at 3pm today"), t
|
|
|
51
51
|
|
|
52
52
|
Notification is opt-in via the prompt. Include this in the prompt when you want a notification:
|
|
53
53
|
|
|
54
|
-
> "When done, use `send_telegram_message` to notify the user with the result."
|
|
54
|
+
> "When done, use `send_telegram_message` to notify the user with the result. Prefix the message with [Cron: \"backup-nightly\" | <current timestamp>]."
|
|
55
55
|
|
|
56
56
|
If you don't want a notification, omit it. The agent follows the prompt literally — conditional notifications work naturally:
|
|
57
57
|
|
|
@@ -59,7 +59,7 @@ If you don't want a notification, omit it. The agent follows the prompt literall
|
|
|
59
59
|
|
|
60
60
|
## Dynamic Scheduling
|
|
61
61
|
|
|
62
|
-
When `create_cron` runs successfully, the agent loop immediately
|
|
62
|
+
When `create_cron`, `update_cron`, or `delete_cron` runs successfully, the agent loop immediately updates the in-memory scheduler — no server restart required.
|
|
63
63
|
|
|
64
64
|
On server restart, all crons in `crons.json` are re-loaded and rescheduled. `once: true` crons that already fired (and deleted themselves) are gone from the file and will not re-run.
|
|
65
65
|
|
|
@@ -77,7 +77,7 @@ Each cron has its own JSONL log at `~/.jarvis/logs/cron-<id>.jsonl`. One entry p
|
|
|
77
77
|
}
|
|
78
78
|
```
|
|
79
79
|
|
|
80
|
-
Use `read_cron_log` to inspect
|
|
80
|
+
Use `read_cron_log` with a cron id to inspect a specific cron, or without an id to get an overview of the last 8 active crons (5 entries each), sorted by time. Ask Jarvis "did my backup run last night?" and it will call `list_crons` + `read_cron_log`.
|
|
81
81
|
|
|
82
82
|
## Tools
|
|
83
83
|
|
|
@@ -85,8 +85,9 @@ Use `read_cron_log` to inspect past runs. Ask Jarvis "did my backup run last nig
|
|
|
85
85
|
|---|---|
|
|
86
86
|
| `create_cron` | Schedule a new cron job |
|
|
87
87
|
| `list_crons` | List all active crons |
|
|
88
|
+
| `update_cron` | Modify an existing cron (name, schedule, prompt, once) |
|
|
88
89
|
| `delete_cron` | Remove a cron by name or id |
|
|
89
|
-
| `read_cron_log` | Read execution history for
|
|
90
|
+
| `read_cron_log` | Read execution history — omit id for cross-cron overview |
|
|
90
91
|
| `get_current_time` | Get current server time for relative scheduling |
|
|
91
92
|
| `send_telegram_message` | Send a proactive message to the Telegram user |
|
|
92
93
|
|
package/docs/system-prompt.md
CHANGED
|
@@ -26,11 +26,11 @@ Only the most recent messages are included in your context (sliding window). Old
|
|
|
26
26
|
You can schedule recurring or one-time tasks using cron jobs.
|
|
27
27
|
|
|
28
28
|
- Use `create_cron` when the user wants to schedule something — even if they don't say "cron". Triggers: "every night", "every 2 hours", "remind me at 3pm", "notify me in 2 hours", "check X every Monday", etc.
|
|
29
|
-
- Call `get_current_time` first when the user specifies a
|
|
29
|
+
- Call `get_current_time` first when the user specifies a time. Note: `get_current_time` returns server time — if you know the user's timezone, convert the desired user-local time to server time before computing the cron expression.
|
|
30
30
|
- The `prompt` stored in the cron is executed by a fresh agent with no prior conversation context. Write it as a complete, self-contained instruction.
|
|
31
31
|
- If the user wants to be notified, include "use send_telegram_message to notify the user with the result" in the prompt. If they explicitly don't want a notification, omit it.
|
|
32
32
|
- For one-time tasks, set `once: true` — the cron deletes itself after firing.
|
|
33
|
-
- Use `list_crons` to show active crons, `delete_cron` to remove one, `read_cron_log` to inspect past runs.
|
|
33
|
+
- Use `list_crons` to show active crons, `update_cron` to modify one, `delete_cron` to remove one, `read_cron_log` to inspect past runs.
|
|
34
34
|
|
|
35
35
|
## Skills
|
|
36
36
|
|
package/package.json
CHANGED
package/src/server/agent.js
CHANGED
|
@@ -241,6 +241,9 @@ export async function runAgentLoop(client, config, session, prepareMessages, usa
|
|
|
241
241
|
if (toolName === 'create_cron') {
|
|
242
242
|
const cronEntry = JSON.parse(resultStr)?.cron;
|
|
243
243
|
if (cronEntry) cronScheduler.schedule(cronEntry);
|
|
244
|
+
} else if (toolName === 'update_cron') {
|
|
245
|
+
const cronEntry = JSON.parse(resultStr)?.cron;
|
|
246
|
+
if (cronEntry) { cronScheduler.unschedule(cronEntry.id); cronScheduler.schedule(cronEntry); }
|
|
244
247
|
} else if (toolName === 'delete_cron') {
|
|
245
248
|
const id = JSON.parse(resultStr)?.id;
|
|
246
249
|
if (id) cronScheduler.unschedule(id);
|
package/src/server/tools.js
CHANGED
|
@@ -364,7 +364,7 @@ const SEED_TOOLS = {
|
|
|
364
364
|
type: 'function',
|
|
365
365
|
function: {
|
|
366
366
|
name: 'get_current_time',
|
|
367
|
-
description: 'Returns the current server time.
|
|
367
|
+
description: 'Returns the current server time. Always call this before creating a cron. Note: returns server time — if you know the user\'s timezone, convert the desired user-local time to server time before computing the cron expression.',
|
|
368
368
|
parameters: { type: 'object', properties: {}, required: [] },
|
|
369
369
|
},
|
|
370
370
|
},
|
|
@@ -383,13 +383,13 @@ const SEED_TOOLS = {
|
|
|
383
383
|
type: 'function',
|
|
384
384
|
function: {
|
|
385
385
|
name: 'create_cron',
|
|
386
|
-
description: 'Schedule a recurring or one-time task. The prompt is executed by a fresh agent with no prior context — write it as a self-contained task. For one-time tasks (e.g. "remind me in 2 hours"), set once: true.
|
|
386
|
+
description: 'Schedule a recurring or one-time task. The prompt is executed by a fresh agent with no prior context — write it as a self-contained task. For one-time tasks (e.g. "remind me in 2 hours"), set once: true. Always call get_current_time first and convert user-local time to server time before computing the cron expression.',
|
|
387
387
|
parameters: {
|
|
388
388
|
type: 'object',
|
|
389
389
|
properties: {
|
|
390
390
|
name: { type: 'string', description: 'Short identifier for this cron, e.g. "backup-nightly".' },
|
|
391
391
|
schedule: { type: 'string', description: 'Cron expression, e.g. "0 3 * * *" for 3am daily. For a one-time task, compute the exact time from get_current_time and express it as a cron expression.' },
|
|
392
|
-
prompt: { type: 'string', description: 'The task prompt the agent will receive when this cron fires. Must be self-contained.
|
|
392
|
+
prompt: { type: 'string', description: 'The task prompt the agent will receive when this cron fires. Must be self-contained. If notification is desired, include: "use send_telegram_message to notify the user with the result. Prefix the message with [Cron: \"<name>\" | <timestamp>] where <name> is the cron name and <timestamp> is the current date and time."' },
|
|
393
393
|
once: { type: 'boolean', description: 'If true, the cron deletes itself after firing once. Use for one-time reminders or tasks.' },
|
|
394
394
|
},
|
|
395
395
|
required: ['name', 'schedule', 'prompt'],
|
|
@@ -429,6 +429,40 @@ const SEED_TOOLS = {
|
|
|
429
429
|
return { status: 'ok', crons };
|
|
430
430
|
`,
|
|
431
431
|
},
|
|
432
|
+
update_cron: {
|
|
433
|
+
definition: {
|
|
434
|
+
type: 'function',
|
|
435
|
+
function: {
|
|
436
|
+
name: 'update_cron',
|
|
437
|
+
description: 'Update an existing cron job. Only the fields you provide will be changed. If updating the schedule, call get_current_time first and convert user-local time to server time before computing the cron expression.',
|
|
438
|
+
parameters: {
|
|
439
|
+
type: 'object',
|
|
440
|
+
properties: {
|
|
441
|
+
id: { type: 'string', description: 'The cron id to update.' },
|
|
442
|
+
name: { type: 'string', description: 'New name.' },
|
|
443
|
+
schedule: { type: 'string', description: 'New cron expression.' },
|
|
444
|
+
prompt: { type: 'string', description: 'New prompt.' },
|
|
445
|
+
once: { type: 'boolean', description: 'New once value.' },
|
|
446
|
+
},
|
|
447
|
+
required: ['id'],
|
|
448
|
+
},
|
|
449
|
+
},
|
|
450
|
+
},
|
|
451
|
+
code: `
|
|
452
|
+
const cronsFile = path.join(process.env.HOME, '.jarvis/data/crons.json');
|
|
453
|
+
const crons = JSON.parse(await fs.promises.readFile(cronsFile, 'utf8').catch(() => '[]'));
|
|
454
|
+
const idx = crons.findIndex(c => c.id === args.id);
|
|
455
|
+
if (idx === -1) return { status: 'not_found' };
|
|
456
|
+
const updated = { ...crons[idx] };
|
|
457
|
+
if (args.name !== undefined) updated.name = args.name;
|
|
458
|
+
if (args.schedule !== undefined) updated.schedule = args.schedule;
|
|
459
|
+
if (args.prompt !== undefined) updated.prompt = args.prompt;
|
|
460
|
+
if (args.once !== undefined) updated.once = args.once;
|
|
461
|
+
crons[idx] = updated;
|
|
462
|
+
await fs.promises.writeFile(cronsFile, JSON.stringify(crons, null, 2), 'utf8');
|
|
463
|
+
return { status: 'ok', cron: updated };
|
|
464
|
+
`,
|
|
465
|
+
},
|
|
432
466
|
delete_cron: {
|
|
433
467
|
definition: {
|
|
434
468
|
type: 'function',
|
|
@@ -513,24 +547,40 @@ const SEED_TOOLS = {
|
|
|
513
547
|
type: 'function',
|
|
514
548
|
function: {
|
|
515
549
|
name: 'read_cron_log',
|
|
516
|
-
description: 'Read
|
|
550
|
+
description: 'Read cron execution logs. Without id: returns recent runs across all crons (last 8 most recently active cron files, 5 entries each). With id: returns runs for that specific cron.',
|
|
517
551
|
parameters: {
|
|
518
552
|
type: 'object',
|
|
519
553
|
properties: {
|
|
520
|
-
id: { type: 'string', description: 'The cron id.' },
|
|
521
|
-
limit: { type: 'number', description: 'Max entries to return. Defaults to 20.' },
|
|
554
|
+
id: { type: 'string', description: 'The cron id. Omit to get an overview across all crons.' },
|
|
555
|
+
limit: { type: 'number', description: 'Max entries to return when reading a specific cron. Defaults to 20.' },
|
|
522
556
|
},
|
|
523
|
-
required: [
|
|
557
|
+
required: [],
|
|
524
558
|
},
|
|
525
559
|
},
|
|
526
560
|
},
|
|
527
561
|
code: `
|
|
528
562
|
const logsDir = path.join(process.env.HOME, '.jarvis/logs');
|
|
563
|
+
if (!args.id) {
|
|
564
|
+
const files = await fs.promises.readdir(logsDir).catch(() => []);
|
|
565
|
+
const cronFiles = files.filter(f => f.startsWith('cron-') && f.endsWith('.jsonl'));
|
|
566
|
+
const withMtime = await Promise.all(cronFiles.map(async f => {
|
|
567
|
+
const stat = await fs.promises.stat(path.join(logsDir, f));
|
|
568
|
+
return { file: f, mtime: stat.mtimeMs };
|
|
569
|
+
}));
|
|
570
|
+
withMtime.sort((a, b) => b.mtime - a.mtime);
|
|
571
|
+
const allEntries = [];
|
|
572
|
+
for (const { file } of withMtime.slice(0, 8)) {
|
|
573
|
+
const content = await fs.promises.readFile(path.join(logsDir, file), 'utf8').catch(() => '');
|
|
574
|
+
const lines = content.trim().split('\\n').filter(Boolean);
|
|
575
|
+
allEntries.push(...lines.slice(-5).map(line => JSON.parse(line)));
|
|
576
|
+
}
|
|
577
|
+
allEntries.sort((a, b) => new Date(b.ts) - new Date(a.ts));
|
|
578
|
+
return { status: 'ok', entries: allEntries };
|
|
579
|
+
}
|
|
529
580
|
const logFile = path.join(logsDir, 'cron-' + args.id + '.jsonl');
|
|
530
581
|
const content = await fs.promises.readFile(logFile, 'utf8').catch(() => '');
|
|
531
582
|
const lines = content.trim().split('\\n').filter(Boolean);
|
|
532
|
-
const
|
|
533
|
-
const entries = lines.slice(-limit).map(line => JSON.parse(line));
|
|
583
|
+
const entries = lines.slice(-(args.limit || 20)).map(line => JSON.parse(line));
|
|
534
584
|
return { status: 'ok', entries };
|
|
535
585
|
`,
|
|
536
586
|
},
|
|
@@ -562,50 +612,6 @@ const SEED_TOOLS = {
|
|
|
562
612
|
return { status: 'ok', name: args.name, content };
|
|
563
613
|
`,
|
|
564
614
|
},
|
|
565
|
-
get_recent_sessions: {
|
|
566
|
-
definition: {
|
|
567
|
-
type: 'function',
|
|
568
|
-
function: {
|
|
569
|
-
name: 'get_recent_sessions',
|
|
570
|
-
description: 'Returns the most recent sessions with their titles and timestamps. Use this to find previous conversations.',
|
|
571
|
-
parameters: {
|
|
572
|
-
type: 'object',
|
|
573
|
-
properties: {
|
|
574
|
-
limit: {
|
|
575
|
-
type: 'number',
|
|
576
|
-
description: 'Number of recent sessions to return. Defaults to 2.',
|
|
577
|
-
},
|
|
578
|
-
},
|
|
579
|
-
required: [],
|
|
580
|
-
},
|
|
581
|
-
},
|
|
582
|
-
},
|
|
583
|
-
code: `const logsDir = path.join(process.env.HOME, '.jarvis/logs'); const limit = args.limit || 2; const files = await fs.promises.readdir(logsDir).catch(() => []); const sessionFiles = files.filter(f => f.startsWith('session-') && f.endsWith('.jsonl')); const sessions = []; for (const file of sessionFiles) { const sessionId = file.replace('session-', '').replace('.jsonl', ''); const content = await fs.promises.readFile(path.join(logsDir, file), 'utf8'); const lines = content.trim().split('\\n').filter(Boolean); if (lines.length === 0) continue; const firstEntry = JSON.parse(lines[0]); const lastEntry = JSON.parse(lines[lines.length - 1]); sessions.push({ sessionId, title: (firstEntry.logSummary || '').substring(0, 80), lastTs: lastEntry.ts }); } sessions.sort((a, b) => new Date(b.lastTs) - new Date(a.lastTs)); return { status: 'ok', sessions: sessions.slice(0, limit) };`,
|
|
584
|
-
},
|
|
585
|
-
read_session_log: {
|
|
586
|
-
definition: {
|
|
587
|
-
type: 'function',
|
|
588
|
-
function: {
|
|
589
|
-
name: 'read_session_log',
|
|
590
|
-
description: 'Read JSONL log entries for a given session. Use this to inspect what happened in a previous run, including tool calls, errors, and summaries.',
|
|
591
|
-
parameters: {
|
|
592
|
-
type: 'object',
|
|
593
|
-
properties: {
|
|
594
|
-
sessionId: {
|
|
595
|
-
type: 'string',
|
|
596
|
-
description: 'The session ID to read logs for.',
|
|
597
|
-
},
|
|
598
|
-
limit: {
|
|
599
|
-
type: 'number',
|
|
600
|
-
description: 'Maximum number of entries to return (from the end). Defaults to 20.',
|
|
601
|
-
},
|
|
602
|
-
},
|
|
603
|
-
required: ['sessionId'],
|
|
604
|
-
},
|
|
605
|
-
},
|
|
606
|
-
},
|
|
607
|
-
code: `const logsDir = path.join(process.env.HOME, '.jarvis/logs'); const logFile = path.join(logsDir, 'session-' + args.sessionId + '.jsonl'); const content = await fs.promises.readFile(logFile, 'utf8').catch(() => ''); const lines = content.trim().split('\\n').filter(Boolean); const limit = args.limit || 20; const entries = lines.slice(-limit).map(line => JSON.parse(line)); return { status: 'ok', entries };`,
|
|
608
|
-
},
|
|
609
615
|
};
|
|
610
616
|
|
|
611
617
|
export function seedTools() {
|