@ducci/jarvis 1.0.79 → 1.0.81
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 +1 -1
- package/src/channels/telegram/index.js +114 -4
- package/src/server/config.js +1 -0
- package/src/server/crons.js +25 -8
- package/src/server/tools.js +10 -3
package/package.json
CHANGED
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
import fs from 'fs';
|
|
2
2
|
import path from 'path';
|
|
3
|
+
import { exec } from 'child_process';
|
|
4
|
+
import { promisify } from 'util';
|
|
5
|
+
const execAsync = promisify(exec);
|
|
6
|
+
import { createRequire } from 'module';
|
|
7
|
+
const _require = createRequire(import.meta.url);
|
|
8
|
+
const { version: JARVIS_VERSION } = _require('../../../package.json');
|
|
3
9
|
import { Bot, InlineKeyboard } from 'grammy';
|
|
4
10
|
import { run } from '@grammyjs/runner';
|
|
5
11
|
import { handleChat, requestAbort } from '../../server/agent.js';
|
|
@@ -90,6 +96,33 @@ async function sendMessage(api, chatId, text, sessionId) {
|
|
|
90
96
|
}
|
|
91
97
|
}
|
|
92
98
|
|
|
99
|
+
// Known model context windows in tokens. Used by /context command.
|
|
100
|
+
// Partial match: checked with model.includes(key) so short keys like 'gpt-4o' match 'openrouter/gpt-4o'.
|
|
101
|
+
const MODEL_CONTEXT_WINDOWS = {
|
|
102
|
+
'claude': 200000, // all claude models (opus, sonnet, haiku)
|
|
103
|
+
'gpt-4o': 128000,
|
|
104
|
+
'gpt-4-turbo': 128000,
|
|
105
|
+
'gpt-3.5': 16385,
|
|
106
|
+
'gemini-1.5-pro': 1000000,
|
|
107
|
+
'gemini-1.5-flash': 1000000,
|
|
108
|
+
'gemini-2': 1000000,
|
|
109
|
+
'llama-3.3': 128000,
|
|
110
|
+
'llama-3.1': 128000,
|
|
111
|
+
'mistral': 32000,
|
|
112
|
+
'deepseek': 64000,
|
|
113
|
+
'glm-5': 200000,
|
|
114
|
+
'glm-4': 128000,
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
function lookupContextWindow(model) {
|
|
118
|
+
if (!model) return null;
|
|
119
|
+
const m = model.toLowerCase();
|
|
120
|
+
for (const [key, size] of Object.entries(MODEL_CONTEXT_WINDOWS)) {
|
|
121
|
+
if (m.includes(key)) return size;
|
|
122
|
+
}
|
|
123
|
+
return null;
|
|
124
|
+
}
|
|
125
|
+
|
|
93
126
|
export async function startTelegramChannel(config) {
|
|
94
127
|
const { token, allowedUserIds } = config.telegram;
|
|
95
128
|
|
|
@@ -160,10 +193,14 @@ export async function startTelegramChannel(config) {
|
|
|
160
193
|
// --- Commands ---
|
|
161
194
|
|
|
162
195
|
await bot.api.setMyCommands([
|
|
163
|
-
{ command: 'new',
|
|
164
|
-
{ command: 'usage',
|
|
165
|
-
{ command: '
|
|
166
|
-
{ command: '
|
|
196
|
+
{ command: 'new', description: 'Reset the active slot (fresh session)' },
|
|
197
|
+
{ command: 'usage', description: 'Token usage for the active slot' },
|
|
198
|
+
{ command: 'context', description: 'Estimated context size vs model limit' },
|
|
199
|
+
{ command: 'stop', description: 'Stop the running agent on the active slot' },
|
|
200
|
+
{ command: 'slots', description: 'Show all slots and their status' },
|
|
201
|
+
{ command: 'version', description: 'Show Jarvis version' },
|
|
202
|
+
{ command: 'update', description: 'Update Jarvis to the latest version' },
|
|
203
|
+
{ command: 'restart', description: 'Restart Jarvis' },
|
|
167
204
|
]);
|
|
168
205
|
|
|
169
206
|
bot.command('usage', async (ctx) => {
|
|
@@ -196,6 +233,79 @@ export async function startTelegramChannel(config) {
|
|
|
196
233
|
);
|
|
197
234
|
});
|
|
198
235
|
|
|
236
|
+
bot.command('version', async (ctx) => {
|
|
237
|
+
const userId = ctx.from?.id;
|
|
238
|
+
if (!allowedUserIds.includes(userId)) return;
|
|
239
|
+
await ctx.reply(`Jarvis v${JARVIS_VERSION}`);
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
bot.command('update', async (ctx) => {
|
|
243
|
+
const userId = ctx.from?.id;
|
|
244
|
+
if (!allowedUserIds.includes(userId)) return;
|
|
245
|
+
await ctx.reply('Updating Jarvis...');
|
|
246
|
+
try {
|
|
247
|
+
const { stdout, stderr } = await execAsync('npm install -g @ducci/jarvis@latest', { timeout: 120000 });
|
|
248
|
+
const out = (stdout + stderr).trim().slice(-1000) || 'Done.';
|
|
249
|
+
await ctx.api.sendMessage(ctx.chat.id, `Update complete:\n${out}`);
|
|
250
|
+
} catch (e) {
|
|
251
|
+
await ctx.api.sendMessage(ctx.chat.id, `Update failed:\n${e.message.slice(0, 1000)}`);
|
|
252
|
+
}
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
bot.command('restart', async (ctx) => {
|
|
256
|
+
const userId = ctx.from?.id;
|
|
257
|
+
if (!allowedUserIds.includes(userId)) return;
|
|
258
|
+
await ctx.reply('Restarting Jarvis...');
|
|
259
|
+
// Fire and forget — process will exit before a response could be sent
|
|
260
|
+
setTimeout(() => execAsync('jarvis restart').catch(() => {}), 500);
|
|
261
|
+
});
|
|
262
|
+
|
|
263
|
+
bot.command('context', async (ctx) => {
|
|
264
|
+
const userId = ctx.from?.id;
|
|
265
|
+
if (!allowedUserIds.includes(userId)) return;
|
|
266
|
+
|
|
267
|
+
const chatId = ctx.chat.id;
|
|
268
|
+
const slot = getActiveSlot(chatId);
|
|
269
|
+
const sessionId = getSessionId(chatId, slot);
|
|
270
|
+
if (!sessionId) {
|
|
271
|
+
await ctx.reply('No active session. Send a message to start one.');
|
|
272
|
+
return;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
const session = await loadSession(sessionId);
|
|
276
|
+
if (!session) {
|
|
277
|
+
await ctx.reply('Could not load session.');
|
|
278
|
+
return;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
const totalMessages = Math.max(0, session.messages.length - 1); // exclude system prompt
|
|
282
|
+
const windowed = session.messages.length <= config.contextWindow + 1
|
|
283
|
+
? session.messages
|
|
284
|
+
: [session.messages[0], ...session.messages.slice(-config.contextWindow)];
|
|
285
|
+
const inContext = Math.max(0, windowed.length - 1);
|
|
286
|
+
const estimatedTokens = Math.round(JSON.stringify(windowed).length / 4);
|
|
287
|
+
const model = config.selectedModel || 'unknown';
|
|
288
|
+
const contextWindow = config.modelContextWindow || lookupContextWindow(model);
|
|
289
|
+
|
|
290
|
+
let lines = [
|
|
291
|
+
`<b>Context — Slot ${slot}</b>`,
|
|
292
|
+
`Model: <code>${escapeHtml(model)}</code>`,
|
|
293
|
+
`Messages on disk: ${totalMessages} | in context: ${inContext}`,
|
|
294
|
+
`Estimated tokens: ~${estimatedTokens.toLocaleString()}`,
|
|
295
|
+
];
|
|
296
|
+
|
|
297
|
+
if (contextWindow) {
|
|
298
|
+
const pct = Math.round((estimatedTokens / contextWindow) * 100);
|
|
299
|
+
const bar = pct >= 90 ? '🔴' : pct >= 70 ? '🟡' : '🟢';
|
|
300
|
+
lines.push(`Model context window: ${contextWindow.toLocaleString()}`);
|
|
301
|
+
lines.push(`Usage: ${bar} ~${pct}%`);
|
|
302
|
+
} else {
|
|
303
|
+
lines.push(`Model context window: unknown`);
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
await ctx.reply(lines.join('\n'), { parse_mode: 'HTML' });
|
|
307
|
+
});
|
|
308
|
+
|
|
199
309
|
bot.command('stop', async (ctx) => {
|
|
200
310
|
const userId = ctx.from?.id;
|
|
201
311
|
if (!allowedUserIds.includes(userId)) return;
|
package/src/server/config.js
CHANGED
|
@@ -72,6 +72,7 @@ export function loadConfig() {
|
|
|
72
72
|
maxIterations: settings.maxIterations || 20,
|
|
73
73
|
maxHandoffs: settings.maxHandoffs || 3,
|
|
74
74
|
contextWindow: settings.contextWindow || 100,
|
|
75
|
+
modelContextWindow: settings.modelContextWindow || null,
|
|
75
76
|
port: settings.port || 18008,
|
|
76
77
|
telegram: {
|
|
77
78
|
token: process.env.TELEGRAM_BOT_TOKEN || null,
|
package/src/server/crons.js
CHANGED
|
@@ -45,6 +45,15 @@ export async function runCron(entry, config) {
|
|
|
45
45
|
const failedApproaches = [];
|
|
46
46
|
const checkpointState = {};
|
|
47
47
|
|
|
48
|
+
// Compact tool call representation for logs: "tool_name first_arg_value"
|
|
49
|
+
function compactToolCalls(toolCalls) {
|
|
50
|
+
return (toolCalls || []).map(tc => {
|
|
51
|
+
const firstVal = Object.values(tc.args || {})[0];
|
|
52
|
+
const argStr = firstVal !== undefined ? ' ' + String(firstVal).slice(0, 80) : '';
|
|
53
|
+
return `${tc.name}${argStr}`;
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
|
|
48
57
|
try {
|
|
49
58
|
while (true) {
|
|
50
59
|
const runStartIndex = session.messages.length;
|
|
@@ -53,9 +62,25 @@ export async function runCron(entry, config) {
|
|
|
53
62
|
run = await runAgentLoop(client, config, session, prepareMessages, usageAccum);
|
|
54
63
|
} catch (e) {
|
|
55
64
|
run = { status: 'error', response: e.message, logSummary: e.message, runToolCalls: [] };
|
|
65
|
+
await appendCronLog(entry.id, {
|
|
66
|
+
cronName: entry.name,
|
|
67
|
+
handoff: handoffCount + 1,
|
|
68
|
+
status: run.status,
|
|
69
|
+
logSummary: run.logSummary,
|
|
70
|
+
toolCalls: [],
|
|
71
|
+
}).catch(e => console.error(`[cron] log error: ${e.message}`));
|
|
56
72
|
break;
|
|
57
73
|
}
|
|
58
74
|
|
|
75
|
+
await appendCronLog(entry.id, {
|
|
76
|
+
cronName: entry.name,
|
|
77
|
+
handoff: handoffCount + 1,
|
|
78
|
+
status: run.status,
|
|
79
|
+
logSummary: run.logSummary,
|
|
80
|
+
...(run.status !== 'checkpoint_reached' && { response: run.response }),
|
|
81
|
+
toolCalls: compactToolCalls(run.runToolCalls),
|
|
82
|
+
}).catch(e => console.error(`[cron] log error: ${e.message}`));
|
|
83
|
+
|
|
59
84
|
if (run.status !== 'checkpoint_reached') break;
|
|
60
85
|
|
|
61
86
|
if (run.checkpoint.failedApproaches?.length > 0) {
|
|
@@ -103,14 +128,6 @@ export async function runCron(entry, config) {
|
|
|
103
128
|
run = { status: 'error', response: e.message, logSummary: e.message, runToolCalls: [] };
|
|
104
129
|
}
|
|
105
130
|
|
|
106
|
-
// Log to cron JSONL
|
|
107
|
-
await appendCronLog(entry.id, {
|
|
108
|
-
cronName: entry.name,
|
|
109
|
-
status: run.status,
|
|
110
|
-
response: run.response,
|
|
111
|
-
logSummary: run.logSummary,
|
|
112
|
-
}).catch(e => console.error(`[cron] log error: ${e.message}`));
|
|
113
|
-
|
|
114
131
|
// once: true — delete after firing
|
|
115
132
|
if (entry.once) {
|
|
116
133
|
try {
|
package/src/server/tools.js
CHANGED
|
@@ -571,12 +571,13 @@ const SEED_TOOLS = {
|
|
|
571
571
|
type: 'function',
|
|
572
572
|
function: {
|
|
573
573
|
name: 'read_cron_log',
|
|
574
|
-
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.',
|
|
574
|
+
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. Each cron execution logs one entry per handoff. Use verbose:true to include tool call details for debugging.',
|
|
575
575
|
parameters: {
|
|
576
576
|
type: 'object',
|
|
577
577
|
properties: {
|
|
578
578
|
id: { type: 'string', description: 'The cron id. Omit to get an overview across all crons.' },
|
|
579
579
|
limit: { type: 'number', description: 'Max entries to return when reading a specific cron. Defaults to 20.' },
|
|
580
|
+
verbose: { type: 'boolean', description: 'Include toolCalls array in each entry. Default false.' },
|
|
580
581
|
},
|
|
581
582
|
required: [],
|
|
582
583
|
},
|
|
@@ -584,6 +585,12 @@ const SEED_TOOLS = {
|
|
|
584
585
|
},
|
|
585
586
|
code: `
|
|
586
587
|
const logsDir = path.join(process.env.HOME, '.jarvis/logs');
|
|
588
|
+
const verbose = !!args.verbose;
|
|
589
|
+
function strip(entry) {
|
|
590
|
+
if (verbose) return entry;
|
|
591
|
+
const { toolCalls, ...rest } = entry;
|
|
592
|
+
return rest;
|
|
593
|
+
}
|
|
587
594
|
if (!args.id) {
|
|
588
595
|
const files = await fs.promises.readdir(logsDir).catch(() => []);
|
|
589
596
|
const cronFiles = files.filter(f => f.startsWith('cron-') && f.endsWith('.jsonl'));
|
|
@@ -596,7 +603,7 @@ const SEED_TOOLS = {
|
|
|
596
603
|
for (const { file } of withMtime.slice(0, 8)) {
|
|
597
604
|
const content = await fs.promises.readFile(path.join(logsDir, file), 'utf8').catch(() => '');
|
|
598
605
|
const lines = content.trim().split('\\n').filter(Boolean);
|
|
599
|
-
allEntries.push(...lines.slice(-5).map(line => JSON.parse(line)));
|
|
606
|
+
allEntries.push(...lines.slice(-5).map(line => strip(JSON.parse(line))));
|
|
600
607
|
}
|
|
601
608
|
allEntries.sort((a, b) => new Date(b.ts) - new Date(a.ts));
|
|
602
609
|
return { status: 'ok', entries: allEntries };
|
|
@@ -604,7 +611,7 @@ const SEED_TOOLS = {
|
|
|
604
611
|
const logFile = path.join(logsDir, 'cron-' + args.id + '.jsonl');
|
|
605
612
|
const content = await fs.promises.readFile(logFile, 'utf8').catch(() => '');
|
|
606
613
|
const lines = content.trim().split('\\n').filter(Boolean);
|
|
607
|
-
const entries = lines.slice(-(args.limit || 20)).map(line => JSON.parse(line));
|
|
614
|
+
const entries = lines.slice(-(args.limit || 20)).map(line => strip(JSON.parse(line)));
|
|
608
615
|
return { status: 'ok', entries };
|
|
609
616
|
`,
|
|
610
617
|
},
|