@ducci/jarvis 1.0.71 → 1.0.73
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/README.md +15 -0
- package/docs/system-prompt.md +2 -1
- package/package.json +1 -1
- package/src/channels/telegram/index.js +34 -1
- package/src/scripts/onboarding.js +11 -0
- package/src/server/agent.js +86 -0
package/README.md
CHANGED
|
@@ -74,6 +74,21 @@ cd ui && npm run build # outputs to ui/dist/, served automatically by the serv
|
|
|
74
74
|
|
|
75
75
|
Jarvis is designed for **local or private server use only**. The API has no authentication — do not expose port `18008` to the public internet. The `exec` tool runs shell commands with the same permissions as the server process.
|
|
76
76
|
|
|
77
|
+
If you run Jarvis on a VPS, make sure your firewall only allows what's necessary. With `ufw`:
|
|
78
|
+
|
|
79
|
+
```bash
|
|
80
|
+
ufw default deny incoming
|
|
81
|
+
ufw default allow outgoing
|
|
82
|
+
ufw allow 22/tcp # SSH
|
|
83
|
+
ufw enable
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
Ports like `18008` stay closed to the outside world — access the UI via an SSH tunnel instead:
|
|
87
|
+
|
|
88
|
+
```bash
|
|
89
|
+
ssh -L 18008:localhost:18008 user@your-vps
|
|
90
|
+
```
|
|
91
|
+
|
|
77
92
|
## Data
|
|
78
93
|
|
|
79
94
|
All runtime data lives in `~/.jarvis/` and is never stored in the repo:
|
package/docs/system-prompt.md
CHANGED
|
@@ -56,6 +56,7 @@ There are two types of responses depending on whether you need to use tools:
|
|
|
56
56
|
}
|
|
57
57
|
|
|
58
58
|
The `response` value must be a string — never an array or object. Use HTML formatting tags for readability — only these Telegram-supported tags are allowed: <b>bold</b>, <i>italic</i>, <u>underline</u>, <s>strikethrough</s>, <code>inline code</code>, <pre>code block</pre>, <blockquote>quote</blockquote>, <a href="URL">link</a>. For line breaks use actual newlines (\n), never <br>. Never use Markdown formatting (no **, __, `, or ```). Always escape literal `<`, `>`, and `&` characters as `<`, `>`, and `&` — this applies everywhere including inside `<code>` and `<pre>` blocks (e.g. HTML snippets, shell redirects, comparisons like `x < 5`, generics like `List<String>`). In `<a href="">` URLs, escape `&` in query parameters as `&` (e.g. `?foo=1&bar=2`). Unescaped characters cause Telegram to reject the message entirely. If you need to present structured data (e.g. a list of items), format it as text within the string value.
|
|
59
|
+
❌ Never use heading or layout tags: <h1> <h2> <h3> <h4> <h5> <h6> <ul> <ol> <li> <div> <span> <p> <hr> — they are not supported by Telegram and will break the message.
|
|
59
60
|
|
|
60
61
|
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.
|
|
61
62
|
|
|
@@ -71,7 +72,7 @@ You have access to a set of tools. Each tool has a name and description that tel
|
|
|
71
72
|
- If a tool fails, record the error in `logSummary` and decide whether to retry with a corrected call or explain the failure to the user.
|
|
72
73
|
- Proactively save user facts with `save_user_info` when the user shares personal details (name, timezone, preferences) — even if not asked.
|
|
73
74
|
- Use `write_file` to create or overwrite files — never `exec` with echo/printf/heredoc (shell escaping silently corrupts content).
|
|
74
|
-
-
|
|
75
|
+
- Never use `&` to background a process. For any long-running or background process, use tmux: `tmux new-session -d -s jarvis-<purpose> "command"`. Always check first with `tmux has-session -t <name>` before starting. Read output with `tmux capture-pane -t <name> -p`. Stop with `tmux kill-session -t <name>`. Record active session names in checkpoint `state` (e.g. `{"serverSession": "jarvis-server"}`).
|
|
75
76
|
- Prefer using tools over making assumptions about the state of the system.
|
|
76
77
|
|
|
77
78
|
## Failure Recovery
|
package/package.json
CHANGED
|
@@ -2,7 +2,7 @@ import fs from 'fs';
|
|
|
2
2
|
import path from 'path';
|
|
3
3
|
import { Bot } from 'grammy';
|
|
4
4
|
import { run } from '@grammyjs/runner';
|
|
5
|
-
import { handleChat } from '../../server/agent.js';
|
|
5
|
+
import { handleChat, requestAbort } from '../../server/agent.js';
|
|
6
6
|
import { loadSession } from '../../server/sessions.js';
|
|
7
7
|
import { PATHS } from '../../server/config.js';
|
|
8
8
|
import { load, save } from './sessions.js';
|
|
@@ -25,6 +25,21 @@ function escapeHtml(str) {
|
|
|
25
25
|
}
|
|
26
26
|
|
|
27
27
|
function markdownToHtml(text) {
|
|
28
|
+
// 0. Sanitize unsupported Telegram HTML tags
|
|
29
|
+
// Headings → <b>
|
|
30
|
+
text = text.replace(/<h[1-6](\s[^>]*)?>/gi, '<b>');
|
|
31
|
+
text = text.replace(/<\/h[1-6]>/gi, '</b>');
|
|
32
|
+
// List items → bullet prefix (strip both opening and closing tags)
|
|
33
|
+
text = text.replace(/<li(\s[^>]*)?>/gi, '• ');
|
|
34
|
+
text = text.replace(/<\/li>/gi, '');
|
|
35
|
+
// Block layout tags → newlines (strip tags, keep content)
|
|
36
|
+
text = text.replace(/<\/?(ul|ol|div|p)(\s[^>]*)?>/gi, '\n');
|
|
37
|
+
// Inline layout tags → strip
|
|
38
|
+
text = text.replace(/<\/?(span)(\s[^>]*)?>/gi, '');
|
|
39
|
+
// <hr> → strip entirely
|
|
40
|
+
text = text.replace(/<hr(\s[^>]*)?\/?>/gi, '');
|
|
41
|
+
// Collapse 3+ consecutive newlines to 2
|
|
42
|
+
text = text.replace(/\n{3,}/g, '\n\n');
|
|
28
43
|
// 1. Block fences: ```[lang]\ncontent\n``` → <pre>content</pre>
|
|
29
44
|
text = text.replace(/```[\w]*\n([\s\S]*?)\n?```/g, (_, content) => {
|
|
30
45
|
return `<pre>${escapeHtml(content)}</pre>`;
|
|
@@ -85,6 +100,7 @@ export async function startTelegramChannel(config) {
|
|
|
85
100
|
await bot.api.setMyCommands([
|
|
86
101
|
{ command: 'new', description: 'Start a fresh session' },
|
|
87
102
|
{ command: 'usage', description: 'Show token usage for the current session' },
|
|
103
|
+
{ command: 'stop', description: 'Stop the current run' },
|
|
88
104
|
]);
|
|
89
105
|
|
|
90
106
|
bot.command('usage', async (ctx) => {
|
|
@@ -116,6 +132,23 @@ export async function startTelegramChannel(config) {
|
|
|
116
132
|
);
|
|
117
133
|
});
|
|
118
134
|
|
|
135
|
+
bot.command('stop', async (ctx) => {
|
|
136
|
+
const userId = ctx.from?.id;
|
|
137
|
+
if (!allowedUserIds.includes(userId)) return;
|
|
138
|
+
|
|
139
|
+
const chatId = ctx.chat.id;
|
|
140
|
+
const sessionId = sessions[chatId];
|
|
141
|
+
|
|
142
|
+
if (!isRunning.has(chatId) || !sessionId) {
|
|
143
|
+
await ctx.reply('Nothing is currently running.');
|
|
144
|
+
return;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
requestAbort(sessionId);
|
|
148
|
+
await appendTelegramChatLog(chatId, sessionId, 'SYSTEM', '--- /stop requested ---');
|
|
149
|
+
await ctx.reply('Stopping current run... I\'ll send a summary when done.');
|
|
150
|
+
});
|
|
151
|
+
|
|
119
152
|
bot.command('new', async (ctx) => {
|
|
120
153
|
const userId = ctx.from?.id;
|
|
121
154
|
if (!allowedUserIds.includes(userId)) return;
|
|
@@ -698,6 +698,17 @@ async function run() {
|
|
|
698
698
|
}
|
|
699
699
|
}
|
|
700
700
|
|
|
701
|
+
// --- TMUX CHECK ---
|
|
702
|
+
const tmuxCheck = spawnSync('which', ['tmux'], { stdio: 'pipe' });
|
|
703
|
+
if (tmuxCheck.status !== 0) {
|
|
704
|
+
console.log(chalk.blue('Installing tmux...'));
|
|
705
|
+
const hasBrew = spawnSync('which', ['brew'], { stdio: 'pipe' }).status === 0;
|
|
706
|
+
const hasApt = spawnSync('which', ['apt-get'], { stdio: 'pipe' }).status === 0;
|
|
707
|
+
if (hasBrew) spawnSync('brew', ['install', 'tmux'], { stdio: 'inherit' });
|
|
708
|
+
else if (hasApt) spawnSync('apt-get', ['install', '-y', 'tmux'], { stdio: 'inherit' });
|
|
709
|
+
else console.log(chalk.yellow('tmux not found. Install manually: apt-get install tmux / brew install tmux'));
|
|
710
|
+
}
|
|
711
|
+
|
|
701
712
|
console.log(chalk.green.bold('\nSetup complete!'));
|
|
702
713
|
}
|
|
703
714
|
|
package/src/server/agent.js
CHANGED
|
@@ -53,6 +53,22 @@ function sanitizeJson(text) {
|
|
|
53
53
|
const CONSECUTIVE_FAILURE_THRESHOLD = 3;
|
|
54
54
|
const MAX_TOOL_RESULT = 4000;
|
|
55
55
|
|
|
56
|
+
const ABORT_NOTE = `[System: The user has requested an immediate stop. This is your final response for this run.
|
|
57
|
+
Respond with your normal JSON, but add a checkpoint field:
|
|
58
|
+
|
|
59
|
+
{
|
|
60
|
+
"response": "Brief message to the user acknowledging the stop and summarising what was completed.",
|
|
61
|
+
"logSummary": "Human-readable summary of what happened before the stop.",
|
|
62
|
+
"checkpoint": {
|
|
63
|
+
"progress": "What has been fully completed — only include items confirmed by tool output.",
|
|
64
|
+
"remaining": "What still needs to be done to finish the original task — as a plain text string, never an array or object.",
|
|
65
|
+
"failedApproaches": ["Concise description of each approach that failed. Leave as empty array if nothing failed."],
|
|
66
|
+
"state": {"factKey": "factValue — concrete facts confirmed by tool output: file paths, binary locations, config values. Use {} if nothing concrete was discovered."}
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
The checkpoint will allow the task to be resumed later if needed.]`;
|
|
71
|
+
|
|
56
72
|
const WRAP_UP_NOTE = `[System: You have reached the iteration limit. This is your final response for this run.
|
|
57
73
|
Respond with your normal JSON, but add a checkpoint field:
|
|
58
74
|
|
|
@@ -74,6 +90,15 @@ The checkpoint field will be used to automatically resume the task in the next r
|
|
|
74
90
|
// queued request finishes).
|
|
75
91
|
const sessionQueues = new Map();
|
|
76
92
|
|
|
93
|
+
// Abort flags: set by requestAbort(), checked at each iteration boundary in
|
|
94
|
+
// runAgentLoop. Always cleared in _runHandleChat's finally block to prevent
|
|
95
|
+
// stale flags from killing subsequent runs.
|
|
96
|
+
const sessionAborts = new Map();
|
|
97
|
+
|
|
98
|
+
export function requestAbort(sessionId) {
|
|
99
|
+
sessionAborts.set(sessionId, true);
|
|
100
|
+
}
|
|
101
|
+
|
|
77
102
|
function accumulateUsage(accum, result) {
|
|
78
103
|
const u = result?.usage;
|
|
79
104
|
if (!u) return;
|
|
@@ -235,6 +260,48 @@ export async function runAgentLoop(client, config, session, prepareMessages, usa
|
|
|
235
260
|
while (iteration < config.maxIterations) {
|
|
236
261
|
iteration++;
|
|
237
262
|
|
|
263
|
+
// Check for user-requested stop. Do a wrap-up call so the user gets a
|
|
264
|
+
// meaningful summary and the session can be resumed later if needed.
|
|
265
|
+
if (sessionAborts.get(config._sessionId)) {
|
|
266
|
+
sessionAborts.delete(config._sessionId);
|
|
267
|
+
const abortMessages = [
|
|
268
|
+
...prepareMessages(session.messages),
|
|
269
|
+
{ role: 'user', content: ABORT_NOTE },
|
|
270
|
+
];
|
|
271
|
+
try {
|
|
272
|
+
const abortResult = await callModelWithFallback(client, config, abortMessages, []);
|
|
273
|
+
accumulateUsage(usageAccum, abortResult);
|
|
274
|
+
const abortContent = abortResult.choices[0]?.message?.content || '';
|
|
275
|
+
let parsedAbort = null;
|
|
276
|
+
try { parsedAbort = JSON.parse(sanitizeJson(abortContent)); } catch { /* use raw */ }
|
|
277
|
+
session.messages.push({ role: 'assistant', content: abortContent });
|
|
278
|
+
if (parsedAbort?.checkpoint) {
|
|
279
|
+
const cp = parsedAbort.checkpoint;
|
|
280
|
+
if (typeof cp.remaining !== 'string') cp.remaining = Array.isArray(cp.remaining) ? cp.remaining.map(String).join('\n') : cp.remaining != null ? JSON.stringify(cp.remaining) : '';
|
|
281
|
+
if (!Array.isArray(cp.failedApproaches)) cp.failedApproaches = [];
|
|
282
|
+
else cp.failedApproaches = cp.failedApproaches.map(i => typeof i === 'string' ? i : JSON.stringify(i));
|
|
283
|
+
if (typeof cp.state !== 'object' || cp.state === null || Array.isArray(cp.state)) cp.state = {};
|
|
284
|
+
}
|
|
285
|
+
return {
|
|
286
|
+
iteration,
|
|
287
|
+
response: parsedAbort?.response || abortContent || 'Run stopped.',
|
|
288
|
+
logSummary: parsedAbort?.logSummary || 'Run stopped by user request.',
|
|
289
|
+
status: 'aborted',
|
|
290
|
+
runToolCalls,
|
|
291
|
+
checkpoint: parsedAbort?.checkpoint || null,
|
|
292
|
+
};
|
|
293
|
+
} catch (e) {
|
|
294
|
+
return {
|
|
295
|
+
iteration,
|
|
296
|
+
response: 'Run stopped.',
|
|
297
|
+
logSummary: `Run stopped by user request. Wrap-up call failed: ${e.message}`,
|
|
298
|
+
status: 'aborted',
|
|
299
|
+
runToolCalls,
|
|
300
|
+
checkpoint: null,
|
|
301
|
+
};
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
|
|
238
305
|
let modelResult;
|
|
239
306
|
const iterationsLeft = config.maxIterations - iteration + 1;
|
|
240
307
|
const base = prepareMessages(session.messages);
|
|
@@ -774,6 +841,21 @@ async function _runHandleChat(config, sessionId, userMessage, attachments = [],
|
|
|
774
841
|
// makes the next one more likely (especially on free models with small context
|
|
775
842
|
// windows). The synthetic note is sufficient context; tool results are preserved
|
|
776
843
|
// in the JSONL log and accessible via read_session_log.
|
|
844
|
+
// On abort: save checkpoint data so the task can be resumed later,
|
|
845
|
+
// same as the checkpoint_reached path does for handoff runs.
|
|
846
|
+
if (finalStatus === 'aborted' && run.checkpoint) {
|
|
847
|
+
if (run.checkpoint.failedApproaches?.length > 0) {
|
|
848
|
+
if (!session.metadata.failedApproaches) session.metadata.failedApproaches = [];
|
|
849
|
+
session.metadata.failedApproaches.push(...run.checkpoint.failedApproaches);
|
|
850
|
+
}
|
|
851
|
+
if (run.checkpoint.state && Object.keys(run.checkpoint.state).length > 0) {
|
|
852
|
+
session.metadata.checkpointState = { ...(session.metadata.checkpointState || {}), ...run.checkpoint.state };
|
|
853
|
+
}
|
|
854
|
+
if (run.checkpoint.remaining) {
|
|
855
|
+
session.metadata.lastCheckpointRemaining = run.checkpoint.remaining.trim();
|
|
856
|
+
}
|
|
857
|
+
}
|
|
858
|
+
|
|
777
859
|
if (finalStatus === 'model_error' || finalStatus === 'format_error') {
|
|
778
860
|
if (finalStatus === 'model_error' && isImageUnsupportedError(run.errorDetail)) {
|
|
779
861
|
finalResponse = 'This model does not support image input. Please switch to a multimodal model (e.g. claude-3.5-sonnet, gpt-4o) in settings.';
|
|
@@ -914,6 +996,10 @@ async function _runHandleChat(config, sessionId, userMessage, attachments = [],
|
|
|
914
996
|
});
|
|
915
997
|
throw e;
|
|
916
998
|
} finally {
|
|
999
|
+
// Clear any stale abort flag — prevents a flag set just as a run finished
|
|
1000
|
+
// from killing the next run.
|
|
1001
|
+
sessionAborts.delete(sessionId);
|
|
1002
|
+
|
|
917
1003
|
// Accumulate token usage into session metadata so /usage can read it
|
|
918
1004
|
if (!session.metadata.tokenUsage) session.metadata.tokenUsage = { prompt: 0, completion: 0, cacheRead: 0, cacheCreation: 0 };
|
|
919
1005
|
session.metadata.tokenUsage.prompt += usageAccum.prompt;
|