@4via6/relay 1.0.0 → 1.1.1

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.
Files changed (66) hide show
  1. package/README.md +99 -48
  2. package/dist/auth.d.ts +1 -0
  3. package/dist/auth.js +6 -3
  4. package/dist/auth.js.map +1 -1
  5. package/dist/bot.js +2 -1
  6. package/dist/bot.js.map +1 -1
  7. package/dist/cli.d.ts +1 -1
  8. package/dist/cli.js +110 -1
  9. package/dist/cli.js.map +1 -1
  10. package/dist/commands/admin.js +134 -29
  11. package/dist/commands/admin.js.map +1 -1
  12. package/dist/commands/media.js +10 -7
  13. package/dist/commands/media.js.map +1 -1
  14. package/dist/config/index.d.ts +7 -0
  15. package/dist/config/index.js +16 -0
  16. package/dist/config/index.js.map +1 -0
  17. package/dist/config/loader.d.ts +11 -0
  18. package/dist/config/loader.js +161 -0
  19. package/dist/config/loader.js.map +1 -0
  20. package/dist/config/schema.d.ts +32 -0
  21. package/dist/config/schema.js +32 -0
  22. package/dist/config/schema.js.map +1 -0
  23. package/dist/config/setup.d.ts +3 -0
  24. package/dist/config/setup.js +142 -0
  25. package/dist/config/setup.js.map +1 -0
  26. package/dist/daemon.d.ts +5 -0
  27. package/dist/daemon.js +215 -0
  28. package/dist/daemon.js.map +1 -0
  29. package/dist/index.js +33 -29
  30. package/dist/index.js.map +1 -1
  31. package/dist/providers/claude.js +35 -33
  32. package/dist/providers/claude.js.map +1 -1
  33. package/dist/providers/codex.js +33 -31
  34. package/dist/providers/codex.js.map +1 -1
  35. package/dist/providers/index.js +2 -1
  36. package/dist/providers/index.js.map +1 -1
  37. package/dist/providers/opencode.js +8 -5
  38. package/dist/providers/opencode.js.map +1 -1
  39. package/dist/session.js +3 -2
  40. package/dist/session.js.map +1 -1
  41. package/dist/update.d.ts +1 -0
  42. package/dist/update.js +132 -0
  43. package/dist/update.js.map +1 -0
  44. package/dist/utils/files.js +2 -1
  45. package/dist/utils/files.js.map +1 -1
  46. package/dist/utils/logger.d.ts +7 -0
  47. package/dist/utils/logger.js +15 -0
  48. package/dist/utils/logger.js.map +1 -0
  49. package/dist/utils/store.js +1 -0
  50. package/dist/utils/store.js.map +1 -1
  51. package/dist/utils/stream.js +6 -3
  52. package/dist/utils/stream.js.map +1 -1
  53. package/dist/utils/stt.js +27 -16
  54. package/dist/utils/stt.js.map +1 -1
  55. package/dist/utils/system-prompt.js +16 -5
  56. package/dist/utils/system-prompt.js.map +1 -1
  57. package/dist/utils/timeout.js +2 -3
  58. package/dist/utils/timeout.js.map +1 -1
  59. package/docs/commands.md +7 -15
  60. package/docs/configuration.md +122 -108
  61. package/docs/features.md +70 -36
  62. package/docs/getting-started.md +75 -30
  63. package/docs/providers.md +18 -32
  64. package/docs/troubleshooting.md +126 -75
  65. package/package.json +4 -3
  66. package/.env.example +0 -50
package/README.md CHANGED
@@ -1,10 +1,16 @@
1
1
  # Relay
2
2
 
