@ducci/jarvis 1.0.42 → 1.0.44

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.
@@ -52,7 +52,7 @@ There are two types of responses depending on whether you need to use tools:
52
52
  "logSummary": "A concise explanation of what you did and why, written for a human reading the logs."
53
53
  }
54
54
 
55
- The `response` value must be a plain text string — never an array or object. If you need to present structured data (e.g. a list of items), format it as text within the string value.
55
+ The `response` value must be a string — never an array or object. Use HTML formatting tags for readability: <b>bold</b>, <i>italic</i>, <code>inline code</code>, <pre>code blocks</pre>, <blockquote>quotes</blockquote>. Never use Markdown formatting (no **, __, `, or ```). If you need to present structured data (e.g. a list of items), format it as text within the string value.
56
56
 
57
57
  Never include markdown code fences, preamble, or any text outside this JSON object. If you cannot complete a task, explain why in the `response` field — still as valid JSON.
58
58
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ducci/jarvis",
3
- "version": "1.0.42",
3
+ "version": "1.0.44",
4
4
  "description": "A fully automated agent system that lives on a server.",
5
5
  "main": "./src/index.js",
6
6
  "type": "module",
@@ -1,9 +1,41 @@
1
+ import fs from 'fs';
2
+ import path from 'path';
1
3
  import { Bot } from 'grammy';
2
4
  import { run } from '@grammyjs/runner';
3
5
  import { handleChat } from '../../server/agent.js';
4
6
  import { loadSession } from '../../server/sessions.js';
7
+ import { PATHS } from '../../server/config.js';
5
8
  import { load, save } from './sessions.js';
6
9
 
