@4via6/relay 1.0.0 → 1.1.0
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 +89 -47
- package/dist/auth.d.ts +1 -0
- package/dist/auth.js +6 -3
- package/dist/auth.js.map +1 -1
- package/dist/bot.js +2 -1
- package/dist/bot.js.map +1 -1
- package/dist/cli.d.ts +1 -1
- package/dist/cli.js +110 -1
- package/dist/cli.js.map +1 -1
- package/dist/commands/admin.js +134 -29
- package/dist/commands/admin.js.map +1 -1
- package/dist/commands/media.js +10 -7
- package/dist/commands/media.js.map +1 -1
- package/dist/config/index.d.ts +7 -0
- package/dist/config/index.js +16 -0
- package/dist/config/index.js.map +1 -0
- package/dist/config/loader.d.ts +11 -0
- package/dist/config/loader.js +235 -0
- package/dist/config/loader.js.map +1 -0
- package/dist/config/schema.d.ts +32 -0
- package/dist/config/schema.js +32 -0
- package/dist/config/schema.js.map +1 -0
- package/dist/config/setup.d.ts +3 -0
- package/dist/config/setup.js +142 -0
- package/dist/config/setup.js.map +1 -0
- package/dist/daemon.d.ts +5 -0
- package/dist/daemon.js +215 -0
- package/dist/daemon.js.map +1 -0
- package/dist/index.js +23 -25
- package/dist/index.js.map +1 -1
- package/dist/providers/claude.js +31 -30
- package/dist/providers/claude.js.map +1 -1
- package/dist/providers/codex.js +29 -28
- package/dist/providers/codex.js.map +1 -1
- package/dist/providers/index.js +2 -1
- package/dist/providers/index.js.map +1 -1
- package/dist/providers/opencode.js +8 -5
- package/dist/providers/opencode.js.map +1 -1
- package/dist/session.js +3 -2
- package/dist/session.js.map +1 -1
- package/dist/update.d.ts +1 -0
- package/dist/update.js +132 -0
- package/dist/update.js.map +1 -0
- package/dist/utils/files.js +2 -1
- package/dist/utils/files.js.map +1 -1
- package/dist/utils/logger.d.ts +8 -0
- package/dist/utils/logger.js +16 -0
- package/dist/utils/logger.js.map +1 -0
- package/dist/utils/store.js +1 -0
- package/dist/utils/store.js.map +1 -1
- package/dist/utils/stream.js +6 -3
- package/dist/utils/stream.js.map +1 -1
- package/dist/utils/stt.js +27 -16
- package/dist/utils/stt.js.map +1 -1
- package/dist/utils/system-prompt.js +16 -5
- package/dist/utils/system-prompt.js.map +1 -1
- package/dist/utils/timeout.js +2 -3
- package/dist/utils/timeout.js.map +1 -1
- package/docs/commands.md +7 -15
- package/docs/configuration.md +124 -108
- package/docs/features.md +70 -36
- package/docs/getting-started.md +63 -30
- package/docs/providers.md +18 -32
- package/docs/troubleshooting.md +108 -63
- package/package.json +4 -3
- package/.env.example +0 -50
package/README.md
CHANGED
|
@@ -1,10 +1,16 @@
|
|
|
1
1
|
# Relay
|
|
2
2
|
|
|
3
|
+
[](https://www.npmjs.com/package/@4via6/relay)
|
|
4
|
+
[](https://www.npmjs.com/package/@4via6/relay)
|
|
5
|
+
[](LICENSE)
|
|
6
|
+
|
|
3
7
|
Telegram bot for managing AI coding agents remotely. Supports [OpenCode](https://github.com/opencode-ai/opencode), [Claude Code](https://docs.anthropic.com/en/docs/agents-and-tools/claude-code/overview), and [OpenAI Codex](https://github.com/openai/codex).
|
|
4
8
|
|
|
5
9
|
## Features
|
|
6
10
|
|
|
7
|
-
- **Multi-provider** -- switch between OpenCode, Claude Code, and Codex
|
|
11
|
+
- **Multi-provider** -- switch between OpenCode, Claude Code, and Codex
|
|
12
|
+
- **Interactive setup** -- `relay onboard` wizard for first-time configuration
|
|
13
|
+
- **Structured logging** -- pino-based JSON logging with configurable levels
|
|
8
14
|
- **Text, voice, photo, and file input** -- send messages in any format
|
|
9
15
|
- **File output** -- receive screenshots, generated files, and artifacts as Telegram attachments (OpenCode)
|
|
10
16
|
- **Streaming responses** -- progressive message editing for real-time output
|
|
@@ -13,7 +19,7 @@ Telegram bot for managing AI coding agents remotely. Supports [OpenCode](https:/
|
|
|
13
19
|
- **MCP servers** -- add, remove, and monitor MCP servers at runtime (OpenCode, Claude)
|
|
14
20
|
- **Shell access** -- run commands on the coding agent's machine
|
|
15
21
|
- **Voice transcription** -- Groq, OpenAI, or AssemblyAI speech-to-text
|
|
16
|
-
- **Custom system prompts** -- load from
|
|
22
|
+
- **Custom system prompts** -- load from `.relay/SKILL.md`, hot-reload on change
|
|
17
23
|
- **File operations** -- read, find, search, and browse project files (all providers)
|
|
18
24
|
- **Code diffs** -- view git diffs from sessions (all providers)
|
|
19
25
|
- **State persistence** -- sessions, model selection, and MCP configs survive restarts
|
|
@@ -36,33 +42,26 @@ Telegram bot for managing AI coding agents remotely. Supports [OpenCode](https:/
|
|
|
36
42
|
### From npm
|
|
37
43
|
|
|
38
44
|
```bash
|
|
39
|
-
npm install -g relay
|
|
45
|
+
npm install -g @4via6/relay
|
|
46
|
+
relay onboard
|
|
40
47
|
```
|
|
41
48
|
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
```bash
|
|
45
|
-
curl -O https://raw.githubusercontent.com/Harsh-2002/relay/main/.env.example
|
|
46
|
-
# Edit .env with your BOT_TOKEN, ALLOWED_USER_ID, and provider config
|
|
47
|
-
relay
|
|
48
|
-
```
|
|
49
|
+
The setup wizard will ask for your bot token, user ID, and provider config, then save everything to `.relay/config.json`.
|
|
49
50
|
|
|
50
51
|
### From source
|
|
51
52
|
|
|
52
53
|
```bash
|
|
53
|
-
git clone https://github.com/Harsh-2002/
|
|
54
|
-
cd
|
|
54
|
+
git clone https://github.com/Harsh-2002/Relay.git
|
|
55
|
+
cd Relay
|
|
55
56
|
npm install
|
|
56
57
|
npm run build
|
|
57
|
-
|
|
58
|
-
# Edit .env with your BOT_TOKEN, ALLOWED_USER_ID, and provider config
|
|
59
|
-
npm start
|
|
58
|
+
npm start -- onboard
|
|
60
59
|
```
|
|
61
60
|
|
|
62
61
|
### With npx (no install)
|
|
63
62
|
|
|
64
63
|
```bash
|
|
65
|
-
npx relay
|
|
64
|
+
npx @4via6/relay onboard
|
|
66
65
|
```
|
|
67
66
|
|
|
68
67
|
### Prerequisites
|
|
@@ -71,40 +70,83 @@ npx relay
|
|
|
71
70
|
- A Telegram bot token (from [@BotFather](https://t.me/BotFather))
|
|
72
71
|
- Provider credentials (see below)
|
|
73
72
|
|
|
73
|
+
## Running
|
|
74
|
+
|
|
75
|
+
### Foreground (default)
|
|
76
|
+
|
|
77
|
+
```bash
|
|
78
|
+
relay
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
Close the terminal and the bot stops.
|
|
82
|
+
|
|
83
|
+
### Background (daemon)
|
|
84
|
+
|
|
85
|
+
```bash
|
|
86
|
+
relay start # Start as background daemon
|
|
87
|
+
relay status # Show PID, uptime, memory
|
|
88
|
+
relay logs # Tail logs (Ctrl+C to exit)
|
|
89
|
+
relay restart # Restart the daemon
|
|
90
|
+
relay stop # Stop the daemon
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
The daemon uses [pm2](https://pm2.keymetrics.io/) under the hood (auto-installed on first `relay start`). CLI flags are forwarded — e.g. `relay start --provider=claude` works as expected.
|
|
94
|
+
|
|
95
|
+
### Updating
|
|
96
|
+
|
|
97
|
+
```bash
|
|
98
|
+
relay update
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
Detects how Relay was installed (npm global or git source) and updates accordingly. If the daemon is running, it's automatically restarted after the update.
|
|
102
|
+
|
|
103
|
+
## Configuration
|
|
104
|
+
|
|
105
|
+
Config is stored in `.relay/config.json`. Use the setup wizard or CLI flags:
|
|
106
|
+
|
|
107
|
+
```bash
|
|
108
|
+
relay onboard # Interactive wizard
|
|
109
|
+
relay --bot-token=xxx --allowed-user-id=123 --provider=opencode # CLI flags
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
### CLI flags
|
|
113
|
+
|
|
114
|
+
| Flag | Description |
|
|
115
|
+
|------|-------------|
|
|
116
|
+
| `--help` | Show help |
|
|
117
|
+
| `--version` | Show version |
|
|
118
|
+
| `--bot-token` | Telegram bot token |
|
|
119
|
+
| `--allowed-user-id` | Telegram user ID |
|
|
120
|
+
| `--provider` | Provider: `opencode`, `claude`, `codex` |
|
|
121
|
+
| `--bot-mode` | `polling` or `webhook` |
|
|
122
|
+
| `--streaming-enabled` | `true` or `false` |
|
|
123
|
+
| `--log-level` | `debug`, `info`, `warn`, `error` |
|
|
124
|
+
| `--data-dir` | Data directory (default: `.relay/`) |
|
|
125
|
+
| `--system-prompt-file` | Custom system prompt file |
|
|
126
|
+
|
|
127
|
+
Environment variables are supported for backward compatibility. Run `relay onboard` to migrate to the config file.
|
|
128
|
+
|
|
74
129
|
## Providers
|
|
75
130
|
|
|
76
|
-
|
|
131
|
+
| Provider | Install |
|
|
132
|
+
|----------|---------|
|
|
133
|
+
| OpenCode | included |
|
|
134
|
+
| Claude Code | `npm install @anthropic-ai/claude-code` |
|
|
135
|
+
| OpenAI Codex | `npm install @openai/codex` |
|
|
77
136
|
|
|
78
|
-
|
|
79
|
-
|----------|-------------|-------------------|---------|
|
|
80
|
-
| OpenCode | `opencode` | `OPENCODE_MODE` | included |
|
|
81
|
-
| Claude Code | `claude` | `ANTHROPIC_API_KEY` | `npm install @anthropic-ai/claude-code` |
|
|
82
|
-
| OpenAI Codex | `codex` | `CODEX_API_KEY` or `OPENAI_API_KEY` | `npm install @openai/codex` |
|
|
137
|
+
Provider API keys (like `ANTHROPIC_API_KEY` or `OPENAI_API_KEY`) are configured in your coding agent's environment, not in Relay.
|
|
83
138
|
|
|
84
139
|
### OpenCode
|
|
85
140
|
|
|
86
|
-
|
|
87
|
-
PROVIDER=opencode
|
|
88
|
-
OPENCODE_MODE=start # "start" (spawn server) or "connect" (remote URL)
|
|
89
|
-
OPENCODE_URL=http://localhost:4096
|
|
90
|
-
```
|
|
141
|
+
Select during `relay onboard` or pass `--provider=opencode`. Supports both `start` mode (spawns local server) and `connect` mode (remote URL).
|
|
91
142
|
|
|
92
143
|
### Claude Code
|
|
93
144
|
|
|
94
|
-
|
|
95
|
-
PROVIDER=claude
|
|
96
|
-
ANTHROPIC_API_KEY=sk-ant-...
|
|
97
|
-
CLAUDE_MODEL=sonnet # use /models to see all available
|
|
98
|
-
CLAUDE_PERMISSION_MODE=acceptEdits
|
|
99
|
-
```
|
|
145
|
+
Select during `relay onboard` or pass `--provider=claude`. Requires `@anthropic-ai/claude-code` to be installed. API key must be set in the environment where Claude Code runs.
|
|
100
146
|
|
|
101
147
|
### OpenAI Codex
|
|
102
148
|
|
|
103
|
-
|
|
104
|
-
PROVIDER=codex
|
|
105
|
-
CODEX_API_KEY=sk-...
|
|
106
|
-
CODEX_MODEL=o3 # use /models to see all available
|
|
107
|
-
```
|
|
149
|
+
Select during `relay onboard` or pass `--provider=codex`. Requires `@openai/codex` to be installed. API key must be set in the environment where Codex runs.
|
|
108
150
|
|
|
109
151
|
## Commands
|
|
110
152
|
|
|
@@ -188,25 +230,24 @@ MCP servers extend the AI's capabilities with additional tools (browsers, databa
|
|
|
188
230
|
|
|
189
231
|
## Voice / STT
|
|
190
232
|
|
|
191
|
-
Configure
|
|
192
|
-
|
|
193
|
-
```env
|
|
194
|
-
GROQ_API_KEY=... # Groq Whisper (fastest, free tier)
|
|
195
|
-
OPENAI_API_KEY=... # OpenAI Whisper
|
|
196
|
-
ASSEMBLYAI_API_KEY=... # AssemblyAI
|
|
197
|
-
```
|
|
233
|
+
Configure speech-to-text providers during `relay onboard` or pass API keys via CLI flags. The cheapest available provider is auto-selected.
|
|
198
234
|
|
|
199
235
|
## System Prompt
|
|
200
236
|
|
|
201
|
-
The bot loads a system prompt from `
|
|
237
|
+
The bot loads a system prompt from `.relay/SKILL.md` (or `./SKILL.md` in cwd for backward compatibility, or a custom path via `--system-prompt-file`). If no file exists, a default prompt is used. The file is watched for changes and reloaded automatically. Use `/system reload` to force a reload.
|
|
202
238
|
|
|
203
239
|
## Architecture
|
|
204
240
|
|
|
205
241
|
```
|
|
206
242
|
src/
|
|
243
|
+
config/
|
|
244
|
+
schema.ts -- Config type definitions
|
|
245
|
+
loader.ts -- Config resolution (CLI > file > env > defaults)
|
|
246
|
+
setup.ts -- Interactive setup wizard
|
|
247
|
+
index.ts -- Config singleton
|
|
207
248
|
providers/
|
|
208
249
|
types.ts -- Provider interface, capabilities, MCP/model types
|
|
209
|
-
index.ts -- Provider factory
|
|
250
|
+
index.ts -- Provider factory
|
|
210
251
|
opencode.ts -- OpenCode SDK provider
|
|
211
252
|
claude.ts -- Claude Code / Agent SDK provider
|
|
212
253
|
codex.ts -- OpenAI Codex SDK provider
|
|
@@ -221,6 +262,7 @@ src/
|
|
|
221
262
|
shell.ts -- Shell and command execution
|
|
222
263
|
mcp.ts -- MCP server management
|
|
223
264
|
utils/
|
|
265
|
+
logger.ts -- Pino-based structured logging
|
|
224
266
|
store.ts -- JSON file-backed persistence (.relay/)
|
|
225
267
|
stream.ts -- Streaming response handler
|
|
226
268
|
files.ts -- Outbound file attachment handling
|
package/dist/auth.d.ts
CHANGED
package/dist/auth.js
CHANGED
|
@@ -1,9 +1,12 @@
|
|
|
1
|
-
//
|
|
2
|
-
|
|
3
|
-
const allowedUserId = raw ? Number(raw) : NaN;
|
|
1
|
+
// Initialized by initAuth() from index.ts
|
|
2
|
+
let allowedUserId = NaN;
|
|
4
3
|
// Simple in-memory rate limiter: max requests per minute per user
|
|
5
4
|
const RATE_LIMIT = 30;
|
|
6
5
|
const rateBuckets = new Map();
|
|
6
|
+
export function initAuth(userId) {
|
|
7
|
+
allowedUserId = userId;
|
|
8
|
+
return isAuthConfigured();
|
|
9
|
+
}
|
|
7
10
|
export function isAuthConfigured() {
|
|
8
11
|
return !isNaN(allowedUserId) && allowedUserId > 0;
|
|
9
12
|
}
|
package/dist/auth.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"auth.js","sourceRoot":"","sources":["../src/auth.ts"],"names":[],"mappings":"AAEA,
|
|
1
|
+
{"version":3,"file":"auth.js","sourceRoot":"","sources":["../src/auth.ts"],"names":[],"mappings":"AAEA,0CAA0C;AAC1C,IAAI,aAAa,GAAG,GAAG,CAAC;AAExB,kEAAkE;AAClE,MAAM,UAAU,GAAG,EAAE,CAAC;AACtB,MAAM,WAAW,GAAG,IAAI,GAAG,EAAoB,CAAC;AAEhD,MAAM,UAAU,QAAQ,CAAC,MAAc;IACrC,aAAa,GAAG,MAAM,CAAC;IACvB,OAAO,gBAAgB,EAAE,CAAC;AAC5B,CAAC;AAED,MAAM,UAAU,gBAAgB;IAC9B,OAAO,CAAC,KAAK,CAAC,aAAa,CAAC,IAAI,aAAa,GAAG,CAAC,CAAC;AACpD,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,cAAc,CAAC,GAAY,EAAE,IAAkB;IACnE,wEAAwE;IACxE,IAAI,CAAC,gBAAgB,EAAE,EAAE,CAAC;QACxB,MAAM,GAAG,CAAC,KAAK,CAAC,oDAAoD,CAAC,CAAC;QACtE,OAAO;IACT,CAAC;IAED,MAAM,MAAM,GAAG,GAAG,CAAC,IAAI,EAAE,EAAE,CAAC;IAC5B,IAAI,MAAM,KAAK,aAAa,EAAE,CAAC;QAC7B,MAAM,GAAG,CAAC,KAAK,CAAC,eAAe,CAAC,CAAC;QACjC,OAAO;IACT,CAAC;IAED,gBAAgB;IAChB,MAAM,GAAG,GAAG,IAAI,CAAC,GAAG,EAAE,CAAC;IACvB,MAAM,MAAM,GAAG,CAAC,WAAW,CAAC,GAAG,CAAC,MAAM,CAAC,IAAI,EAAE,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,GAAG,GAAG,CAAC,GAAG,MAAM,CAAC,CAAC;IAC/E,IAAI,MAAM,CAAC,MAAM,IAAI,UAAU,EAAE,CAAC;QAChC,MAAM,GAAG,CAAC,KAAK,CAAC,oCAAoC,CAAC,CAAC;QACtD,OAAO;IACT,CAAC;IACD,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;IACjB,WAAW,CAAC,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAEhC,MAAM,IAAI,EAAE,CAAC;AACf,CAAC"}
|
package/dist/bot.js
CHANGED
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
import { Bot } from "grammy";
|
|
2
2
|
import { authMiddleware } from "./auth.js";
|
|
3
3
|
import { registerCommands } from "./commands/index.js";
|
|
4
|
+
import { botLogger } from "./utils/logger.js";
|
|
4
5
|
export function createBot(token) {
|
|
5
6
|
const bot = new Bot(token);
|
|
6
7
|
bot.use(authMiddleware);
|
|
7
8
|
registerCommands(bot);
|
|
8
9
|
bot.catch((err) => {
|
|
9
10
|
const e = err.error;
|
|
10
|
-
|
|
11
|
+
botLogger.error({ err: e instanceof Error ? e.message : String(e) }, "Bot error");
|
|
11
12
|
});
|
|
12
13
|
return bot;
|
|
13
14
|
}
|
package/dist/bot.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"bot.js","sourceRoot":"","sources":["../src/bot.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,GAAG,EAAE,MAAM,QAAQ,CAAC;AAC7B,OAAO,EAAE,cAAc,EAAE,MAAM,WAAW,CAAC;AAC3C,OAAO,EAAE,gBAAgB,EAAE,MAAM,qBAAqB,CAAC;
|
|
1
|
+
{"version":3,"file":"bot.js","sourceRoot":"","sources":["../src/bot.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,GAAG,EAAE,MAAM,QAAQ,CAAC;AAC7B,OAAO,EAAE,cAAc,EAAE,MAAM,WAAW,CAAC;AAC3C,OAAO,EAAE,gBAAgB,EAAE,MAAM,qBAAqB,CAAC;AACvD,OAAO,EAAE,SAAS,EAAE,MAAM,mBAAmB,CAAC;AAE9C,MAAM,UAAU,SAAS,CAAC,KAAa;IACrC,MAAM,GAAG,GAAG,IAAI,GAAG,CAAC,KAAK,CAAC,CAAC;IAE3B,GAAG,CAAC,GAAG,CAAC,cAAc,CAAC,CAAC;IACxB,gBAAgB,CAAC,GAAG,CAAC,CAAC;IAEtB,GAAG,CAAC,KAAK,CAAC,CAAC,GAAG,EAAE,EAAE;QAChB,MAAM,CAAC,GAAG,GAAG,CAAC,KAAK,CAAC;QACpB,SAAS,CAAC,KAAK,CAAC,EAAE,GAAG,EAAE,CAAC,YAAY,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,WAAW,CAAC,CAAC;IACpF,CAAC,CAAC,CAAC;IAEH,OAAO,GAAG,CAAC;AACb,CAAC"}
|
package/dist/cli.d.ts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
|
|
2
|
+
export {};
|
package/dist/cli.js
CHANGED
|
@@ -1,3 +1,112 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import "
|
|
2
|
+
import { readFileSync } from "fs";
|
|
3
|
+
import { join, dirname } from "path";
|
|
4
|
+
import { fileURLToPath } from "url";
|
|
5
|
+
import { loadConfig, setConfig } from "./config/index.js";
|
|
6
|
+
import { runSetupWizard } from "./config/setup.js";
|
|
7
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
8
|
+
function printVersion() {
|
|
9
|
+
try {
|
|
10
|
+
const pkg = JSON.parse(readFileSync(join(__dirname, "..", "package.json"), "utf-8"));
|
|
11
|
+
console.log(`relay v${pkg.version}`);
|
|
12
|
+
}
|
|
13
|
+
catch {
|
|
14
|
+
console.log("relay (unknown version)");
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
function printHelp() {
|
|
18
|
+
console.log(`
|
|
19
|
+
relay — Telegram bot for AI coding agents
|
|
20
|
+
|
|
21
|
+
Usage:
|
|
22
|
+
relay Start the bot (foreground)
|
|
23
|
+
relay onboard Interactive configuration wizard
|
|
24
|
+
relay start Start the bot as a background daemon
|
|
25
|
+
relay stop Stop the background daemon
|
|
26
|
+
relay restart Restart the background daemon
|
|
27
|
+
relay logs Tail daemon logs (Ctrl+C to exit)
|
|
28
|
+
relay status Show daemon status
|
|
29
|
+
relay update Update Relay to the latest version
|
|
30
|
+
relay --help Show this help message
|
|
31
|
+
relay --version Show version
|
|
32
|
+
|
|
33
|
+
Options:
|
|
34
|
+
--bot-token <token> Telegram bot token
|
|
35
|
+
--allowed-user-id <id> Telegram user ID
|
|
36
|
+
--provider <name> Provider: opencode, claude, codex
|
|
37
|
+
--bot-mode <mode> Bot mode: polling, webhook
|
|
38
|
+
--streaming-enabled <bool> Enable streaming responses
|
|
39
|
+
--log-level <level> Log level: debug, info, warn, error
|
|
40
|
+
--data-dir <path> Data directory (default: .relay/)
|
|
41
|
+
--system-prompt-file <path> System prompt file path
|
|
42
|
+
|
|
43
|
+
Config is loaded from: .relay/config.json
|
|
44
|
+
Run 'relay onboard' to create or update config interactively.
|
|
45
|
+
|
|
46
|
+
Documentation: https://github.com/Harsh-2002/Relay
|
|
47
|
+
`);
|
|
48
|
+
}
|
|
49
|
+
const DAEMON_COMMANDS = new Set(["start", "stop", "restart", "logs", "status", "update"]);
|
|
50
|
+
async function main() {
|
|
51
|
+
const args = process.argv.slice(2);
|
|
52
|
+
const subcommand = args[0];
|
|
53
|
+
// Route daemon subcommands before loading config
|
|
54
|
+
if (subcommand && DAEMON_COMMANDS.has(subcommand)) {
|
|
55
|
+
const daemon = await import("./daemon.js");
|
|
56
|
+
switch (subcommand) {
|
|
57
|
+
case "start":
|
|
58
|
+
daemon.daemonStart();
|
|
59
|
+
break;
|
|
60
|
+
case "stop":
|
|
61
|
+
daemon.daemonStop();
|
|
62
|
+
break;
|
|
63
|
+
case "restart":
|
|
64
|
+
daemon.daemonRestart();
|
|
65
|
+
break;
|
|
66
|
+
case "logs":
|
|
67
|
+
daemon.daemonLogs();
|
|
68
|
+
break;
|
|
69
|
+
case "status":
|
|
70
|
+
daemon.daemonStatus();
|
|
71
|
+
break;
|
|
72
|
+
case "update": {
|
|
73
|
+
const { update } = await import("./update.js");
|
|
74
|
+
update();
|
|
75
|
+
break;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
// Check for 'onboard' subcommand before parsing flags
|
|
81
|
+
const isOnboard = subcommand === "onboard";
|
|
82
|
+
const result = loadConfig();
|
|
83
|
+
if (result.showVersion) {
|
|
84
|
+
printVersion();
|
|
85
|
+
process.exit(0);
|
|
86
|
+
}
|
|
87
|
+
if (result.showHelp) {
|
|
88
|
+
printHelp();
|
|
89
|
+
process.exit(0);
|
|
90
|
+
}
|
|
91
|
+
if (isOnboard || result.needsSetup) {
|
|
92
|
+
if (result.needsSetup && !isOnboard) {
|
|
93
|
+
console.log("\n No config found. Starting setup wizard...\n");
|
|
94
|
+
}
|
|
95
|
+
const config = await runSetupWizard(result.config.dataDir);
|
|
96
|
+
setConfig(config);
|
|
97
|
+
// If this was an explicit 'onboard' command, exit after saving
|
|
98
|
+
if (isOnboard) {
|
|
99
|
+
process.exit(0);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
else {
|
|
103
|
+
setConfig(result.config);
|
|
104
|
+
}
|
|
105
|
+
// Now start the bot
|
|
106
|
+
await import("./index.js");
|
|
107
|
+
}
|
|
108
|
+
main().catch((err) => {
|
|
109
|
+
console.error("Fatal:", err?.message ?? err);
|
|
110
|
+
process.exit(1);
|
|
111
|
+
});
|
|
3
112
|
//# sourceMappingURL=cli.js.map
|
package/dist/cli.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"cli.js","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":";AACA,OAAO,YAAY,CAAC"}
|
|
1
|
+
{"version":3,"file":"cli.js","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":"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"}
|
package/dist/commands/admin.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { InputFile } from "grammy";
|
|
1
|
+
import { InputFile, InlineKeyboard } from "grammy";
|
|
2
2
|
import { getProvider, getProviderName } from "../providers/index.js";
|
|
3
3
|
import { setSelectedModel, getSelectedModel } from "../session.js";
|
|
4
4
|
import { chunkMessage } from "../utils/chunker.js";
|
|
@@ -7,6 +7,76 @@ import { isStreamingEnabled } from "../utils/stream.js";
|
|
|
7
7
|
import { getSystemPrompt, reloadSystemPrompt, isUsingCustomPrompt } from "../utils/system-prompt.js";
|
|
8
8
|
import { formatCatchError } from "../utils/errors.js";
|
|
9
9
|
import { escapeHtml } from "../utils/html.js";
|
|
10
|
+
const MODELS_PER_PAGE = 8;
|
|
11
|
+
function buildModelKeyboard(models, page, selectedModel) {
|
|
12
|
+
// Group by provider
|
|
13
|
+
const grouped = new Map();
|
|
14
|
+
for (const m of models) {
|
|
15
|
+
const list = grouped.get(m.provider) ?? [];
|
|
16
|
+
list.push(m);
|
|
17
|
+
grouped.set(m.provider, list);
|
|
18
|
+
}
|
|
19
|
+
// Flatten into display order with provider headers
|
|
20
|
+
const items = [];
|
|
21
|
+
const allItems = [];
|
|
22
|
+
for (const [provId, provModels] of grouped) {
|
|
23
|
+
allItems.push({ type: "header", provider: provId });
|
|
24
|
+
for (const m of provModels) {
|
|
25
|
+
allItems.push({ type: "model", model: m });
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
// Count total model items (excluding headers) for pagination
|
|
29
|
+
const modelItems = allItems.filter(i => i.type === "model");
|
|
30
|
+
const totalPages = Math.max(1, Math.ceil(modelItems.length / MODELS_PER_PAGE));
|
|
31
|
+
const safePage = Math.max(0, Math.min(page, totalPages - 1));
|
|
32
|
+
// Get the slice of models for this page
|
|
33
|
+
const pageModels = modelItems.slice(safePage * MODELS_PER_PAGE, (safePage + 1) * MODELS_PER_PAGE);
|
|
34
|
+
const pageModelIds = new Set(pageModels.map(i => i.type === "model" ? i.model.id : ""));
|
|
35
|
+
// Build keyboard
|
|
36
|
+
const kb = new InlineKeyboard();
|
|
37
|
+
let headerText = `<b>Available Models</b> (${modelItems.length})`;
|
|
38
|
+
if (totalPages > 1)
|
|
39
|
+
headerText += ` — page ${safePage + 1}/${totalPages}`;
|
|
40
|
+
// Figure out which providers appear on this page
|
|
41
|
+
const pageProviders = new Set();
|
|
42
|
+
for (const item of pageModels) {
|
|
43
|
+
if (item.type === "model")
|
|
44
|
+
pageProviders.add(item.model.provider);
|
|
45
|
+
}
|
|
46
|
+
let lastProvider = "";
|
|
47
|
+
for (const item of pageModels) {
|
|
48
|
+
if (item.type !== "model")
|
|
49
|
+
continue;
|
|
50
|
+
const m = item.model;
|
|
51
|
+
// Add provider header row if new provider
|
|
52
|
+
if (m.provider !== lastProvider) {
|
|
53
|
+
kb.row().text(`— ${m.provider} —`, "mdl_noop");
|
|
54
|
+
lastProvider = m.provider;
|
|
55
|
+
}
|
|
56
|
+
const isActive = selectedModel?.providerID === m.provider && selectedModel?.modelID === m.id;
|
|
57
|
+
const badges = [];
|
|
58
|
+
if (m.reasoning)
|
|
59
|
+
badges.push("reasoning");
|
|
60
|
+
if (m.attachment)
|
|
61
|
+
badges.push("vision");
|
|
62
|
+
const badgeStr = badges.length > 0 ? " [" + badges.join(", ") + "]" : "";
|
|
63
|
+
const label = isActive ? `✓ ${m.name}${badgeStr}` : `${m.name}${badgeStr}`;
|
|
64
|
+
const callbackData = `mdl:${m.provider}/${m.id}`;
|
|
65
|
+
kb.row().text(label, callbackData);
|
|
66
|
+
}
|
|
67
|
+
// Pagination row
|
|
68
|
+
if (totalPages > 1) {
|
|
69
|
+
kb.row();
|
|
70
|
+
if (safePage > 0) {
|
|
71
|
+
kb.text("« Prev", `mdl_pg:${safePage - 1}`);
|
|
72
|
+
}
|
|
73
|
+
kb.text(`${safePage + 1}/${totalPages}`, "mdl_noop");
|
|
74
|
+
if (safePage < totalPages - 1) {
|
|
75
|
+
kb.text("Next »", `mdl_pg:${safePage + 1}`);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
return { keyboard: kb, text: headerText };
|
|
79
|
+
}
|
|
10
80
|
export function registerAdminCommands(bot) {
|
|
11
81
|
bot.command("health", async (ctx) => {
|
|
12
82
|
try {
|
|
@@ -199,33 +269,8 @@ export function registerAdminCommands(bot) {
|
|
|
199
269
|
m.active = true;
|
|
200
270
|
}
|
|
201
271
|
}
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
for (const m of models) {
|
|
205
|
-
const list = grouped.get(m.provider) ?? [];
|
|
206
|
-
list.push(m);
|
|
207
|
-
grouped.set(m.provider, list);
|
|
208
|
-
}
|
|
209
|
-
let text = `<b>Available Models</b>\n`;
|
|
210
|
-
for (const [provId, provModels] of grouped) {
|
|
211
|
-
text += `\n<b>${escapeHtml(provId)}</b>\n`;
|
|
212
|
-
for (const m of provModels) {
|
|
213
|
-
const badges = [];
|
|
214
|
-
if (m.reasoning)
|
|
215
|
-
badges.push("reasoning");
|
|
216
|
-
if (m.attachment)
|
|
217
|
-
badges.push("vision");
|
|
218
|
-
if (m.active)
|
|
219
|
-
badges.push("active");
|
|
220
|
-
const badgeStr = badges.length > 0 ? " " + badges.map(b => `[${b}]`).join(" ") : "";
|
|
221
|
-
text += ` <code>${escapeHtml(m.id)}</code>${badgeStr}\n`;
|
|
222
|
-
}
|
|
223
|
-
}
|
|
224
|
-
text += `\n<i>Use /model provider/model to switch</i>`;
|
|
225
|
-
const chunks = chunkMessage(text);
|
|
226
|
-
for (const chunk of chunks) {
|
|
227
|
-
await ctx.reply(chunk, { parse_mode: "HTML" });
|
|
228
|
-
}
|
|
272
|
+
const { keyboard, text } = buildModelKeyboard(models, 0, selected);
|
|
273
|
+
await ctx.reply(text, { parse_mode: "HTML", reply_markup: keyboard });
|
|
229
274
|
}
|
|
230
275
|
catch (err) {
|
|
231
276
|
await ctx.reply(formatCatchError(err, "listing models"), { parse_mode: "HTML" });
|
|
@@ -309,6 +354,66 @@ export function registerAdminCommands(bot) {
|
|
|
309
354
|
}
|
|
310
355
|
await ctx.reply(`Model set to <code>${providerID}/${modelID}</code>${capStr}`, { parse_mode: "HTML" });
|
|
311
356
|
});
|
|
357
|
+
// --- Callback query handlers for model selection ---
|
|
358
|
+
bot.callbackQuery(/^mdl:(.+)$/, async (ctx) => {
|
|
359
|
+
try {
|
|
360
|
+
const data = ctx.match[1];
|
|
361
|
+
const slashIdx = data.indexOf("/");
|
|
362
|
+
if (slashIdx === -1) {
|
|
363
|
+
await ctx.answerCallbackQuery({ text: "Invalid model" });
|
|
364
|
+
return;
|
|
365
|
+
}
|
|
366
|
+
const providerID = data.slice(0, slashIdx);
|
|
367
|
+
const modelID = data.slice(slashIdx + 1);
|
|
368
|
+
setSelectedModel(providerID, modelID);
|
|
369
|
+
// Build capability string
|
|
370
|
+
let capStr = "";
|
|
371
|
+
try {
|
|
372
|
+
const provider = getProvider();
|
|
373
|
+
const models = await provider.listModels();
|
|
374
|
+
const match = models.find(m => m.id === modelID && m.provider === providerID);
|
|
375
|
+
if (match) {
|
|
376
|
+
const caps = [];
|
|
377
|
+
if (match.reasoning)
|
|
378
|
+
caps.push("reasoning");
|
|
379
|
+
if (match.attachment)
|
|
380
|
+
caps.push("vision");
|
|
381
|
+
capStr = caps.length > 0 ? `\n<b>Capabilities:</b> ${caps.join(", ")}` : "";
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
catch {
|
|
385
|
+
// Ignore — capabilities are optional info
|
|
386
|
+
}
|
|
387
|
+
await ctx.editMessageText(`Model set to <code>${escapeHtml(providerID)}/${escapeHtml(modelID)}</code>${capStr}`, { parse_mode: "HTML" });
|
|
388
|
+
await ctx.answerCallbackQuery({ text: "Model selected!" });
|
|
389
|
+
}
|
|
390
|
+
catch (err) {
|
|
391
|
+
await ctx.answerCallbackQuery({ text: "Failed to set model" });
|
|
392
|
+
}
|
|
393
|
+
});
|
|
394
|
+
bot.callbackQuery(/^mdl_pg:(\d+)$/, async (ctx) => {
|
|
395
|
+
try {
|
|
396
|
+
const page = parseInt(ctx.match[1], 10);
|
|
397
|
+
const provider = getProvider();
|
|
398
|
+
const models = await provider.listModels();
|
|
399
|
+
const selected = getSelectedModel();
|
|
400
|
+
// Mark active
|
|
401
|
+
for (const m of models) {
|
|
402
|
+
if (selected && selected.providerID === m.provider && selected.modelID === m.id) {
|
|
403
|
+
m.active = true;
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
const { keyboard, text } = buildModelKeyboard(models, page, selected);
|
|
407
|
+
await ctx.editMessageText(text, { parse_mode: "HTML", reply_markup: keyboard });
|
|
408
|
+
await ctx.answerCallbackQuery();
|
|
409
|
+
}
|
|
410
|
+
catch (err) {
|
|
411
|
+
await ctx.answerCallbackQuery({ text: "Failed to load page" });
|
|
412
|
+
}
|
|
413
|
+
});
|
|
414
|
+
bot.callbackQuery("mdl_noop", async (ctx) => {
|
|
415
|
+
await ctx.answerCallbackQuery();
|
|
416
|
+
});
|
|
312
417
|
bot.command("system", async (ctx) => {
|
|
313
418
|
const action = ctx.match?.trim();
|
|
314
419
|
if (action === "reload") {
|
|
@@ -318,7 +423,7 @@ export function registerAdminCommands(bot) {
|
|
|
318
423
|
return;
|
|
319
424
|
}
|
|
320
425
|
const prompt = getSystemPrompt();
|
|
321
|
-
const source = isUsingCustomPrompt() ? "Custom (
|
|
426
|
+
const source = isUsingCustomPrompt() ? "Custom (SKILL.md)" : "Default (built-in)";
|
|
322
427
|
const escaped = escapeHtml(prompt.length > 500 ? prompt.slice(0, 500) + "\n\n...(truncated)" : prompt);
|
|
323
428
|
await ctx.reply(`<b>System Prompt</b>\n` +
|
|
324
429
|
`<b>Source:</b> ${source} | <b>Length:</b> ${prompt.length} chars\n\n` +
|