@marcopeg/hal 1.0.11
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +655 -0
- package/dist/agent/index.d.ts +17 -0
- package/dist/agent/index.d.ts.map +1 -0
- package/dist/agent/index.js +30 -0
- package/dist/agent/index.js.map +1 -0
- package/dist/bot/commands/clear.d.ts +7 -0
- package/dist/bot/commands/clear.d.ts.map +1 -0
- package/dist/bot/commands/clear.js +23 -0
- package/dist/bot/commands/clear.js.map +1 -0
- package/dist/bot/commands/help.d.ts +3 -0
- package/dist/bot/commands/help.d.ts.map +1 -0
- package/dist/bot/commands/help.js +16 -0
- package/dist/bot/commands/help.js.map +1 -0
- package/dist/bot/commands/loader.d.ts +26 -0
- package/dist/bot/commands/loader.d.ts.map +1 -0
- package/dist/bot/commands/loader.js +206 -0
- package/dist/bot/commands/loader.js.map +1 -0
- package/dist/bot/commands/start.d.ts +3 -0
- package/dist/bot/commands/start.d.ts.map +1 -0
- package/dist/bot/commands/start.js +10 -0
- package/dist/bot/commands/start.js.map +1 -0
- package/dist/bot/commands/watcher.d.ts +11 -0
- package/dist/bot/commands/watcher.d.ts.map +1 -0
- package/dist/bot/commands/watcher.js +106 -0
- package/dist/bot/commands/watcher.js.map +1 -0
- package/dist/bot/handlers/document.d.ts +7 -0
- package/dist/bot/handlers/document.d.ts.map +1 -0
- package/dist/bot/handlers/document.js +128 -0
- package/dist/bot/handlers/document.js.map +1 -0
- package/dist/bot/handlers/index.d.ts +5 -0
- package/dist/bot/handlers/index.d.ts.map +1 -0
- package/dist/bot/handlers/index.js +5 -0
- package/dist/bot/handlers/index.js.map +1 -0
- package/dist/bot/handlers/photo.d.ts +7 -0
- package/dist/bot/handlers/photo.d.ts.map +1 -0
- package/dist/bot/handlers/photo.js +87 -0
- package/dist/bot/handlers/photo.js.map +1 -0
- package/dist/bot/handlers/text.d.ts +7 -0
- package/dist/bot/handlers/text.d.ts.map +1 -0
- package/dist/bot/handlers/text.js +186 -0
- package/dist/bot/handlers/text.js.map +1 -0
- package/dist/bot/handlers/voice.d.ts +7 -0
- package/dist/bot/handlers/voice.d.ts.map +1 -0
- package/dist/bot/handlers/voice.js +147 -0
- package/dist/bot/handlers/voice.js.map +1 -0
- package/dist/bot/middleware/auth.d.ts +7 -0
- package/dist/bot/middleware/auth.d.ts.map +1 -0
- package/dist/bot/middleware/auth.js +23 -0
- package/dist/bot/middleware/auth.js.map +1 -0
- package/dist/bot/middleware/rateLimit.d.ts +11 -0
- package/dist/bot/middleware/rateLimit.d.ts.map +1 -0
- package/dist/bot/middleware/rateLimit.js +49 -0
- package/dist/bot/middleware/rateLimit.js.map +1 -0
- package/dist/bot.d.ts +11 -0
- package/dist/bot.d.ts.map +1 -0
- package/dist/bot.js +93 -0
- package/dist/bot.js.map +1 -0
- package/dist/claude/executor.d.ts +21 -0
- package/dist/claude/executor.d.ts.map +1 -0
- package/dist/claude/executor.js +185 -0
- package/dist/claude/executor.js.map +1 -0
- package/dist/claude/parser.d.ts +13 -0
- package/dist/claude/parser.d.ts.map +1 -0
- package/dist/claude/parser.js +63 -0
- package/dist/claude/parser.js.map +1 -0
- package/dist/cli.d.ts +3 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +192 -0
- package/dist/cli.js.map +1 -0
- package/dist/config.d.ts +216 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +396 -0
- package/dist/config.js.map +1 -0
- package/dist/context/resolver.d.ts +19 -0
- package/dist/context/resolver.d.ts.map +1 -0
- package/dist/context/resolver.js +171 -0
- package/dist/context/resolver.js.map +1 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +5 -0
- package/dist/index.js.map +1 -0
- package/dist/logger.d.ts +17 -0
- package/dist/logger.d.ts.map +1 -0
- package/dist/logger.js +44 -0
- package/dist/logger.js.map +1 -0
- package/dist/telegram/chunker.d.ts +10 -0
- package/dist/telegram/chunker.d.ts.map +1 -0
- package/dist/telegram/chunker.js +88 -0
- package/dist/telegram/chunker.js.map +1 -0
- package/dist/telegram/fileSender.d.ts +8 -0
- package/dist/telegram/fileSender.d.ts.map +1 -0
- package/dist/telegram/fileSender.js +46 -0
- package/dist/telegram/fileSender.js.map +1 -0
- package/dist/transcription/whisper.d.ts +11 -0
- package/dist/transcription/whisper.d.ts.map +1 -0
- package/dist/transcription/whisper.js +58 -0
- package/dist/transcription/whisper.js.map +1 -0
- package/dist/types.d.ts +22 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +2 -0
- package/dist/types.js.map +1 -0
- package/dist/user/setup.d.ts +26 -0
- package/dist/user/setup.d.ts.map +1 -0
- package/dist/user/setup.js +73 -0
- package/dist/user/setup.js.map +1 -0
- package/package.json +56 -0
package/README.md
ADDED
|
@@ -0,0 +1,655 @@
|
|
|
1
|
+
# HAL
|
|
2
|
+
|
|
3
|
+
A Telegram bot that provides access to Claude Code as a personal assistant. Run Claude Code across multiple projects simultaneously, each with its own dedicated Telegram bot.
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
|
|
7
|
+
- **Multi-project support** — run multiple bots from a single config, each connected to a different directory
|
|
8
|
+
- Chat with Claude Code via Telegram
|
|
9
|
+
- Send images and documents for analysis
|
|
10
|
+
- **Voice message support** with local Whisper transcription
|
|
11
|
+
- **File sending** — Claude can send files back to you
|
|
12
|
+
- **Context injection** — every message includes metadata (timestamps, user info, custom values) and supports hot-reloaded hooks
|
|
13
|
+
- **Custom slash commands** — add `.mjs` command files per-project or globally; hot-reloaded so Claude can create new commands at runtime
|
|
14
|
+
- **Skills** — Claude Code `.claude/skills/` entries are automatically exposed as Telegram slash commands; no extra setup needed
|
|
15
|
+
- Persistent conversation sessions per user
|
|
16
|
+
- Per-project access control, rate limiting, and logging
|
|
17
|
+
- Log persistence to file with daily rotation support
|
|
18
|
+
|
|
19
|
+
## How It Works
|
|
20
|
+
|
|
21
|
+
This tool runs one Claude Code subprocess per project, each in its configured working directory. Claude Code reads all its standard config files from that directory:
|
|
22
|
+
|
|
23
|
+
- `CLAUDE.md` — Project-specific instructions and context
|
|
24
|
+
- `.claude/settings.json` — Permissions and tool settings
|
|
25
|
+
- `.claude/commands/` — Custom slash commands
|
|
26
|
+
- `.mcp.json` — MCP server configurations
|
|
27
|
+
|
|
28
|
+
You get the full power of Claude Code — file access, code execution, configured MCP tools — all accessible through Telegram.
|
|
29
|
+
|
|
30
|
+
See [Claude Code documentation](https://docs.anthropic.com/en/docs/claude-code) for details on Claude Code configuration.
|
|
31
|
+
|
|
32
|
+
## Prerequisites
|
|
33
|
+
|
|
34
|
+
- Node.js 18+
|
|
35
|
+
- [Claude Code CLI](https://github.com/anthropics/claude-code) installed and authenticated
|
|
36
|
+
- A Telegram bot token per project (from [@BotFather](https://t.me/BotFather)) — see [Creating a Telegram Bot](#creating-a-telegram-bot)
|
|
37
|
+
- **ffmpeg** (required for voice messages) — `brew install ffmpeg` on macOS
|
|
38
|
+
|
|
39
|
+
## Quick Start
|
|
40
|
+
|
|
41
|
+
```bash
|
|
42
|
+
# Create hal.config.json in the current directory
|
|
43
|
+
npx @marcopeg/hal init
|
|
44
|
+
|
|
45
|
+
# Edit hal.config.json: add your bot token and project path
|
|
46
|
+
# then start all bots
|
|
47
|
+
npx @marcopeg/hal
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
## Installation
|
|
51
|
+
|
|
52
|
+
```bash
|
|
53
|
+
# Initialize config in a specific directory
|
|
54
|
+
npx @marcopeg/hal init --cwd ./workspace
|
|
55
|
+
|
|
56
|
+
# Start bots using the config in that directory
|
|
57
|
+
npx @marcopeg/hal --cwd ./workspace
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
## Configuration
|
|
61
|
+
|
|
62
|
+
### hal.config.json
|
|
63
|
+
|
|
64
|
+
Create a `hal.config.json` in your workspace directory (where you run the CLI from). Secrets like bot tokens should be kept out of this file — use `${VAR_NAME}` placeholders and store the values in `.env.local` or the shell environment instead.
|
|
65
|
+
|
|
66
|
+
```json
|
|
67
|
+
{
|
|
68
|
+
"globals": {
|
|
69
|
+
"claude": { "command": "claude" },
|
|
70
|
+
"logging": { "level": "info", "flow": true, "persist": false },
|
|
71
|
+
"rateLimit": { "max": 10, "windowMs": 60000 },
|
|
72
|
+
"access": { "allowedUserIds": [] }
|
|
73
|
+
},
|
|
74
|
+
"projects": [
|
|
75
|
+
{
|
|
76
|
+
"name": "backend",
|
|
77
|
+
"cwd": "./backend",
|
|
78
|
+
"telegram": { "botToken": "${BACKEND_BOT_TOKEN}" },
|
|
79
|
+
"access": { "allowedUserIds": [123456789] },
|
|
80
|
+
"logging": { "persist": true }
|
|
81
|
+
},
|
|
82
|
+
{
|
|
83
|
+
"name": "frontend",
|
|
84
|
+
"cwd": "./frontend",
|
|
85
|
+
"telegram": { "botToken": "${FRONTEND_BOT_TOKEN}" },
|
|
86
|
+
"access": { "allowedUserIds": [123456789] }
|
|
87
|
+
}
|
|
88
|
+
]
|
|
89
|
+
}
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
### hal.config.local.json
|
|
93
|
+
|
|
94
|
+
An optional `hal.config.local.json` placed next to `hal.config.json` is deep-merged on top of the base config at boot time. It is gitignored and is the recommended place for machine-specific values or secrets that you don't want committed.
|
|
95
|
+
|
|
96
|
+
Every field is optional. Project entries are matched to base projects by `name` (preferred) or `cwd` — they cannot introduce new projects.
|
|
97
|
+
|
|
98
|
+
```json
|
|
99
|
+
{
|
|
100
|
+
"projects": [
|
|
101
|
+
{
|
|
102
|
+
"name": "backend",
|
|
103
|
+
"telegram": { "botToken": "7123456789:AAHActual-token-here" },
|
|
104
|
+
"logging": { "persist": true }
|
|
105
|
+
}
|
|
106
|
+
]
|
|
107
|
+
}
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
### Environment variable substitution
|
|
111
|
+
|
|
112
|
+
Any string value in `hal.config.json` or `hal.config.local.json` (except inside `context` blocks — see [Context Injection](#context-injection)) can reference an environment variable with `${VAR_NAME}` syntax. Variables are resolved at boot time from the following sources in priority order (first match wins):
|
|
113
|
+
|
|
114
|
+
1. `<config-dir>/.env.local` _(gitignored)_
|
|
115
|
+
2. `<config-dir>/.env`
|
|
116
|
+
3. `<project-cwd>/.env.local` _(gitignored)_
|
|
117
|
+
4. `<project-cwd>/.env`
|
|
118
|
+
5. Shell environment (`process.env`)
|
|
119
|
+
|
|
120
|
+
```bash
|
|
121
|
+
# .env (safe to commit — no real secrets)
|
|
122
|
+
BACKEND_BOT_TOKEN=
|
|
123
|
+
FRONTEND_BOT_TOKEN=
|
|
124
|
+
|
|
125
|
+
# .env.local (gitignored — real secrets go here)
|
|
126
|
+
BACKEND_BOT_TOKEN=7123456789:AAHActual-token-here
|
|
127
|
+
FRONTEND_BOT_TOKEN=7987654321:AAHAnother-token-here
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
If a referenced variable cannot be resolved from any source the bot exits at boot with a clear error message naming the variable and the config field that references it.
|
|
131
|
+
|
|
132
|
+
On every boot an `info`-level log lists all config and env files that were loaded, in resolution order, so you can always see exactly where each value came from.
|
|
133
|
+
|
|
134
|
+
### Context Injection
|
|
135
|
+
|
|
136
|
+
Every message sent to Claude is automatically enriched with a structured context header. This provides metadata (message info, timestamps, custom values) so Claude can reason about the current request without extra tool calls.
|
|
137
|
+
|
|
138
|
+
#### Implicit context (always-on)
|
|
139
|
+
|
|
140
|
+
These keys are injected for every message, even without any `context` configuration:
|
|
141
|
+
|
|
142
|
+
| Key | Description |
|
|
143
|
+
|-----|-------------|
|
|
144
|
+
| `bot.messageId` | Telegram message ID |
|
|
145
|
+
| `bot.timestamp` | Message Unix timestamp (seconds) |
|
|
146
|
+
| `bot.datetime` | Message datetime, ISO 8601 |
|
|
147
|
+
| `bot.userId` | Sender's Telegram user ID |
|
|
148
|
+
| `bot.username` | Sender's @username (if set) |
|
|
149
|
+
| `bot.firstName` | Sender's first name |
|
|
150
|
+
| `bot.chatId` | Chat ID |
|
|
151
|
+
| `bot.messageType` | `text` / `photo` / `document` / `voice` |
|
|
152
|
+
| `project.name` | Project name (falls back to internal slug if not set) |
|
|
153
|
+
| `project.cwd` | Resolved absolute project working directory |
|
|
154
|
+
| `project.slug` | Claude Code-compatible slug (full path with `/` → `-`) |
|
|
155
|
+
| `sys.datetime` | Current local datetime with timezone |
|
|
156
|
+
| `sys.date` | Current date, `YYYY-MM-DD` |
|
|
157
|
+
| `sys.time` | Current time, `HH:MM:SS` |
|
|
158
|
+
| `sys.ts` | Current Unix timestamp (seconds) |
|
|
159
|
+
| `sys.tz` | Timezone name (e.g. `Europe/Berlin`) |
|
|
160
|
+
|
|
161
|
+
#### Custom context via config
|
|
162
|
+
|
|
163
|
+
Add a `context` object at the root level of `hal.config.json` (applies to all projects) or inside individual projects (overrides root per key):
|
|
164
|
+
|
|
165
|
+
```json
|
|
166
|
+
{
|
|
167
|
+
"globals": { ... },
|
|
168
|
+
"context": {
|
|
169
|
+
"messageId": "${bot.messageId}",
|
|
170
|
+
"currentTime": "${sys.datetime}",
|
|
171
|
+
"buildVersion": "#{git rev-parse --short HEAD}"
|
|
172
|
+
},
|
|
173
|
+
"projects": [
|
|
174
|
+
{
|
|
175
|
+
"name": "backend",
|
|
176
|
+
"cwd": "./backend",
|
|
177
|
+
"telegram": { "botToken": "${BACKEND_BOT_TOKEN}" },
|
|
178
|
+
"context": {
|
|
179
|
+
"project": "backend",
|
|
180
|
+
"liveTimestamp": "@{date +\"%Y-%m-%d %H:%M:%S\"}"
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
]
|
|
184
|
+
}
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
Project context is merged on top of root — `backend` inherits `messageId`, `currentTime`, and `buildVersion` from root context, and adds `project` and `liveTimestamp`.
|
|
188
|
+
|
|
189
|
+
#### Variable substitution patterns
|
|
190
|
+
|
|
191
|
+
Three patterns are supported in context values:
|
|
192
|
+
|
|
193
|
+
| Pattern | Evaluated | Description |
|
|
194
|
+
|---------|-----------|-------------|
|
|
195
|
+
| `${expr}` | Per message | Looks up `expr` in implicit context (`bot.*`, `sys.*`), then env vars |
|
|
196
|
+
| `#{cmd}` | Once at boot | Runs shell command, caches result for all messages |
|
|
197
|
+
| `@{cmd}` | Per message | Runs shell command fresh for each message |
|
|
198
|
+
|
|
199
|
+
#### Context hooks
|
|
200
|
+
|
|
201
|
+
For advanced enrichment, you can provide a `context.mjs` hook file that transforms the context object with arbitrary JavaScript. Two hook locations are supported:
|
|
202
|
+
|
|
203
|
+
| Location | Scope |
|
|
204
|
+
|----------|-------|
|
|
205
|
+
| `{configDir}/.hal/hooks/context.mjs` | Global — runs for all projects |
|
|
206
|
+
| `{project.cwd}/.hal/hooks/context.mjs` | Project — runs for that project only |
|
|
207
|
+
|
|
208
|
+
When both exist, they chain: global runs first, its output feeds into the project hook. Both are **hot-reloaded** on every message (no restart needed) — so Claude Code itself can create or modify hooks at runtime.
|
|
209
|
+
|
|
210
|
+
```js
|
|
211
|
+
// .hal/hooks/context.mjs
|
|
212
|
+
export default async (context) => ({
|
|
213
|
+
...context,
|
|
214
|
+
project: "my-tracker",
|
|
215
|
+
user: await fetchUserProfile(context["bot.userId"])
|
|
216
|
+
})
|
|
217
|
+
```
|
|
218
|
+
|
|
219
|
+
- **Input**: fully-resolved `Record<string, string>` context
|
|
220
|
+
- **Output**: a `Record<string, string>` — the final context passed to Claude
|
|
221
|
+
- If a hook throws, the bot logs the error and falls back to the pre-hook context
|
|
222
|
+
|
|
223
|
+
#### Prompt format
|
|
224
|
+
|
|
225
|
+
The resolved context is prepended to the user message before passing to Claude:
|
|
226
|
+
|
|
227
|
+
```
|
|
228
|
+
# Context
|
|
229
|
+
- bot.messageId: 12345
|
|
230
|
+
- sys.datetime: 2026-02-26 14:30:00 UTC+1
|
|
231
|
+
- project: backend
|
|
232
|
+
|
|
233
|
+
# User Message
|
|
234
|
+
What files changed today?
|
|
235
|
+
```
|
|
236
|
+
|
|
237
|
+
### `globals`
|
|
238
|
+
|
|
239
|
+
Default settings applied to all projects. Any setting defined in a project overrides its global counterpart.
|
|
240
|
+
|
|
241
|
+
| Key | Description | Default |
|
|
242
|
+
|-----|-------------|---------|
|
|
243
|
+
| `globals.claude.command` | Claude CLI command | `"claude"` |
|
|
244
|
+
| `globals.logging.level` | Log level: `debug`, `info`, `warn`, `error` | `"info"` |
|
|
245
|
+
| `globals.logging.flow` | Write logs to terminal | `true` |
|
|
246
|
+
| `globals.logging.persist` | Write logs to file | `false` |
|
|
247
|
+
| `globals.rateLimit.max` | Max messages per window per user | `10` |
|
|
248
|
+
| `globals.rateLimit.windowMs` | Rate limit window in ms | `60000` |
|
|
249
|
+
| `globals.access.allowedUserIds` | Telegram user IDs allowed by default | `[]` |
|
|
250
|
+
| `globals.dataDir` | Default user data directory | _(see below)_ |
|
|
251
|
+
| `globals.transcription.model` | Whisper model for voice | `"base.en"` |
|
|
252
|
+
| `globals.transcription.showTranscription` | Show transcribed text | `true` |
|
|
253
|
+
|
|
254
|
+
### `projects[]`
|
|
255
|
+
|
|
256
|
+
Each project entry creates one Telegram bot connected to one directory.
|
|
257
|
+
|
|
258
|
+
| Key | Required | Description |
|
|
259
|
+
|-----|----------|-------------|
|
|
260
|
+
| `name` | No | Unique identifier used as a slug for logs/data paths |
|
|
261
|
+
| `cwd` | **Yes** | Path to the project directory (relative to config file, or absolute) |
|
|
262
|
+
| `telegram.botToken` | **Yes** | Telegram bot token from BotFather |
|
|
263
|
+
| `access.allowedUserIds` | No | Override the global user whitelist for this bot |
|
|
264
|
+
| `claude.command` | No | Override the Claude CLI command |
|
|
265
|
+
| `logging.level` | No | Override log level |
|
|
266
|
+
| `logging.flow` | No | Override terminal logging |
|
|
267
|
+
| `logging.persist` | No | Override file logging |
|
|
268
|
+
| `rateLimit.max` | No | Override rate limit max |
|
|
269
|
+
| `rateLimit.windowMs` | No | Override rate limit window |
|
|
270
|
+
| `transcription.model` | No | Override Whisper model |
|
|
271
|
+
| `transcription.showTranscription` | No | Override transcription display |
|
|
272
|
+
| `dataDir` | No | Override user data directory (see below) |
|
|
273
|
+
| `context` | No | Per-project context overrides (see [Context Injection](#context-injection)) |
|
|
274
|
+
|
|
275
|
+
### Project Slug
|
|
276
|
+
|
|
277
|
+
The slug is used as a folder name for log and data paths. It is derived from:
|
|
278
|
+
1. The `name` field, if provided
|
|
279
|
+
2. Otherwise, the `cwd` value slugified (e.g. `./foo/bar` → `foo-bar`)
|
|
280
|
+
|
|
281
|
+
### `dataDir` Values
|
|
282
|
+
|
|
283
|
+
| Value | Resolved Path |
|
|
284
|
+
|-------|---------------|
|
|
285
|
+
| _(empty)_ | `<project-cwd>/.hal/users` |
|
|
286
|
+
| `~` | `<config-dir>/.hal/<slug>/data` |
|
|
287
|
+
| Relative path (e.g. `.mydata`) | `<project-cwd>/<value>` |
|
|
288
|
+
| Absolute path | Used as-is |
|
|
289
|
+
|
|
290
|
+
### Log Files
|
|
291
|
+
|
|
292
|
+
When `logging.persist: true`, logs are written to:
|
|
293
|
+
```
|
|
294
|
+
<config-dir>/.hal/logs/<project-slug>/YYYY-MM-DD.txt
|
|
295
|
+
```
|
|
296
|
+
|
|
297
|
+
## Directory Structure
|
|
298
|
+
|
|
299
|
+
With a config at `~/workspace/hal.config.json`:
|
|
300
|
+
|
|
301
|
+
```
|
|
302
|
+
~/workspace/
|
|
303
|
+
├── hal.config.json
|
|
304
|
+
├── hal.config.local.json (gitignored — local overrides / secrets)
|
|
305
|
+
├── .hal/
|
|
306
|
+
│ ├── hooks/
|
|
307
|
+
│ │ └── context.mjs (global context hook, optional)
|
|
308
|
+
│ ├── commands/
|
|
309
|
+
│ │ └── mycommand.mjs (global command, available to all projects)
|
|
310
|
+
│ └── logs/
|
|
311
|
+
│ ├── backend/
|
|
312
|
+
│ │ └── 2026-02-26.txt (when persist: true)
|
|
313
|
+
│ └── frontend/
|
|
314
|
+
│ └── 2026-02-26.txt
|
|
315
|
+
├── .env (variable declarations, safe to commit)
|
|
316
|
+
├── .env.local (gitignored — actual secret values)
|
|
317
|
+
├── backend/
|
|
318
|
+
│ ├── CLAUDE.md
|
|
319
|
+
│ ├── .claude/
|
|
320
|
+
│ │ ├── settings.json
|
|
321
|
+
│ │ └── skills/
|
|
322
|
+
│ │ └── deploy/
|
|
323
|
+
│ │ └── SKILL.md (skill exposed as /deploy command)
|
|
324
|
+
│ └── .hal/
|
|
325
|
+
│ ├── hooks/
|
|
326
|
+
│ │ └── context.mjs (project context hook, optional)
|
|
327
|
+
│ ├── commands/
|
|
328
|
+
│ │ └── deploy.mjs (project-specific command, optional)
|
|
329
|
+
│ └── users/
|
|
330
|
+
│ └── {userId}/
|
|
331
|
+
│ ├── uploads/ # Files FROM user (to Claude)
|
|
332
|
+
│ ├── downloads/ # Files TO user (from Claude)
|
|
333
|
+
│ └── session.json # Session data
|
|
334
|
+
└── frontend/
|
|
335
|
+
├── CLAUDE.md
|
|
336
|
+
└── .hal/
|
|
337
|
+
└── users/
|
|
338
|
+
```
|
|
339
|
+
|
|
340
|
+
## CLI Commands
|
|
341
|
+
|
|
342
|
+
```bash
|
|
343
|
+
# Show help
|
|
344
|
+
npx @marcopeg/hal --help
|
|
345
|
+
|
|
346
|
+
# Initialize config file
|
|
347
|
+
npx @marcopeg/hal init
|
|
348
|
+
npx @marcopeg/hal init --cwd ./workspace
|
|
349
|
+
|
|
350
|
+
# Start all bots
|
|
351
|
+
npx @marcopeg/hal
|
|
352
|
+
npx @marcopeg/hal --cwd ./workspace
|
|
353
|
+
```
|
|
354
|
+
|
|
355
|
+
## Bot Commands
|
|
356
|
+
|
|
357
|
+
| Command | Description |
|
|
358
|
+
|----------|----------------------------|
|
|
359
|
+
| `/start` | Welcome message |
|
|
360
|
+
| `/help` | Show help information |
|
|
361
|
+
| `/clear` | Clear conversation history |
|
|
362
|
+
|
|
363
|
+
## Custom Commands
|
|
364
|
+
|
|
365
|
+
You can add your own slash commands as `.mjs` files. When a user sends `/mycommand`, the bot looks for a matching file before passing the message to Claude.
|
|
366
|
+
|
|
367
|
+
### File locations
|
|
368
|
+
|
|
369
|
+
| Location | Scope |
|
|
370
|
+
|----------|-------|
|
|
371
|
+
| `{project.cwd}/.hal/commands/<name>.mjs` | Project-specific |
|
|
372
|
+
| `{configDir}/.hal/commands/<name>.mjs` | Global — available to all projects |
|
|
373
|
+
|
|
374
|
+
Project-specific commands take precedence over global ones on name collision.
|
|
375
|
+
|
|
376
|
+
### Command file format
|
|
377
|
+
|
|
378
|
+
```js
|
|
379
|
+
// .hal/commands/deploy.mjs
|
|
380
|
+
export const description = 'Deploy the project'; // shown in Telegram's / menu
|
|
381
|
+
|
|
382
|
+
export default async function({ args, ctx, projectCtx }) {
|
|
383
|
+
const env = args[0] ?? 'staging';
|
|
384
|
+
return `Deploying to ${env}...`;
|
|
385
|
+
}
|
|
386
|
+
```
|
|
387
|
+
|
|
388
|
+
The only required export is `description` (shown in Telegram's `/` suggestion menu) and a `default` function. The return value is sent to the user as a message. Return `null` or `undefined` to suppress the reply (e.g. if your command sends its own response via `gram`).
|
|
389
|
+
|
|
390
|
+
### Handler arguments
|
|
391
|
+
|
|
392
|
+
#### `args: string[]`
|
|
393
|
+
|
|
394
|
+
Tokens following the command name, split on whitespace.
|
|
395
|
+
|
|
396
|
+
```
|
|
397
|
+
/deploy staging eu-west → args = ['staging', 'eu-west']
|
|
398
|
+
/status → args = []
|
|
399
|
+
```
|
|
400
|
+
|
|
401
|
+
#### `ctx: Record<string, string>`
|
|
402
|
+
|
|
403
|
+
The fully-resolved context that would be sent to the AI for this message — identical to what Claude sees in its `# Context` header. Includes all implicit keys plus any config vars and hook results:
|
|
404
|
+
|
|
405
|
+
| Key group | Description |
|
|
406
|
+
|-----------|-------------|
|
|
407
|
+
| `bot.*` | `bot.userId`, `bot.username`, `bot.firstName`, `bot.chatId`, `bot.messageId`, `bot.timestamp`, `bot.datetime`, `bot.messageType` |
|
|
408
|
+
| `sys.*` | `sys.date`, `sys.time`, `sys.datetime`, `sys.ts`, `sys.tz` |
|
|
409
|
+
| `project.*` | `project.name`, `project.cwd`, `project.slug` |
|
|
410
|
+
| custom | Any keys defined in `context` config blocks, after `${}` / `#{}` / `@{}` substitution and context hook transforms |
|
|
411
|
+
|
|
412
|
+
Use `/context` (the built-in global command) to inspect the exact keys available at runtime.
|
|
413
|
+
|
|
414
|
+
#### `gram: Grammy Context`
|
|
415
|
+
|
|
416
|
+
The raw [Grammy](https://grammy.dev) message context, giving direct access to the Telegram Bot API. Only needed for advanced use cases: sending multiple messages, editing or deleting messages, uploading files, reacting to messages, etc.
|
|
417
|
+
|
|
418
|
+
Common patterns:
|
|
419
|
+
|
|
420
|
+
```js
|
|
421
|
+
// Send a temporary status message, then delete it
|
|
422
|
+
const status = await gram.reply('Working...');
|
|
423
|
+
// ... do work ...
|
|
424
|
+
await gram.api.deleteMessage(gram.chat.id, status.message_id);
|
|
425
|
+
|
|
426
|
+
// Edit the status message while working
|
|
427
|
+
await gram.api.editMessageText(gram.chat.id, status.message_id, 'Still working...');
|
|
428
|
+
|
|
429
|
+
// React to the original message
|
|
430
|
+
await gram.react([{ type: 'emoji', emoji: '👍' }]);
|
|
431
|
+
|
|
432
|
+
// Send a file
|
|
433
|
+
await gram.replyWithDocument(new InputFile('/path/to/file.pdf'));
|
|
434
|
+
```
|
|
435
|
+
|
|
436
|
+
When using `gram` to send your own reply, return `null` or `undefined` to suppress the default text reply:
|
|
437
|
+
|
|
438
|
+
```js
|
|
439
|
+
export default async function({ gram }) {
|
|
440
|
+
await gram.reply('Done!');
|
|
441
|
+
return null;
|
|
442
|
+
}
|
|
443
|
+
```
|
|
444
|
+
|
|
445
|
+
#### `agent: Agent`
|
|
446
|
+
|
|
447
|
+
An engine-agnostic interface for making one-shot AI calls from within a command. The underlying provider is configured per-project — currently Claude Code, with support for other engines planned. Command handlers always use this interface and never talk to any engine directly.
|
|
448
|
+
|
|
449
|
+
```ts
|
|
450
|
+
interface Agent {
|
|
451
|
+
call(
|
|
452
|
+
prompt: string,
|
|
453
|
+
options?: { onProgress?: (message: string) => void }
|
|
454
|
+
): Promise<string>;
|
|
455
|
+
}
|
|
456
|
+
```
|
|
457
|
+
|
|
458
|
+
Unlike regular user messages, agent calls have no session history and no context header prepended — the prompt is sent to the engine as-is.
|
|
459
|
+
|
|
460
|
+
| Option | Type | Description |
|
|
461
|
+
|--------|------|-------------|
|
|
462
|
+
| `onProgress` | `(message: string) => void` | Called during execution with activity updates (e.g. `"Reading: /path/to/file"`). Use it to keep the user informed while the agent is working. |
|
|
463
|
+
|
|
464
|
+
Returns the agent's final text output as a string. Throws on failure — the bot's command error handler will catch it and reply with `Command failed: <message>`.
|
|
465
|
+
|
|
466
|
+
```js
|
|
467
|
+
export default async function({ args, gram, agent }) {
|
|
468
|
+
const status = await gram.reply('Thinking...');
|
|
469
|
+
|
|
470
|
+
const answer = await agent.call(`Summarise: ${args.join(' ')}`, {
|
|
471
|
+
onProgress: async (activity) => {
|
|
472
|
+
try {
|
|
473
|
+
await gram.api.editMessageText(gram.chat.id, status.message_id, `⏳ ${activity}`);
|
|
474
|
+
} catch { /* ignore if message was already edited */ }
|
|
475
|
+
},
|
|
476
|
+
});
|
|
477
|
+
|
|
478
|
+
await gram.api.deleteMessage(gram.chat.id, status.message_id);
|
|
479
|
+
return answer;
|
|
480
|
+
}
|
|
481
|
+
```
|
|
482
|
+
|
|
483
|
+
See [`examples/.hal/commands/joke.mjs`](examples/.hal/commands/joke.mjs) for a full example that combines `gram` for live status cycling with `agent.call` + `onProgress` for activity updates.
|
|
484
|
+
|
|
485
|
+
#### `projectCtx: ProjectContext`
|
|
486
|
+
|
|
487
|
+
The project-level context object. Useful fields:
|
|
488
|
+
|
|
489
|
+
| Field | Type | Description |
|
|
490
|
+
|-------|------|-------------|
|
|
491
|
+
| `projectCtx.config.name` | `string \| undefined` | Project name from config |
|
|
492
|
+
| `projectCtx.config.slug` | `string` | Internal slug (used for log/data paths) |
|
|
493
|
+
| `projectCtx.config.cwd` | `string` | Absolute path to the project directory |
|
|
494
|
+
| `projectCtx.config.configDir` | `string` | Absolute path to the directory containing `hal.config.json` |
|
|
495
|
+
| `projectCtx.config.dataDir` | `string` | Absolute path to user data storage root |
|
|
496
|
+
| `projectCtx.config.context` | `Record<string, string> \| undefined` | Raw config-level context values (pre-hook) |
|
|
497
|
+
| `projectCtx.logger` | Pino logger | Structured logger — use for debug output that ends up in log files |
|
|
498
|
+
|
|
499
|
+
### Examples
|
|
500
|
+
|
|
501
|
+
- [`examples/obsidian/.hal/commands/status.mjs`](examples/obsidian/.hal/commands/status.mjs) — project-specific command using `projectCtx.config`
|
|
502
|
+
- [`examples/.hal/commands/context.mjs`](examples/.hal/commands/context.mjs) — global command that dumps the full resolved context
|
|
503
|
+
- [`examples/.hal/commands/joke.mjs`](examples/.hal/commands/joke.mjs) — global command using `agent.call` with live status cycling and `onProgress` updates
|
|
504
|
+
|
|
505
|
+
### Skills
|
|
506
|
+
|
|
507
|
+
[Claude Code skills](https://docs.anthropic.com/en/docs/claude-code/skills) live in `.claude/skills/` inside the project directory. Each skill is a folder containing a `SKILL.md` file with a YAML frontmatter block and a prompt body:
|
|
508
|
+
|
|
509
|
+
```
|
|
510
|
+
<project-cwd>/
|
|
511
|
+
└── .claude/
|
|
512
|
+
└── skills/
|
|
513
|
+
└── chuck/
|
|
514
|
+
└── SKILL.md
|
|
515
|
+
```
|
|
516
|
+
|
|
517
|
+
```markdown
|
|
518
|
+
---
|
|
519
|
+
name: chuck
|
|
520
|
+
description: Tells a joke about Chuck Norris.
|
|
521
|
+
---
|
|
522
|
+
|
|
523
|
+
Tell a short, funny joke about Chuck Norris.
|
|
524
|
+
```
|
|
525
|
+
|
|
526
|
+
At boot time (and whenever `SKILL.md` files change) the bot reads every skill folder, parses the frontmatter, and registers the skills as Telegram slash commands via `setMyCommands`. The **folder name** is used as the command name — if the frontmatter `name` field differs from the folder name the bot logs a warning and uses the folder name.
|
|
527
|
+
|
|
528
|
+
When a user invokes a skill command (e.g. `/chuck`) the bot:
|
|
529
|
+
1. Reads the `SKILL.md` prompt body
|
|
530
|
+
2. Appends any user arguments as `User input: <args>` if present
|
|
531
|
+
3. Calls the AI engine with that prompt via the engine-agnostic `agent.call()` interface
|
|
532
|
+
4. Sends the response back to the user
|
|
533
|
+
|
|
534
|
+
Skills can be **overridden per-project**: create a `.hal/commands/<name>.mjs` file with the same name as the skill and the `.mjs` handler takes full precedence.
|
|
535
|
+
|
|
536
|
+
**Command precedence** (highest wins):
|
|
537
|
+
|
|
538
|
+
```
|
|
539
|
+
project .hal/commands/<name>.mjs > global .hal/commands/<name>.mjs > .claude/skills/<name>/
|
|
540
|
+
```
|
|
541
|
+
|
|
542
|
+
See [`examples/obsidian/.claude/skills/chuck/`](examples/obsidian/.claude/skills/chuck/SKILL.md) and [`examples/obsidian/.claude/skills/weather/`](examples/obsidian/.claude/skills/weather/SKILL.md) for example skills.
|
|
543
|
+
|
|
544
|
+
|
|
545
|
+
### Hot-reload
|
|
546
|
+
|
|
547
|
+
Commands and skills are **hot-reloaded** — drop a new `.mjs` file or `SKILL.md` into the relevant directory and the bot registers it with Telegram automatically, with no restart. This means Claude can write new command or skill files as part of a task and users see them in the `/` menu immediately.
|
|
548
|
+
|
|
549
|
+
## Creating a Telegram Bot
|
|
550
|
+
|
|
551
|
+
1. Message [@BotFather](https://t.me/BotFather) on Telegram
|
|
552
|
+
2. Send `/newbot`
|
|
553
|
+
3. Choose a display name (e.g. "My Backend Assistant")
|
|
554
|
+
4. Choose a username ending in `bot` (e.g. `my_backend_assistant_bot`)
|
|
555
|
+
5. Add the token to `.env.local` and reference it via `${VAR_NAME}` in `hal.config.json`
|
|
556
|
+
|
|
557
|
+
For each project you need a separate bot and token.
|
|
558
|
+
|
|
559
|
+
## Finding Your Telegram User ID
|
|
560
|
+
|
|
561
|
+
1. Message [@userinfobot](https://t.me/userinfobot) on Telegram
|
|
562
|
+
2. It will reply with your numeric user ID
|
|
563
|
+
3. Add it to `allowedUserIds`
|
|
564
|
+
|
|
565
|
+
## Voice Messages
|
|
566
|
+
|
|
567
|
+
Voice messages are transcribed locally using [Whisper](https://github.com/openai/whisper) via the `nodejs-whisper` package. No audio is sent to external services.
|
|
568
|
+
|
|
569
|
+
### Setup
|
|
570
|
+
|
|
571
|
+
1. **ffmpeg** — for audio conversion
|
|
572
|
+
```bash
|
|
573
|
+
brew install ffmpeg # macOS
|
|
574
|
+
sudo apt install ffmpeg # Ubuntu/Debian
|
|
575
|
+
```
|
|
576
|
+
|
|
577
|
+
2. **CMake** — for building the Whisper executable
|
|
578
|
+
```bash
|
|
579
|
+
brew install cmake # macOS
|
|
580
|
+
sudo apt install cmake # Ubuntu/Debian
|
|
581
|
+
```
|
|
582
|
+
|
|
583
|
+
3. **Download and build Whisper** — run once after installation:
|
|
584
|
+
```bash
|
|
585
|
+
npx nodejs-whisper download
|
|
586
|
+
```
|
|
587
|
+
|
|
588
|
+
### Whisper Models
|
|
589
|
+
|
|
590
|
+
| Model | Size | Speed | Quality |
|
|
591
|
+
|-------|------|-------|---------|
|
|
592
|
+
| `tiny` | ~75 MB | Fastest | Basic |
|
|
593
|
+
| `tiny.en` | ~75 MB | Fastest | English-only |
|
|
594
|
+
| `base` | ~142 MB | Fast | Good |
|
|
595
|
+
| `base.en` | ~142 MB | Fast | English-only (default) |
|
|
596
|
+
| `small` | ~466 MB | Medium | Good multilingual |
|
|
597
|
+
| `medium` | ~1.5 GB | Slower | Very good multilingual |
|
|
598
|
+
| `large-v3-turbo` | ~1.5 GB | Fast | Near-large quality |
|
|
599
|
+
|
|
600
|
+
## Sending Files to Users
|
|
601
|
+
|
|
602
|
+
Claude can send files back through Telegram. Each user has a `downloads/` folder under their data directory. Claude is informed of this path in every prompt.
|
|
603
|
+
|
|
604
|
+
1. Claude writes a file to the downloads folder
|
|
605
|
+
2. The bot detects it after Claude's response completes
|
|
606
|
+
3. The file is sent via Telegram (as a document)
|
|
607
|
+
4. The file is deleted from the server after delivery
|
|
608
|
+
|
|
609
|
+
## Migration from v1 (Single-Project Config)
|
|
610
|
+
|
|
611
|
+
The old single-project config format is no longer supported. Migrate by wrapping your config:
|
|
612
|
+
|
|
613
|
+
**Before:**
|
|
614
|
+
```json
|
|
615
|
+
{
|
|
616
|
+
"telegram": { "botToken": "..." },
|
|
617
|
+
"access": { "allowedUserIds": [123] },
|
|
618
|
+
"claude": { "command": "claude" },
|
|
619
|
+
"logging": { "level": "info" }
|
|
620
|
+
}
|
|
621
|
+
```
|
|
622
|
+
|
|
623
|
+
**After:**
|
|
624
|
+
```json
|
|
625
|
+
{
|
|
626
|
+
"globals": {
|
|
627
|
+
"claude": { "command": "claude" },
|
|
628
|
+
"logging": { "level": "info" }
|
|
629
|
+
},
|
|
630
|
+
"projects": [
|
|
631
|
+
{
|
|
632
|
+
"cwd": ".",
|
|
633
|
+
"telegram": { "botToken": "..." },
|
|
634
|
+
"access": { "allowedUserIds": [123] }
|
|
635
|
+
}
|
|
636
|
+
]
|
|
637
|
+
}
|
|
638
|
+
```
|
|
639
|
+
|
|
640
|
+
> **Note:** Named environment variable overrides from v1 (`TELEGRAM_BOT_TOKEN`, `ALLOWED_USER_IDS`, etc.) are no longer supported. Use `${VAR_NAME}` substitution in `hal.config.json` instead — see [Environment variable substitution](#environment-variable-substitution).
|
|
641
|
+
|
|
642
|
+
## Security Notice
|
|
643
|
+
|
|
644
|
+
**Important**: Conversations with this bot are not end-to-end encrypted. Messages pass through Telegram's servers. Do not share:
|
|
645
|
+
|
|
646
|
+
- Passwords or API keys
|
|
647
|
+
- Personal identification numbers
|
|
648
|
+
- Financial information
|
|
649
|
+
- Confidential business data
|
|
650
|
+
|
|
651
|
+
This bot is intended for development assistance only. Treat all conversations as potentially visible to third parties.
|
|
652
|
+
|
|
653
|
+
## License
|
|
654
|
+
|
|
655
|
+
ISC
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { Agent, ProjectContext } from "../types.js";
|
|
2
|
+
/**
|
|
3
|
+
* Return the engine-specific skills directory for the given project.
|
|
4
|
+
* Today Claude Code stores skills in `.claude/skills/`.
|
|
5
|
+
* When new engines are supported this function will branch on the engine type.
|
|
6
|
+
*/
|
|
7
|
+
export declare function getSkillsDir(projectCwd: string): string;
|
|
8
|
+
/**
|
|
9
|
+
* Create an Agent for the given project context.
|
|
10
|
+
*
|
|
11
|
+
* This factory is the single place where the underlying AI engine is selected.
|
|
12
|
+
* Today it always returns a Claude Code-backed agent. When support for other
|
|
13
|
+
* providers (Codex, Copilot, …) is added, engine selection will happen here
|
|
14
|
+
* based on project config — command handlers never need to change.
|
|
15
|
+
*/
|
|
16
|
+
export declare function createAgent(projectCtx: ProjectContext): Agent;
|
|
17
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/agent/index.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,KAAK,EAAE,cAAc,EAAE,MAAM,aAAa,CAAC;AAEzD;;;;GAIG;AACH,wBAAgB,YAAY,CAAC,UAAU,EAAE,MAAM,GAAG,MAAM,CAEvD;AAED;;;;;;;GAOG;AACH,wBAAgB,WAAW,CAAC,UAAU,EAAE,cAAc,GAAG,KAAK,CAa7D"}
|