10
+ async function sendMessage(api, chatId, text, sessionId) {
11
+ const MAX_TG = 4096;
12
+ const chunks = [];
13
+ for (let i = 0; i < text.length; i += MAX_TG) {
14
+ chunks.push(text.slice(i, i + MAX_TG));
15
+ }
16
+ for (const chunk of chunks) {
17
+ try {
18
+ await api.sendMessage(chatId, chunk, { parse_mode: 'HTML' });
19
+ } catch (e) {
20
+ if (e.error_code === 400) {
21
+ console.error(`[telegram] HTML parse error chat_id=${chatId}, falling back to plaintext: ${e.description}`);
22
+ if (sessionId) {
23
+ const logFile = path.join(PATHS.logsDir, `session-${sessionId}.jsonl`);
24
+ fs.promises.appendFile(logFile, JSON.stringify({
25
+ ts: new Date().toISOString(),
26
+ sessionId,
27
+ type: 'telegram_parse_error',
28
+ description: e.description || e.message,
29
+ }) + '\n', 'utf8').catch(() => {});
30
+ }
31
+ await api.sendMessage(chatId, chunk);
32
+ } else {
33
+ throw e;
34
+ }
35
+ }
36
+ }
37
+ }
38
+
7
39
  export async function startTelegramChannel(config) {
8
40
  const { token, allowedUserIds } = config.telegram;
9
41
 
@@ -103,23 +135,16 @@ export async function startTelegramChannel(config) {
103
135
  }
104
136
 
105
137
  try {
106
- const MAX_TG = 4096;
107
138
  const rawResponse = typeof result.response === 'string'
108
139
  ? result.response
109
140
  : result.response != null ? JSON.stringify(result.response, null, 2) : '';
110
141
  const text = rawResponse.trim()
111
142
  || 'The agent encountered an error and could not produce a response. Please try again.';
112
- if (text.length <= MAX_TG) {
113
- await ctx.reply(text);
114
- } else {
115
- for (let i = 0; i < text.length; i += MAX_TG) {
116
- await ctx.reply(text.slice(i, i + MAX_TG));
117
- }
118
- }
143
+ await sendMessage(ctx.api, chatId, text, result.sessionId);
119
144
  console.log(`[telegram] response sent chat_id=${chatId} length=${text.length}`);
120
145
  } catch (e) {
121
146
  console.error(`[telegram] delivery error chat_id=${chatId}: ${e.message}`);
122
- await ctx.reply('Sorry, something went wrong sending the response. Please try again.').catch(() => {});
147
+ await ctx.api.sendMessage(chatId, 'Sorry, something went wrong sending the response. Please try again.').catch(() => {});
123
148
  } finally {
124
149
  clearInterval(typingInterval);
125
150
  }
@@ -162,24 +187,17 @@ export async function startTelegramChannel(config) {
162
187
  }
163
188
 
164
189
  try {
165
- const MAX_TG = 4096;
166
190
  // Guard against empty or non-string response (e.g. model returns array instead of string)
167
191
  const rawResponse = typeof result.response === 'string'
168
192
  ? result.response
169
193
  : result.response != null ? JSON.stringify(result.response, null, 2) : '';
170
194
  const text = rawResponse.trim()
171
195
  || 'The agent encountered an error and could not produce a response. Please try again.';
172
- if (text.length <= MAX_TG) {
173
- await ctx.reply(text);
174
- } else {
175
- for (let i = 0; i < text.length; i += MAX_TG) {
176
- await ctx.reply(text.slice(i, i + MAX_TG));
177
- }
178
- }
196
+ await sendMessage(ctx.api, chatId, text, result.sessionId);
179
197
  console.log(`[telegram] response sent chat_id=${chatId} length=${text.length}`);
180
198
  } catch (e) {
181
199
  console.error(`[telegram] delivery error chat_id=${chatId}: ${e.message}`);
182
- await ctx.reply('Sorry, something went wrong sending the response. Please try again.').catch(() => {});
200
+ await ctx.api.sendMessage(chatId, 'Sorry, something went wrong sending the response. Please try again.').catch(() => {});
183
201
  } finally {
184
202
  clearInterval(typingInterval);
185
203
  }
@@ -98,7 +98,9 @@ export function resolveSystemPrompt(promptTemplate, sessionId) {
98
98
  const skills = [];
99
99
  for (const entry of entries) {
100
100
  if (!entry.isDirectory()) continue;
101
- const skillFile = path.join(PATHS.skillsDir, entry.name, 'skill.md');
101
+ const skillFileName = fs.readdirSync(path.join(PATHS.skillsDir, entry.name))
102
+ .find(f => f.toLowerCase() === 'skill.md') || 'skill.md';
103
+ const skillFile = path.join(PATHS.skillsDir, entry.name, skillFileName);
102
104
  try {
103
105
  const content = fs.readFileSync(skillFile, 'utf8');
104
106
  const meta = parseSkillFrontmatter(content);
@@ -11,35 +11,40 @@ const AsyncFunction = Object.getPrototypeOf(async function () {}).constructor;
11
11
  const TOOL_TIMEOUT_MS = 60_000;
12
12
 
13
13
  const SEED_TOOLS = {
14
- list_dir: {
14
+ read_file: {
15
15
  definition: {
16
16
  type: 'function',
17
17
  function: {
18
- name: 'list_dir',
19
- description: 'List directory contents (similar to ls -la). Use this to explore the filesystem and see what files and directories exist at a given path.',
18
+ name: 'read_file',
19
+ description: 'Read a file from disk. Returns the file content as a string. Use offset and limit to read large files in chunks instead of loading everything at once.',
20
20
  parameters: {
21
21
  type: 'object',
22
22
  properties: {
23
23
  path: {
24
24
  type: 'string',
25
- description: 'Directory path to list. Defaults to the current working directory if omitted.',
25
+ description: 'Absolute or relative path to the file to read.',
26
+ },
27
+ offset: {
28
+ type: 'number',
29
+ description: 'Line number to start reading from (1-based). Omit to start from the beginning.',
30
+ },
31
+ limit: {
32
+ type: 'number',
33
+ description: 'Maximum number of lines to return. Omit to read the entire file (or remainder from offset).',
26
34
  },
27
35
  },
28
- required: [],
36
+ required: ['path'],
29
37
  },
30
38
  },
31
39
  },
32
40
  code: `
33
- const { execFile } = require("child_process");
34
- const { promisify } = require("util");
35
- const execFileAsync = promisify(execFile);
36
- const targetPath = args.path || process.cwd();
37
- const resolved = path.resolve(targetPath);
38
- const { stdout: output } = await execFileAsync("ls", ["-la", resolved], {
39
- encoding: "utf8",
40
- timeout: 10000,
41
- });
42
- return { status: "ok", path: resolved, output };
41
+ const targetPath = path.resolve(args.path);
42
+ const raw = await fs.promises.readFile(targetPath, 'utf8');
43
+ const lines = raw.split('\\n');
44
+ const offset = args.offset ? args.offset - 1 : 0;
45
+ const slice = args.limit ? lines.slice(offset, offset + args.limit) : lines.slice(offset);
46
+ const totalLines = lines.length;
47
+ return { status: 'ok', path: targetPath, content: slice.join('\\n'), totalLines, returnedLines: slice.length, offset: offset + 1 };
43
48
  `,
44
49
  },
45
50
  exec: {
@@ -472,8 +477,7 @@ const SEED_TOOLS = {
472
477
  const chatId = settings.channels?.telegram?.allowedUserIds?.[0];
473
478
  if (!chatId) return { status: 'error', error: 'No Telegram chat_id configured.' };
474
479
  if (!token) return { status: 'error', error: 'No TELEGRAM_BOT_TOKEN configured.' };
475
- const body = JSON.stringify({ chat_id: chatId, text: args.message });
476
- await new Promise((resolve, reject) => {
480
+ const sendRequest = (body) => new Promise((resolve, reject) => {
477
481
  const req = https.request({
478
482
  hostname: 'api.telegram.org',
479
483
  path: '/bot' + token + '/sendMessage',
@@ -484,7 +488,7 @@ const SEED_TOOLS = {
484
488
  res.on('data', chunk => data += chunk);
485
489
  res.on('end', () => {
486
490
  const parsed = JSON.parse(data);
487
- if (!parsed.ok) reject(new Error(parsed.description));
491
+ if (!parsed.ok) reject(Object.assign(new Error(parsed.description), { description: parsed.description, error_code: parsed.error_code }));
488
492
  else resolve(parsed);
489
493
  });
490
494
  });
@@ -492,6 +496,15 @@ const SEED_TOOLS = {
492
496
  req.write(body);
493
497
  req.end();
494
498
  });
499
+ try {
500
+ await sendRequest(JSON.stringify({ chat_id: chatId, text: args.message, parse_mode: 'HTML' }));
501
+ } catch (e) {
502
+ if (e.error_code === 400) {
503
+ await sendRequest(JSON.stringify({ chat_id: chatId, text: args.message }));
504
+ } else {
505
+ throw e;
506
+ }
507
+ }
495
508
  return { status: 'ok', chatId };
496
509
  `,
497
510
  },
@@ -540,7 +553,10 @@ const SEED_TOOLS = {
540
553
  },
541
554
  },
542
555
  code: `
543
- const skillFile = path.join(process.env.HOME, '.jarvis/data/skills', args.name, 'skill.md');
556
+ const skillDir = path.join(process.env.HOME, '.jarvis/data/skills', args.name);
557
+ const dirFiles = await fs.promises.readdir(skillDir).catch(() => []);
558
+ const skillFileName = dirFiles.find(f => f.toLowerCase() === 'skill.md') || 'skill.md';
559
+ const skillFile = path.join(skillDir, skillFileName);
544
560
  const content = await fs.promises.readFile(skillFile, 'utf8').catch(() => null);
545
561
  if (!content) return { status: 'not_found', name: args.name };
546
562
  return { status: 'ok', name: args.name, content };