@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.
Files changed (66) hide show
  1. package/README.md +89 -47
  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 +235 -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 +23 -25
  30. package/dist/index.js.map +1 -1
  31. package/dist/providers/claude.js +31 -30
  32. package/dist/providers/claude.js.map +1 -1
  33. package/dist/providers/codex.js +29 -28
  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 +8 -0
  47. package/dist/utils/logger.js +16 -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 +124 -108
  61. package/docs/features.md +70 -36
  62. package/docs/getting-started.md +63 -30
  63. package/docs/providers.md +18 -32
  64. package/docs/troubleshooting.md +108 -63
  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
@@ -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,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
- Set `PROVIDER` in `.env` to select your coding agent backend.
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
- | 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` |
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
- ```env
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
- ```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
- ```
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
- ```env
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 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
- ```
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 `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.
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 (selects based on PROVIDER env var)
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
@@ -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":";AACA,OAAO,EAAE,YAAY,EAAE,MAAM,IAAI,CAAC;AAClC,OAAO,EAAE,IAAI,EAAE,OAAO,EAAE,MAAM,MAAM,CAAC;AACrC,OAAO,EAAE,aAAa,EAAE,MAAM,KAAK,CAAC;AACpC,OAAO,EAAE,UAAU,EAAE,SAAS,EAAE,MAAM,mBAAmB,CAAC;AAC1D,OAAO,EAAE,cAAc,EAAE,MAAM,mBAAmB,CAAC;AAEnD,MAAM,SAAS,GAAG,OAAO,CAAC,aAAa,CAAC,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC;AAE1D,SAAS,YAAY;IACnB,IAAI,CAAC;QACH,MAAM,GAAG,GAAG,IAAI,CAAC,KAAK,CAAC,YAAY,CAAC,IAAI,CAAC,SAAS,EAAE,IAAI,EAAE,cAAc,CAAC,EAAE,OAAO,CAAC,CAAC,CAAC;QACrF,OAAO,CAAC,GAAG,CAAC,UAAU,GAAG,CAAC,OAAO,EAAE,CAAC,CAAC;IACvC,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,CAAC,GAAG,CAAC,yBAAyB,CAAC,CAAC;IACzC,CAAC;AACH,CAAC;AAED,SAAS,SAAS;IAChB,OAAO,CAAC,GAAG,CAAC;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CA6Bb,CAAC,CAAC;AACH,CAAC;AAED,MAAM,eAAe,GAAG,IAAI,GAAG,CAAC,CAAC,OAAO,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,EAAE,QAAQ,EAAE,QAAQ,CAAC,CAAC,CAAC;AAE1F,KAAK,UAAU,IAAI;IACjB,MAAM,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC;IACnC,MAAM,UAAU,GAAG,IAAI,CAAC,CAAC,CAAC,CAAC;IAE3B,iDAAiD;IACjD,IAAI,UAAU,IAAI,eAAe,CAAC,GAAG,CAAC,UAAU,CAAC,EAAE,CAAC;QAClD,MAAM,MAAM,GAAG,MAAM,MAAM,CAAC,aAAa,CAAC,CAAC;QAC3C,QAAQ,UAAU,EAAE,CAAC;YACnB,KAAK,OAAO;gBACV,MAAM,CAAC,WAAW,EAAE,CAAC;gBACrB,MAAM;YACR,KAAK,MAAM;gBACT,MAAM,CAAC,UAAU,EAAE,CAAC;gBACpB,MAAM;YACR,KAAK,SAAS;gBACZ,MAAM,CAAC,aAAa,EAAE,CAAC;gBACvB,MAAM;YACR,KAAK,MAAM;gBACT,MAAM,CAAC,UAAU,EAAE,CAAC;gBACpB,MAAM;YACR,KAAK,QAAQ;gBACX,MAAM,CAAC,YAAY,EAAE,CAAC;gBACtB,MAAM;YACR,KAAK,QAAQ,CAAC,CAAC,CAAC;gBACd,MAAM,EAAE,MAAM,EAAE,GAAG,MAAM,MAAM,CAAC,aAAa,CAAC,CAAC;gBAC/C,MAAM,EAAE,CAAC;gBACT,MAAM;YACR,CAAC;QACH,CAAC;QACD,OAAO;IACT,CAAC;IAED,sDAAsD;IACtD,MAAM,SAAS,GAAG,UAAU,KAAK,SAAS,CAAC;IAE3C,MAAM,MAAM,GAAG,UAAU,EAAE,CAAC;IAE5B,IAAI,MAAM,CAAC,WAAW,EAAE,CAAC;QACvB,YAAY,EAAE,CAAC;QACf,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;IAClB,CAAC;IAED,IAAI,MAAM,CAAC,QAAQ,EAAE,CAAC;QACpB,SAAS,EAAE,CAAC;QACZ,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;IAClB,CAAC;IAED,IAAI,SAAS,IAAI,MAAM,CAAC,UAAU,EAAE,CAAC;QACnC,IAAI,MAAM,CAAC,UAAU,IAAI,CAAC,SAAS,EAAE,CAAC;YACpC,OAAO,CAAC,GAAG,CAAC,iDAAiD,CAAC,CAAC;QACjE,CAAC;QACD,MAAM,MAAM,GAAG,MAAM,cAAc,CAAC,MAAM,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC;QAC3D,SAAS,CAAC,MAAM,CAAC,CAAC;QAElB,+DAA+D;QAC/D,IAAI,SAAS,EAAE,CAAC;YACd,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;QAClB,CAAC;IACH,CAAC;SAAM,CAAC;QACN,SAAS,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC;IAC3B,CAAC;IAED,oBAAoB;IACpB,MAAM,MAAM,CAAC,YAAY,CAAC,CAAC;AAC7B,CAAC;AAED,IAAI,EAAE,CAAC,KAAK,CAAC,CAAC,GAAG,EAAE,EAAE;IACnB,OAAO,CAAC,KAAK,CAAC,QAAQ,EAAE,GAAG,EAAE,OAAO,IAAI,GAAG,CAAC,CAAC;IAC7C,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;AAClB,CAAC,CAAC,CAAC"}
@@ -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` +