@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ducci/jarvis",
3
- "version": "1.0.79",
3
+ "version": "1.0.81",
4
4
  "description": "A fully automated agent system that lives on a server.",
5
5
  "main": "./src/index.js",
6
6
  "type": "module",
@@ -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', description: 'Reset the active slot (fresh session)' },
164
- { command: 'usage', description: 'Token usage for the active slot' },
165
- { command: 'stop', description: 'Stop the running agent on the active slot' },
166
- { command: 'slots', description: 'Show all slots and their status' },
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;
@@ -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,
@@ -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 {
@@ -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
  },