@ducci/jarvis 1.0.9 → 1.0.11
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/agent.md +16 -2
- package/docs/cli.md +2 -1
- package/docs/setup.md +45 -2
- package/docs/telegram.md +235 -0
- package/package.json +3 -1
- package/src/channels/telegram/index.js +70 -0
- package/src/channels/telegram/sessions.js +18 -0
- package/src/index.js +43 -2
- package/src/scripts/onboarding.js +90 -20
- package/src/server/agent.js +65 -12
- package/src/server/app.js +2 -0
- package/src/server/config.js +4 -0
- package/src/server/tools.js +47 -0
- package/ui/dist/assets/{index-BFT9aOnN.js → index-DLrFBZmf.js} +3 -3
- package/ui/dist/index.html +1 -1
package/docs/agent.md
CHANGED
|
@@ -198,7 +198,7 @@ Seed tool included for sanity checks:
|
|
|
198
198
|
Jarvis uses the provider tool-calling API:
|
|
199
199
|
|
|
200
200
|
1. The model returns an assistant message containing a `tool_calls` array.
|
|
201
|
-
2. Jarvis
|
|
201
|
+
2. Jarvis normalizes each tool call before appending to the conversation history: if `function.arguments` is missing or empty, it is set to `"{}"`. Some models (especially smaller/free ones) omit `arguments` for no-arg tools. Storing a malformed tool call would cause the next API request to fail with a 400 validation error.
|
|
202
202
|
3. Jarvis executes those tools in order, serially.
|
|
203
203
|
4. Each tool result is appended to the conversation as a `role: "tool"` message with a matching `tool_call_id`.
|
|
204
204
|
5. Jarvis calls the model again with the updated conversation.
|
|
@@ -447,7 +447,13 @@ Tool inputs/outputs:
|
|
|
447
447
|
|
|
448
448
|
- Model call failures: try the selected model once, then one fallback model attempt. If both fail, end the run with a `500` error and a clear message.
|
|
449
449
|
- Tool failures: pass the error result back to the model and continue the loop. Best case would be that the next model response include another tool call to fix the previous tool call. All tool errors (especially `exec` failures) must be reported in the `logSummary` with enough detail for a human to understand the cause.
|
|
450
|
-
- Malformed JSON on final response:
|
|
450
|
+
- Malformed JSON on final response: attempt two recovery steps before giving up:
|
|
451
|
+
1. **Fallback model retry** — call the fallback model with the same conversation messages (the bad response is not saved to the session yet). If this produces valid JSON, use it and continue normally.
|
|
452
|
+
2. **Nudge retry** — if the fallback model also returns non-JSON, append a temporary nudge message to the conversation (not saved to the session) and call `callModelWithFallback` once more:
|
|
453
|
+
```
|
|
454
|
+
Your previous response was not valid JSON. Respond only with the required JSON object: {"response": "...", "logSummary": "..."}
|
|
455
|
+
```
|
|
456
|
+
3. **Give up** — if all three attempts fail, return `format_error` without pushing any assistant content to the session. The nudge message is never persisted regardless of outcome.
|
|
451
457
|
|
|
452
458
|
**Error Payload Structure**:
|
|
453
459
|
|
|
@@ -462,6 +468,14 @@ Tool inputs/outputs:
|
|
|
462
468
|
- Use `500 Internal Server Error` for API failures, tool runtime errors, or model communication issues.
|
|
463
469
|
- Always append a log entry on failure so the outcome is visible in the session log.
|
|
464
470
|
|
|
471
|
+
**Synthetic error note on failure**: when a run ends with `model_error` or `format_error`, a synthetic assistant message is appended to the session before saving:
|
|
472
|
+
|
|
473
|
+
```
|
|
474
|
+
[System: Previous run failed (model_error): <logSummary>. Error detail: <errorDetail JSON>]
|
|
475
|
+
```
|
|
476
|
+
|
|
477
|
+
The full `errorDetail` (provider error body, HTTP status, etc.) is included so the model has enough information to understand and potentially recover from the failure without needing to call `read_session_log`. Without this, the session would contain a dangling user message with no reply, and the model would have no way to understand or recover from the failure.
|
|
478
|
+
|
|
465
479
|
Model configuration:
|
|
466
480
|
|
|
467
481
|
- Selected model ID is stored in the same config file created during setup.
|
package/docs/cli.md
CHANGED
|
@@ -38,7 +38,8 @@ Lifecycle management is handled by the CLI using the **programmatic PM2 API** fo
|
|
|
38
38
|
- Stops the background process named `jarvis-server` using PM2.
|
|
39
39
|
|
|
40
40
|
### `jarvis status`
|
|
41
|
-
-
|
|
41
|
+
- Displays the current status of the `jarvis-server` process.
|
|
42
|
+
- Outputs: name, status, PID, uptime, restart count, and log file path.
|
|
42
43
|
|
|
43
44
|
## Local Development
|
|
44
45
|
|
package/docs/setup.md
CHANGED
|
@@ -71,11 +71,54 @@ Within this directory:
|
|
|
71
71
|
"port": 18008
|
|
72
72
|
}
|
|
73
73
|
```
|
|
74
|
+
- `conversations/` — per-session conversation history
|
|
75
|
+
- `tools/tools.json` — seed and custom tool definitions
|
|
76
|
+
- `user-info.json` — key-value facts about the user (written by `save_user_info` tool)
|
|
74
77
|
- `logs/`
|
|
75
|
-
- `server.log`
|
|
78
|
+
- `server.log` — PM2 stdout/stderr (process-level log)
|
|
79
|
+
- `session-<id>.jsonl` — structured per-session JSONL logs written by the agent
|
|
80
|
+
|
|
81
|
+
## Telegram Channel Setup (Optional)
|
|
82
|
+
|
|
83
|
+
At the end of `jarvis setup`, after the model selection step, the user is asked whether they want to configure the Telegram channel:
|
|
84
|
+
|
|
85
|
+
```
|
|
86
|
+
Do you want to configure the Telegram channel? (y/N)
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
If yes:
|
|
90
|
+
|
|
91
|
+
1. Bot token step
|
|
92
|
+
- Check if `TELEGRAM_BOT_TOKEN` already exists in `.env`.
|
|
93
|
+
- If it exists, prompt to keep or replace.
|
|
94
|
+
- If missing, prompt for a new token (password input).
|
|
95
|
+
- Write/update `TELEGRAM_BOT_TOKEN=...` in `.env`.
|
|
96
|
+
|
|
97
|
+
2. Allowed user IDs step
|
|
98
|
+
- Read current value from `channels.telegram.allowedUserIds` in `settings.json`.
|
|
99
|
+
- If values exist, show them and prompt to keep or replace.
|
|
100
|
+
- If missing, prompt for one or more Telegram user IDs (comma-separated input, parsed as integers).
|
|
101
|
+
- Write to `channels.telegram.allowedUserIds` in `settings.json`.
|
|
102
|
+
|
|
103
|
+
If the user answers no, the Telegram step is skipped entirely and the existing channel config (if any) is left unchanged.
|
|
104
|
+
|
|
105
|
+
## Post-Setup Restart
|
|
106
|
+
|
|
107
|
+
After `jarvis setup` completes (including the optional Telegram step), the CLI checks whether `jarvis-server` is currently running via PM2.
|
|
108
|
+
|
|
109
|
+
If the server is running, prompt:
|
|
110
|
+
|
|
111
|
+
```
|
|
112
|
+
Server is running. Restart now to apply changes? (Y/n)
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
- If yes (or Enter): restart the server using PM2 restart.
|
|
116
|
+
- If no: print a reminder — `Run \`jarvis stop && jarvis start\` when ready to apply changes.`
|
|
117
|
+
|
|
118
|
+
If the server is not running, no prompt is shown.
|
|
76
119
|
|
|
77
120
|
## First Run Flow
|
|
78
121
|
|
|
79
122
|
1. User installs Jarvis globally.
|
|
80
|
-
2. User runs `jarvis setup` to configure API
|
|
123
|
+
2. User runs `jarvis setup` to configure API key, model, and optionally the Telegram channel.
|
|
81
124
|
3. User runs `jarvis start` to launch the background server.
|
package/docs/telegram.md
ADDED
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
# Telegram Channel
|
|
2
|
+
|
|
3
|
+
This document specifies the Telegram channel adapter for Jarvis. It is intended as the implementation source of truth.
|
|
4
|
+
|
|
5
|
+
## Goals
|
|
6
|
+
|
|
7
|
+
- Thin adapter — no agent logic lives here; all intelligence is in the existing agent layer
|
|
8
|
+
- Long-polling via grammy-runner for reliable, concurrent update handling
|
|
9
|
+
- Session continuity — each Telegram user resumes the same Jarvis session across messages
|
|
10
|
+
- Runs inside the `jarvis-server` process — no second process or PM2 entry needed
|
|
11
|
+
- Allowlist-based access — only messages from configured user IDs are processed
|
|
12
|
+
|
|
13
|
+
## Architecture
|
|
14
|
+
|
|
15
|
+
The Telegram channel starts as part of the Jarvis server process. When the server boots, it checks if `TELEGRAM_BOT_TOKEN` is set in `.env`. If present, it initializes the bot and starts long polling alongside the Express server.
|
|
16
|
+
|
|
17
|
+
The channel calls the agent layer directly (no HTTP hop) — it imports and calls the same internal function that the HTTP handler uses. The `chat_id → sessionId` mapping is maintained by the channel and passed into the agent call the same way the HTTP handler passes a `sessionId`.
|
|
18
|
+
|
|
19
|
+
```
|
|
20
|
+
Telegram user
|
|
21
|
+
↓ (text message)
|
|
22
|
+
Telegram Bot API ←→ grammy-runner (long polling)
|
|
23
|
+
↓
|
|
24
|
+
Channel adapter (src/channels/telegram/index.js)
|
|
25
|
+
↓ direct function call
|
|
26
|
+
Agent layer (src/server/agent.js)
|
|
27
|
+
↓
|
|
28
|
+
Channel adapter
|
|
29
|
+
↓ sendMessage
|
|
30
|
+
Telegram user
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
## Dependencies
|
|
34
|
+
|
|
35
|
+
- `grammy` — Telegram bot framework
|
|
36
|
+
- `@grammyjs/runner` — concurrent long polling runner
|
|
37
|
+
|
|
38
|
+
These are added to the root `package.json`.
|
|
39
|
+
|
|
40
|
+
## File Layout
|
|
41
|
+
|
|
42
|
+
```
|
|
43
|
+
src/channels/telegram/
|
|
44
|
+
├── index.js — bot setup, allowlist guard, message handler, polling start
|
|
45
|
+
└── sessions.js — chat_id → sessionId mapping (load/save)
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
Persistent state:
|
|
49
|
+
|
|
50
|
+
```
|
|
51
|
+
~/.jarvis/data/channels/telegram/sessions.json
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
## Configuration
|
|
55
|
+
|
|
56
|
+
**Bot token** — stored in `~/.jarvis/.env` (it is a secret):
|
|
57
|
+
|
|
58
|
+
```
|
|
59
|
+
TELEGRAM_BOT_TOKEN=<token from BotFather>
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
**Allowed user IDs** — stored in `~/.jarvis/data/config/settings.json` (it is config):
|
|
63
|
+
|
|
64
|
+
```json
|
|
65
|
+
{
|
|
66
|
+
"selectedModel": "...",
|
|
67
|
+
"channels": {
|
|
68
|
+
"telegram": {
|
|
69
|
+
"allowedUserIds": [123456789]
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
If `TELEGRAM_BOT_TOKEN` is absent from `.env` at server startup, the Telegram channel is silently skipped — the server starts normally without it.
|
|
76
|
+
|
|
77
|
+
## Allowlist Guard
|
|
78
|
+
|
|
79
|
+
Every incoming message is checked against `allowedUserIds` before any processing occurs. The check uses `ctx.from.id` (the sender's Telegram user ID).
|
|
80
|
+
|
|
81
|
+
- If the sender is on the allowlist: process the message normally.
|
|
82
|
+
- If the sender is not on the allowlist: silently ignore — no reply, no log entry. This avoids confirming that the bot exists and is active.
|
|
83
|
+
|
|
84
|
+
## Session Mapping
|
|
85
|
+
|
|
86
|
+
Each Telegram `chat_id` (a number) maps to a Jarvis `sessionId` (a UUID string). The mapping persists to disk so sessions survive process restarts.
|
|
87
|
+
|
|
88
|
+
Path:
|
|
89
|
+
|
|
90
|
+
```
|
|
91
|
+
~/.jarvis/data/channels/telegram/sessions.json
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
Schema:
|
|
95
|
+
|
|
96
|
+
```json
|
|
97
|
+
{
|
|
98
|
+
"12345678": "550e8400-e29b-41d4-a716-446655440000"
|
|
99
|
+
}
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
`sessions.js` exposes:
|
|
103
|
+
|
|
104
|
+
- `load()` — read the file from disk; returns an empty object if missing
|
|
105
|
+
- `save(map)` — write the full map to disk (creates parent directories if needed)
|
|
106
|
+
|
|
107
|
+
The map is loaded once at startup and held in memory. It is written to disk after every new session is created (i.e. after the first message from a new user).
|
|
108
|
+
|
|
109
|
+
## Message Handler
|
|
110
|
+
|
|
111
|
+
On each incoming Telegram text message:
|
|
112
|
+
|
|
113
|
+
1. Check `ctx.from.id` against `allowedUserIds` — silently drop if not allowed
|
|
114
|
+
2. Read `chat_id` from `ctx.chat.id`
|
|
115
|
+
3. Look up `sessionId` in the in-memory map (may be `undefined` for new users)
|
|
116
|
+
4. Start a typing indicator interval (see below)
|
|
117
|
+
5. Call the agent function directly with `{ sessionId, message: ctx.message.text }`
|
|
118
|
+
6. If this was the first message (`sessionId` was `undefined`), store the returned `sessionId` in the map and save to disk
|
|
119
|
+
7. Send `response` back to the user via `ctx.reply(response)`
|
|
120
|
+
8. Clear the typing indicator interval
|
|
121
|
+
|
|
122
|
+
On any error during the agent call, clear the interval and reply with:
|
|
123
|
+
|
|
124
|
+
```
|
|
125
|
+
Sorry, something went wrong. Please try again.
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
## Typing Indicator
|
|
129
|
+
|
|
130
|
+
Telegram's `sendChatAction('typing')` shows a "typing…" indicator in the chat, but it expires after **5 seconds**. A single call before the agent runs is not sufficient — the agent can take well over 5 seconds when tool use or multiple handoffs are involved.
|
|
131
|
+
|
|
132
|
+
The solution is to send the action once immediately, then repeat it on a 4-second interval while the agent is running. The interval fires just under the 5-second expiry so the indicator stays alive continuously. The interval is always cleared in a `finally` block to avoid leaking it on errors.
|
|
133
|
+
|
|
134
|
+
```js
|
|
135
|
+
await ctx.api.sendChatAction(chatId, 'typing');
|
|
136
|
+
const typingInterval = setInterval(() => {
|
|
137
|
+
ctx.api.sendChatAction(chatId, 'typing').catch(() => {});
|
|
138
|
+
}, 4000);
|
|
139
|
+
|
|
140
|
+
try {
|
|
141
|
+
const result = await handleChat(...);
|
|
142
|
+
// ...
|
|
143
|
+
} finally {
|
|
144
|
+
clearInterval(typingInterval);
|
|
145
|
+
}
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
Errors on the interval call are silently swallowed (`.catch(() => {})`) — a failed heartbeat should never abort the agent run.
|
|
149
|
+
|
|
150
|
+
## Long Polling with grammy-runner
|
|
151
|
+
|
|
152
|
+
Use `run(bot)` from `@grammyjs/runner` instead of `bot.start()`. The runner fetches updates concurrently and processes them with a default concurrency level.
|
|
153
|
+
|
|
154
|
+
```js
|
|
155
|
+
import { Bot } from 'grammy'
|
|
156
|
+
import { run } from '@grammyjs/runner'
|
|
157
|
+
|
|
158
|
+
const bot = new Bot(process.env.TELEGRAM_BOT_TOKEN)
|
|
159
|
+
|
|
160
|
+
bot.on('message:text', async (ctx) => { /* ... */ })
|
|
161
|
+
|
|
162
|
+
run(bot)
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
The runner handles graceful shutdown on `SIGINT`/`SIGTERM` automatically. The server's existing shutdown logic does not need to change.
|
|
166
|
+
|
|
167
|
+
## Server Integration
|
|
168
|
+
|
|
169
|
+
The Telegram channel is initialized from the server entry point after the Express server is ready:
|
|
170
|
+
|
|
171
|
+
```js
|
|
172
|
+
// src/server/app.js (or equivalent entry point)
|
|
173
|
+
import { startTelegramChannel } from '../channels/telegram/index.js'
|
|
174
|
+
|
|
175
|
+
// after app.listen(...)
|
|
176
|
+
startTelegramChannel()
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
`startTelegramChannel()` checks for `TELEGRAM_BOT_TOKEN` and exits silently if it is not set. No error is thrown.
|
|
180
|
+
|
|
181
|
+
## CLI Integration
|
|
182
|
+
|
|
183
|
+
The Telegram channel is configured as an optional step inside `jarvis setup` — there is no separate command. The full setup flow is specified in [docs/setup.md](./setup.md). After setup completes, the CLI offers to restart the server automatically.
|
|
184
|
+
|
|
185
|
+
## Logging
|
|
186
|
+
|
|
187
|
+
Log lines use a simple prefix format, written to stdout (captured by PM2 alongside server logs in `~/.jarvis/logs/`):
|
|
188
|
+
|
|
189
|
+
```
|
|
190
|
+
[telegram] channel started
|
|
191
|
+
[telegram] incoming chat_id=12345678
|
|
192
|
+
[telegram] session resolved sessionId=550e8400
|
|
193
|
+
[telegram] response sent chat_id=12345678
|
|
194
|
+
[telegram] error chat_id=12345678: <error message>
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
No JSONL session logging — that is handled by the agent layer for every run.
|
|
198
|
+
|
|
199
|
+
## Commands
|
|
200
|
+
|
|
201
|
+
### `/new` — Start a fresh session
|
|
202
|
+
|
|
203
|
+
Sending `/new` resets the conversation. The `chat_id → sessionId` mapping for the sender is removed from the in-memory map and from `sessions.json` on disk. The underlying session file is left on disk (not deleted) — it is simply unlinked from the Telegram chat.
|
|
204
|
+
|
|
205
|
+
The next text message after `/new` will create a new session as if the user were messaging for the first time.
|
|
206
|
+
|
|
207
|
+
**Command registration**
|
|
208
|
+
|
|
209
|
+
Commands are registered with the Telegram Bot API at startup via `bot.api.setMyCommands()`. This makes them visible to users in two places:
|
|
210
|
+
|
|
211
|
+
- The autocomplete menu that appears when the user types `/` in the chat input
|
|
212
|
+
- The `⌘` menu button next to the chat input field
|
|
213
|
+
|
|
214
|
+
Without registration the command still works if typed manually, but users would not see it suggested. Registration is idempotent — calling `setMyCommands()` on every startup is safe.
|
|
215
|
+
|
|
216
|
+
```js
|
|
217
|
+
await bot.api.setMyCommands([
|
|
218
|
+
{ command: 'new', description: 'Start a fresh session' },
|
|
219
|
+
]);
|
|
220
|
+
```
|
|
221
|
+
|
|
222
|
+
**Behavior summary**
|
|
223
|
+
|
|
224
|
+
| State | What happens |
|
|
225
|
+
|---|---|
|
|
226
|
+
| User sends `/new`, has an existing session | Session unlinked, confirmation sent: "New session started." |
|
|
227
|
+
| User sends `/new`, no session exists yet | No-op, same confirmation sent |
|
|
228
|
+
| Next text message after `/new` | New session created, mapped to `chat_id` |
|
|
229
|
+
|
|
230
|
+
## Non-Goals (v1)
|
|
231
|
+
|
|
232
|
+
- No support for photos, files, or other media types (text only)
|
|
233
|
+
- No inline keyboards or callback queries
|
|
234
|
+
- No group chat support (only private chats)
|
|
235
|
+
- No message editing or deletion handling
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ducci/jarvis",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.11",
|
|
4
4
|
"description": "A fully automated agent system that lives on a server.",
|
|
5
5
|
"main": "./src/index.js",
|
|
6
6
|
"type": "module",
|
|
@@ -36,10 +36,12 @@
|
|
|
36
36
|
},
|
|
37
37
|
"license": "ISC",
|
|
38
38
|
"dependencies": {
|
|
39
|
+
"@grammyjs/runner": "^2.0.3",
|
|
39
40
|
"chalk": "^5.6.2",
|
|
40
41
|
"commander": "^14.0.3",
|
|
41
42
|
"dotenv": "^17.3.1",
|
|
42
43
|
"express": "^5.2.1",
|
|
44
|
+
"grammy": "^1.40.1",
|
|
43
45
|
"inquirer": "^12.11.1",
|
|
44
46
|
"openai": "^6.22.0",
|
|
45
47
|
"pm2": "^6.0.14"
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { Bot } from 'grammy';
|
|
2
|
+
import { run } from '@grammyjs/runner';
|
|
3
|
+
import { handleChat } from '../../server/agent.js';
|
|
4
|
+
import { load, save } from './sessions.js';
|
|
5
|
+
|
|
6
|
+
export async function startTelegramChannel(config) {
|
|
7
|
+
const { token, allowedUserIds } = config.telegram;
|
|
8
|
+
|
|
9
|
+
if (!token) return;
|
|
10
|
+
|
|
11
|
+
const bot = new Bot(token);
|
|
12
|
+
const sessions = load();
|
|
13
|
+
|
|
14
|
+
await bot.api.setMyCommands([
|
|
15
|
+
{ command: 'new', description: 'Start a fresh session' },
|
|
16
|
+
]);
|
|
17
|
+
|
|
18
|
+
bot.command('new', async (ctx) => {
|
|
19
|
+
const userId = ctx.from?.id;
|
|
20
|
+
if (!allowedUserIds.includes(userId)) return;
|
|
21
|
+
|
|
22
|
+
const chatId = ctx.chat.id;
|
|
23
|
+
if (sessions[chatId]) {
|
|
24
|
+
delete sessions[chatId];
|
|
25
|
+
save(sessions);
|
|
26
|
+
console.log(`[telegram] session unlinked chat_id=${chatId}`);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
await ctx.reply('New session started.');
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
bot.on('message:text', async (ctx) => {
|
|
33
|
+
const userId = ctx.from?.id;
|
|
34
|
+
|
|
35
|
+
// Allowlist guard — silently ignore unauthorized users
|
|
36
|
+
if (!allowedUserIds.includes(userId)) return;
|
|
37
|
+
|
|
38
|
+
const chatId = ctx.chat.id;
|
|
39
|
+
const sessionId = sessions[chatId] || null;
|
|
40
|
+
|
|
41
|
+
console.log(`[telegram] incoming chat_id=${chatId}`);
|
|
42
|
+
|
|
43
|
+
await ctx.api.sendChatAction(chatId, 'typing');
|
|
44
|
+
const typingInterval = setInterval(() => {
|
|
45
|
+
ctx.api.sendChatAction(chatId, 'typing').catch(() => {});
|
|
46
|
+
}, 4000);
|
|
47
|
+
|
|
48
|
+
try {
|
|
49
|
+
const result = await handleChat(config, sessionId, ctx.message.text);
|
|
50
|
+
|
|
51
|
+
// Persist new session mapping on first message
|
|
52
|
+
if (!sessions[chatId]) {
|
|
53
|
+
sessions[chatId] = result.sessionId;
|
|
54
|
+
save(sessions);
|
|
55
|
+
console.log(`[telegram] session created sessionId=${result.sessionId.slice(0, 8)}`);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
await ctx.reply(result.response);
|
|
59
|
+
console.log(`[telegram] response sent chat_id=${chatId}`);
|
|
60
|
+
} catch (e) {
|
|
61
|
+
console.error(`[telegram] error chat_id=${chatId}: ${e.message}`);
|
|
62
|
+
await ctx.reply('Sorry, something went wrong. Please try again.');
|
|
63
|
+
} finally {
|
|
64
|
+
clearInterval(typingInterval);
|
|
65
|
+
}
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
run(bot);
|
|
69
|
+
console.log('[telegram] channel started');
|
|
70
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import os from 'os';
|
|
4
|
+
|
|
5
|
+
const sessionsFile = path.join(os.homedir(), '.jarvis', 'data', 'channels', 'telegram', 'sessions.json');
|
|
6
|
+
|
|
7
|
+
export function load() {
|
|
8
|
+
try {
|
|
9
|
+
return JSON.parse(fs.readFileSync(sessionsFile, 'utf8'));
|
|
10
|
+
} catch {
|
|
11
|
+
return {};
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function save(map) {
|
|
16
|
+
fs.mkdirSync(path.dirname(sessionsFile), { recursive: true });
|
|
17
|
+
fs.writeFileSync(sessionsFile, JSON.stringify(map, null, 2), 'utf8');
|
|
18
|
+
}
|
package/src/index.js
CHANGED
|
@@ -7,10 +7,13 @@ import path from 'path';
|
|
|
7
7
|
import fs from 'fs';
|
|
8
8
|
import os from 'os';
|
|
9
9
|
import pm2 from 'pm2';
|
|
10
|
+
import inquirer from 'inquirer';
|
|
10
11
|
|
|
11
12
|
const __filename = fileURLToPath(import.meta.url);
|
|
12
13
|
const __dirname = path.dirname(__filename);
|
|
13
14
|
|
|
15
|
+
const { version } = JSON.parse(fs.readFileSync(path.join(__dirname, '..', 'package.json'), 'utf8'));
|
|
16
|
+
|
|
14
17
|
const JARVIS_DIR = path.join(os.homedir(), '.jarvis');
|
|
15
18
|
const ENV_FILE = path.join(JARVIS_DIR, '.env');
|
|
16
19
|
const SETTINGS_FILE = path.join(JARVIS_DIR, 'data', 'config', 'settings.json');
|
|
@@ -83,19 +86,57 @@ function pm2Describe() {
|
|
|
83
86
|
});
|
|
84
87
|
}
|
|
85
88
|
|
|
89
|
+
function pm2Restart() {
|
|
90
|
+
return new Promise((resolve, reject) => {
|
|
91
|
+
pm2.restart(PROCESS_NAME, (err) => {
|
|
92
|
+
if (err) reject(err);
|
|
93
|
+
else resolve();
|
|
94
|
+
});
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
|
|
86
98
|
const program = new Command();
|
|
87
99
|
|
|
88
100
|
program
|
|
89
101
|
.name('jarvis')
|
|
90
102
|
.description('A fully automated agent system that lives on a server.')
|
|
91
|
-
.version(
|
|
103
|
+
.version(version);
|
|
92
104
|
|
|
93
105
|
program
|
|
94
106
|
.command('setup')
|
|
95
107
|
.description('Run interactive onboarding to configure API key and model.')
|
|
96
|
-
.action(() => {
|
|
108
|
+
.action(async () => {
|
|
97
109
|
const onboardingScript = path.join(__dirname, 'scripts', 'onboarding.js');
|
|
98
110
|
spawnSync('node', [onboardingScript], { stdio: 'inherit' });
|
|
111
|
+
|
|
112
|
+
// Offer to restart the server if it is currently running
|
|
113
|
+
try {
|
|
114
|
+
await connectPm2();
|
|
115
|
+
const desc = await pm2Describe().catch(() => []);
|
|
116
|
+
const isRunning = desc.length > 0 && desc[0].pm2_env?.status === 'online';
|
|
117
|
+
pm2.disconnect();
|
|
118
|
+
|
|
119
|
+
if (isRunning) {
|
|
120
|
+
const { doRestart } = await inquirer.prompt([
|
|
121
|
+
{
|
|
122
|
+
type: 'confirm',
|
|
123
|
+
name: 'doRestart',
|
|
124
|
+
message: 'Server is running. Restart now to apply changes?',
|
|
125
|
+
default: true
|
|
126
|
+
}
|
|
127
|
+
]);
|
|
128
|
+
if (doRestart) {
|
|
129
|
+
await connectPm2();
|
|
130
|
+
await pm2Restart();
|
|
131
|
+
pm2.disconnect();
|
|
132
|
+
console.log('Jarvis server restarted.');
|
|
133
|
+
} else {
|
|
134
|
+
console.log('Run `jarvis stop && jarvis start` when ready to apply changes.');
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
} catch {
|
|
138
|
+
// PM2 not available or server not managed — silently skip
|
|
139
|
+
}
|
|
99
140
|
});
|
|
100
141
|
|
|
101
142
|
program
|
|
@@ -17,30 +17,24 @@ function ensureDirectories() {
|
|
|
17
17
|
fs.mkdirSync(logsDir, { recursive: true });
|
|
18
18
|
}
|
|
19
19
|
|
|
20
|
-
function
|
|
20
|
+
function loadEnvVar(key) {
|
|
21
21
|
if (fs.existsSync(envFile)) {
|
|
22
22
|
const content = fs.readFileSync(envFile, 'utf8');
|
|
23
|
-
const match = content.match(
|
|
24
|
-
if (match)
|
|
25
|
-
return match[1].trim();
|
|
26
|
-
}
|
|
23
|
+
const match = content.match(new RegExp(`^${key}=(.*)$`, 'm'));
|
|
24
|
+
if (match) return match[1].trim();
|
|
27
25
|
}
|
|
28
26
|
return null;
|
|
29
27
|
}
|
|
30
28
|
|
|
31
|
-
function
|
|
32
|
-
let content = '';
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
content = content.replace(/^OPENROUTER_API_KEY=.*$/m, `OPENROUTER_API_KEY=${apiKey}`);
|
|
37
|
-
} else {
|
|
38
|
-
content += `\nOPENROUTER_API_KEY=${apiKey}\n`;
|
|
39
|
-
}
|
|
29
|
+
function saveEnvVar(key, value) {
|
|
30
|
+
let content = fs.existsSync(envFile) ? fs.readFileSync(envFile, 'utf8') : '';
|
|
31
|
+
const line = `${key}=${value}`;
|
|
32
|
+
if (content.match(new RegExp(`^${key}=`, 'm'))) {
|
|
33
|
+
content = content.replace(new RegExp(`^${key}=.*$`, 'm'), line);
|
|
40
34
|
} else {
|
|
41
|
-
content = `
|
|
35
|
+
content = content ? `${content.trim()}\n${line}\n` : `${line}\n`;
|
|
42
36
|
}
|
|
43
|
-
fs.writeFileSync(envFile, content
|
|
37
|
+
fs.writeFileSync(envFile, content, 'utf8');
|
|
44
38
|
}
|
|
45
39
|
|
|
46
40
|
function loadSettings() {
|
|
@@ -83,7 +77,7 @@ async function run() {
|
|
|
83
77
|
console.log(chalk.green.bold('\n=== Jarvis Setup ===\n'));
|
|
84
78
|
|
|
85
79
|
// --- API KEY STEP ---
|
|
86
|
-
let existingKey =
|
|
80
|
+
let existingKey = loadEnvVar('OPENROUTER_API_KEY');
|
|
87
81
|
let apiKey = existingKey;
|
|
88
82
|
|
|
89
83
|
if (existingKey) {
|
|
@@ -106,7 +100,7 @@ async function run() {
|
|
|
106
100
|
}
|
|
107
101
|
]);
|
|
108
102
|
apiKey = newKey;
|
|
109
|
-
|
|
103
|
+
saveEnvVar('OPENROUTER_API_KEY', apiKey);
|
|
110
104
|
console.log(chalk.green('API key updated.'));
|
|
111
105
|
}
|
|
112
106
|
} else {
|
|
@@ -119,7 +113,7 @@ async function run() {
|
|
|
119
113
|
}
|
|
120
114
|
]);
|
|
121
115
|
apiKey = newKey;
|
|
122
|
-
|
|
116
|
+
saveEnvVar('OPENROUTER_API_KEY', apiKey);
|
|
123
117
|
console.log(chalk.green('API key saved.'));
|
|
124
118
|
}
|
|
125
119
|
|
|
@@ -230,7 +224,83 @@ async function run() {
|
|
|
230
224
|
|
|
231
225
|
saveSettings(settings);
|
|
232
226
|
console.log(chalk.green(`\nModel ${chalk.bold(selectedModel)} saved to settings.`));
|
|
233
|
-
|
|
227
|
+
|
|
228
|
+
// --- TELEGRAM CHANNEL STEP (OPTIONAL) ---
|
|
229
|
+
const { configureTelegram } = await inquirer.prompt([
|
|
230
|
+
{
|
|
231
|
+
type: 'confirm',
|
|
232
|
+
name: 'configureTelegram',
|
|
233
|
+
message: 'Do you want to configure the Telegram channel?',
|
|
234
|
+
default: false
|
|
235
|
+
}
|
|
236
|
+
]);
|
|
237
|
+
|
|
238
|
+
if (configureTelegram) {
|
|
239
|
+
// Bot token
|
|
240
|
+
const existingToken = loadEnvVar('TELEGRAM_BOT_TOKEN');
|
|
241
|
+
let keepToken = false;
|
|
242
|
+
if (existingToken) {
|
|
243
|
+
const { keep } = await inquirer.prompt([
|
|
244
|
+
{
|
|
245
|
+
type: 'confirm',
|
|
246
|
+
name: 'keep',
|
|
247
|
+
message: 'A TELEGRAM_BOT_TOKEN is already configured. Do you want to keep it?',
|
|
248
|
+
default: true
|
|
249
|
+
}
|
|
250
|
+
]);
|
|
251
|
+
keepToken = keep;
|
|
252
|
+
}
|
|
253
|
+
if (!keepToken) {
|
|
254
|
+
const { token } = await inquirer.prompt([
|
|
255
|
+
{
|
|
256
|
+
type: 'password',
|
|
257
|
+
name: 'token',
|
|
258
|
+
message: 'Enter your Telegram bot token (from BotFather):',
|
|
259
|
+
validate: (input) => input.trim().length > 0 || 'Bot token cannot be empty.'
|
|
260
|
+
}
|
|
261
|
+
]);
|
|
262
|
+
saveEnvVar('TELEGRAM_BOT_TOKEN', token.trim());
|
|
263
|
+
console.log(chalk.green('Telegram bot token saved.'));
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
// Allowed user IDs
|
|
267
|
+
const existingIds = settings.channels?.telegram?.allowedUserIds;
|
|
268
|
+
let keepIds = false;
|
|
269
|
+
if (existingIds && existingIds.length > 0) {
|
|
270
|
+
const { keep } = await inquirer.prompt([
|
|
271
|
+
{
|
|
272
|
+
type: 'confirm',
|
|
273
|
+
name: 'keep',
|
|
274
|
+
message: `Allowed Telegram user IDs are already configured (${existingIds.join(', ')}). Keep them?`,
|
|
275
|
+
default: true
|
|
276
|
+
}
|
|
277
|
+
]);
|
|
278
|
+
keepIds = keep;
|
|
279
|
+
}
|
|
280
|
+
if (!keepIds) {
|
|
281
|
+
const { rawIds } = await inquirer.prompt([
|
|
282
|
+
{
|
|
283
|
+
type: 'input',
|
|
284
|
+
name: 'rawIds',
|
|
285
|
+
message: 'Enter allowed Telegram user ID(s), comma-separated:',
|
|
286
|
+
validate: (input) => {
|
|
287
|
+
const parts = input.split(',').map(s => s.trim()).filter(Boolean);
|
|
288
|
+
if (parts.length === 0) return 'At least one user ID is required.';
|
|
289
|
+
if (parts.some(p => !/^\d+$/.test(p))) return 'All values must be numeric user IDs.';
|
|
290
|
+
return true;
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
]);
|
|
294
|
+
const ids = rawIds.split(',').map(s => parseInt(s.trim(), 10));
|
|
295
|
+
if (!settings.channels) settings.channels = {};
|
|
296
|
+
if (!settings.channels.telegram) settings.channels.telegram = {};
|
|
297
|
+
settings.channels.telegram.allowedUserIds = ids;
|
|
298
|
+
saveSettings(settings);
|
|
299
|
+
console.log(chalk.green(`Allowed user IDs saved: ${ids.join(', ')}`));
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
console.log(chalk.green.bold('\nSetup complete!'));
|
|
234
304
|
}
|
|
235
305
|
|
|
236
306
|
run().catch(error => {
|