3
+ [![npm version](https://img.shields.io/npm/v/@4via6/relay)](https://www.npmjs.com/package/@4via6/relay)
4
+ [![npm downloads](https://img.shields.io/npm/dm/@4via6/relay)](https://www.npmjs.com/package/@4via6/relay)
5
+ [![License: MIT](https://img.shields.io/badge/License-MIT-green.svg)](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 with a single env var
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 file, hot-reload on change
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
@@ -25,7 +31,7 @@ Telegram bot for managing AI coding agents remotely. Supports [OpenCode](https:/
25
31
  | Guide | Description |
26
32
  |-------|-------------|
27
33
  | [Getting Started](docs/getting-started.md) | Installation, prerequisites, first steps |
28
- | [Configuration](docs/configuration.md) | All environment variables and options |
34
+ | [Configuration](docs/configuration.md) | All config options and CLI flags |
29
35
  | [Providers](docs/providers.md) | Detailed setup for each provider |
30
36
  | [Commands](docs/commands.md) | Full command reference with examples |
31
37
  | [Features](docs/features.md) | Streaming, file attachments, voice, MCP, models |
@@ -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
- Then create a `.env` file in your working directory (see [Configuration](docs/configuration.md)):
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/relay.git
54
- cd relay
54
+ git clone https://github.com/Harsh-2002/Relay.git
55
+ cd Relay
55
56
  npm install
56
57
  npm run build
57
- cp .env.example .env
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,92 @@ 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
+ **Upgrading from v1.0.x** (before `relay update` existed):
104
+
105
+ ```bash
106
+ # npm
107
+ npm install -g @4via6/relay@latest
108
+
109
+ # From source
110
+ git pull && npm install && npm run build
111
+ ```
112
+
113
+ ## Configuration
114
+
115
+ Config is stored in `.relay/config.json`. Use the setup wizard or CLI flags:
116
+
117
+ ```bash
118
+ relay onboard # Interactive wizard
119
+ relay --bot-token=xxx --allowed-user-id=123 --provider=opencode # CLI flags
120
+ ```
121
+
122
+ ### CLI flags
123
+
124
+ | Flag | Description |
125
+ |------|-------------|
126
+ | `--help` | Show help |
127
+ | `--version` | Show version |
128
+ | `--bot-token` | Telegram bot token |
129
+ | `--allowed-user-id` | Telegram user ID |
130
+ | `--provider` | Provider: `opencode`, `claude`, `codex` |
131
+ | `--bot-mode` | `polling` or `webhook` |
132
+ | `--streaming-enabled` | `true` or `false` |
133
+ | `--log-level` | `debug`, `info`, `warn`, `error` |
134
+ | `--data-dir` | Data directory (default: `.relay/`) |
135
+ | `--system-prompt-file` | Custom system prompt file |
136
+
137
+
74
138
  ## Providers
75
139
 
76
- Set `PROVIDER` in `.env` to select your coding agent backend.
140
+ | Provider | Requirement |
141
+ |----------|-------------|
142
+ | [OpenCode](https://github.com/opencode-ai/opencode) | included |
143
+ | [Claude Code](https://docs.anthropic.com/en/docs/agents-and-tools/claude-code/overview) | must be installed separately |
144
+ | [OpenAI Codex](https://github.com/openai/codex) | must be installed separately |
77
145
 
78
- | Provider | `PROVIDER=` | Required env vars | Install |
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` |
146
+ Each provider must be installed on the system where Relay runs. Relay calls the tool directly — how you install it (npm, pip, binary, etc.) is up to you.
83
147
 
84
148
  ### OpenCode
85
149
 
86
- ```env
87
- PROVIDER=opencode
88
- OPENCODE_MODE=start # "start" (spawn server) or "connect" (remote URL)
89
- OPENCODE_URL=http://localhost:4096
90
- ```
150
+ Select during `relay onboard` or pass `--provider=opencode`. Supports both `start` mode (spawns local server) and `connect` mode (remote URL).
91
151
 
92
152
  ### Claude Code
93
153
 
94
- ```env
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
- ```
154
+ Select during `relay onboard` or pass `--provider=claude`. Claude Code must be installed and accessible. API key must be set in the environment where Claude Code runs.
100
155
 
101
156
  ### OpenAI Codex
102
157
 
103
- ```env
104
- PROVIDER=codex
105
- CODEX_API_KEY=sk-...
106
- CODEX_MODEL=o3 # use /models to see all available
107
- ```
158
+ Select during `relay onboard` or pass `--provider=codex`. Codex must be installed and accessible. API key must be set in the environment where Codex runs.
108
159
 
109
160
  ## Commands
110
161
 
@@ -188,25 +239,24 @@ MCP servers extend the AI's capabilities with additional tools (browsers, databa
188
239
 
189
240
  ## Voice / STT
190
241
 
191
- Configure one or more speech-to-text providers for voice message support. The cheapest available provider is auto-selected.
192
-
193
- ```env
194
- GROQ_API_KEY=... # Groq Whisper (fastest, free tier)
195
- OPENAI_API_KEY=... # OpenAI Whisper
196
- ASSEMBLYAI_API_KEY=... # AssemblyAI
197
- ```
242
+ Configure speech-to-text providers during `relay onboard` or pass API keys via CLI flags. The cheapest available provider is auto-selected.
198
243
 
199
244
  ## System Prompt
200
245
 
201
- The bot loads a system prompt from `skill.md` in the project root (or the path set in `SYSTEM_PROMPT_FILE`). If the file doesn't exist, a default prompt is used. The file is watched for changes and reloaded automatically. Use `/system reload` to force a reload.
246
+ 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
247
 
203
248
  ## Architecture
204
249
 
205
250
  ```
206
251
  src/
252
+ config/
253
+ schema.ts -- Config type definitions
254
+ loader.ts -- Config resolution (CLI > file > env > defaults)
255
+ setup.ts -- Interactive setup wizard
256
+ index.ts -- Config singleton
207
257
  providers/
208
258
  types.ts -- Provider interface, capabilities, MCP/model types
209
- index.ts -- Provider factory (selects based on PROVIDER env var)
259
+ index.ts -- Provider factory
210
260
  opencode.ts -- OpenCode SDK provider
211
261
  claude.ts -- Claude Code / Agent SDK provider
212
262
  codex.ts -- OpenAI Codex SDK provider
@@ -221,6 +271,7 @@ src/
221
271
  shell.ts -- Shell and command execution
222
272
  mcp.ts -- MCP server management
223
273
  utils/
274
+ logger.ts -- Pino-based structured logging
224
275
  store.ts -- JSON file-backed persistence (.relay/)
225
276
  stream.ts -- Streaming response handler
226
277
  files.ts -- Outbound file attachment handling
package/dist/auth.d.ts CHANGED
@@ -1,3 +1,4 @@
1
1
  import type { Context, NextFunction } from "grammy";
2
+ export declare function initAuth(userId: number): boolean;
2
3
  export declare function isAuthConfigured(): boolean;
3
4
  export declare function authMiddleware(ctx: Context, next: NextFunction): Promise<void>;
package/dist/auth.js CHANGED
@@ -1,9 +1,12 @@
1
- // Parse ALLOWED_USER_ID fail-secure: NaN means "block all"
2
- const raw = process.env.ALLOWED_USER_ID;
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,6DAA6D;AAC7D,MAAM,GAAG,GAAG,OAAO,CAAC,GAAG,CAAC,eAAe,CAAC;AACxC,MAAM,aAAa,GAAG,GAAG,CAAC,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC;AAE9C,kEAAkE;AAClE,MAAM,UAAU,GAAG,EAAE,CAAC;AACtB,MAAM,WAAW,GAAG,IAAI,GAAG,EAAoB,CAAC;AAEhD,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"}
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
- console.error("Bot error:", e instanceof Error ? e.message : String(e));
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;AAEvD,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,OAAO,CAAC,KAAK,CAAC,YAAY,EAAE,CAAC,YAAY,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,CAAC;IAC1E,CAAC,CAAC,CAAC;IAEH,OAAO,GAAG,CAAC;AACb,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
- import "./index.js";
2
+ export {};
package/dist/cli.js CHANGED
@@ -1,3 +1,112 @@
1
1
  #!/usr/bin/env node
2
- import "./index.js";
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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"}
@@ -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
- // Group by provider
203
- const grouped = new Map();
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 (skill.md)" : "Default (built-in)";
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` +