@marcopeg/hal 1.0.18 → 1.0.22
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 +90 -743
- package/dist/agent/index.d.ts +3 -3
- package/dist/agent/index.d.ts.map +1 -1
- package/dist/agent/index.js +5 -5
- package/dist/agent/index.js.map +1 -1
- package/dist/bot/commands/git/callback.d.ts +8 -0
- package/dist/bot/commands/git/callback.d.ts.map +1 -0
- package/dist/bot/commands/git/callback.js +72 -0
- package/dist/bot/commands/git/callback.js.map +1 -0
- package/dist/bot/commands/git/clean.d.ts +4 -0
- package/dist/bot/commands/git/clean.d.ts.map +1 -0
- package/dist/bot/commands/git/clean.js +50 -0
- package/dist/bot/commands/git/clean.js.map +1 -0
- package/dist/bot/commands/git/commit.d.ts +4 -0
- package/dist/bot/commands/git/commit.d.ts.map +1 -0
- package/dist/bot/commands/git/commit.js +72 -0
- package/dist/bot/commands/git/commit.js.map +1 -0
- package/dist/bot/commands/git/exec.d.ts +6 -0
- package/dist/bot/commands/git/exec.d.ts.map +1 -0
- package/dist/bot/commands/git/exec.js +16 -0
- package/dist/bot/commands/git/exec.js.map +1 -0
- package/dist/bot/commands/git/index.d.ts +6 -0
- package/dist/bot/commands/git/index.d.ts.map +1 -0
- package/dist/bot/commands/git/index.js +6 -0
- package/dist/bot/commands/git/index.js.map +1 -0
- package/dist/bot/commands/git/init.d.ts +4 -0
- package/dist/bot/commands/git/init.d.ts.map +1 -0
- package/dist/bot/commands/git/init.js +22 -0
- package/dist/bot/commands/git/init.js.map +1 -0
- package/dist/bot/commands/git/status.d.ts +4 -0
- package/dist/bot/commands/git/status.d.ts.map +1 -0
- package/dist/bot/commands/git/status.js +18 -0
- package/dist/bot/commands/git/status.js.map +1 -0
- package/dist/bot/commands/help.js +1 -1
- package/dist/bot/commands/help.js.map +1 -1
- package/dist/bot/commands/loader.d.ts +14 -6
- package/dist/bot/commands/loader.d.ts.map +1 -1
- package/dist/bot/commands/loader.js +126 -34
- package/dist/bot/commands/loader.js.map +1 -1
- package/dist/bot/commands/message.d.ts.map +1 -1
- package/dist/bot/commands/message.js +51 -25
- package/dist/bot/commands/message.js.map +1 -1
- package/dist/bot/commands/model-callback.d.ts +4 -0
- package/dist/bot/commands/model-callback.d.ts.map +1 -0
- package/dist/bot/commands/model-callback.js +34 -0
- package/dist/bot/commands/model-callback.js.map +1 -0
- package/dist/bot/commands/model.d.ts +4 -0
- package/dist/bot/commands/model.d.ts.map +1 -0
- package/dist/bot/commands/model.js +83 -0
- package/dist/bot/commands/model.js.map +1 -0
- package/dist/bot/commands/reset.d.ts +3 -2
- package/dist/bot/commands/reset.d.ts.map +1 -1
- package/dist/bot/commands/reset.js +73 -10
- package/dist/bot/commands/reset.js.map +1 -1
- package/dist/bot/commands/resetPrompt.d.ts +20 -0
- package/dist/bot/commands/resetPrompt.d.ts.map +1 -0
- package/dist/bot/commands/resetPrompt.js +58 -0
- package/dist/bot/commands/resetPrompt.js.map +1 -0
- package/dist/bot/commands/session.d.ts +2 -2
- package/dist/bot/commands/session.d.ts.map +1 -1
- package/dist/bot/commands/session.js +7 -4
- package/dist/bot/commands/session.js.map +1 -1
- package/dist/bot/commands/start.js +2 -2
- package/dist/bot/commands/start.js.map +1 -1
- package/dist/bot/commands/watcher.d.ts +2 -1
- package/dist/bot/commands/watcher.d.ts.map +1 -1
- package/dist/bot/commands/watcher.js +11 -5
- package/dist/bot/commands/watcher.js.map +1 -1
- package/dist/bot/handlers/text.d.ts.map +1 -1
- package/dist/bot/handlers/text.js +11 -6
- package/dist/bot/handlers/text.js.map +1 -1
- package/dist/bot/middleware/auth.d.ts +0 -3
- package/dist/bot/middleware/auth.d.ts.map +1 -1
- package/dist/bot/middleware/auth.js +13 -12
- package/dist/bot/middleware/auth.js.map +1 -1
- package/dist/bot.d.ts.map +1 -1
- package/dist/bot.js +43 -11
- package/dist/bot.js.map +1 -1
- package/dist/cli.js +52 -29
- package/dist/cli.js.map +1 -1
- package/dist/config-watcher.d.ts +10 -0
- package/dist/config-watcher.d.ts.map +1 -0
- package/dist/config-watcher.js +55 -0
- package/dist/config-watcher.js.map +1 -0
- package/dist/config-writer.d.ts +8 -0
- package/dist/config-writer.d.ts.map +1 -0
- package/dist/config-writer.js +57 -0
- package/dist/config-writer.js.map +1 -0
- package/dist/config.d.ts +338 -53
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +337 -92
- package/dist/config.js.map +1 -1
- package/dist/context/resolver.d.ts +4 -0
- package/dist/context/resolver.d.ts.map +1 -1
- package/dist/context/resolver.js +8 -2
- package/dist/context/resolver.js.map +1 -1
- package/dist/default-models.d.ts +3 -0
- package/dist/default-models.d.ts.map +1 -0
- package/dist/default-models.js +16 -0
- package/dist/default-models.js.map +1 -0
- package/dist/engine/adapters/antigravity.d.ts +13 -0
- package/dist/engine/adapters/antigravity.d.ts.map +1 -0
- package/dist/engine/adapters/antigravity.js +230 -0
- package/dist/engine/adapters/antigravity.js.map +1 -0
- package/dist/engine/adapters/claude.js +2 -2
- package/dist/engine/adapters/claude.js.map +1 -1
- package/dist/engine/adapters/codex.d.ts.map +1 -1
- package/dist/engine/adapters/codex.js +27 -8
- package/dist/engine/adapters/codex.js.map +1 -1
- package/dist/engine/adapters/copilot.d.ts.map +1 -1
- package/dist/engine/adapters/copilot.js +6 -3
- package/dist/engine/adapters/copilot.js.map +1 -1
- package/dist/engine/adapters/cursor.d.ts +3 -0
- package/dist/engine/adapters/cursor.d.ts.map +1 -0
- package/dist/engine/adapters/cursor.js +106 -0
- package/dist/engine/adapters/cursor.js.map +1 -0
- package/dist/engine/adapters/opencode.d.ts +2 -2
- package/dist/engine/adapters/opencode.d.ts.map +1 -1
- package/dist/engine/adapters/opencode.js +33 -13
- package/dist/engine/adapters/opencode.js.map +1 -1
- package/dist/engine/detect.d.ts +26 -0
- package/dist/engine/detect.d.ts.map +1 -0
- package/dist/engine/detect.js +129 -0
- package/dist/engine/detect.js.map +1 -0
- package/dist/engine/model-cache.d.ts +25 -0
- package/dist/engine/model-cache.d.ts.map +1 -0
- package/dist/engine/model-cache.js +162 -0
- package/dist/engine/model-cache.js.map +1 -0
- package/dist/engine/model-validation.d.ts +9 -0
- package/dist/engine/model-validation.d.ts.map +1 -0
- package/dist/engine/model-validation.js +21 -0
- package/dist/engine/model-validation.js.map +1 -0
- package/dist/engine/models-data.generated.d.ts +4 -0
- package/dist/engine/models-data.generated.d.ts.map +1 -0
- package/dist/engine/models-data.generated.js +196 -0
- package/dist/engine/models-data.generated.js.map +1 -0
- package/dist/engine/models-fetch.d.ts +5 -0
- package/dist/engine/models-fetch.d.ts.map +1 -0
- package/dist/engine/models-fetch.js +54 -0
- package/dist/engine/models-fetch.js.map +1 -0
- package/dist/engine/probe-utils.d.ts +6 -0
- package/dist/engine/probe-utils.d.ts.map +1 -0
- package/dist/engine/probe-utils.js +42 -0
- package/dist/engine/probe-utils.js.map +1 -0
- package/dist/engine/prompt.d.ts.map +1 -1
- package/dist/engine/prompt.js +8 -0
- package/dist/engine/prompt.js.map +1 -1
- package/dist/engine/registry.d.ts.map +1 -1
- package/dist/engine/registry.js +4 -0
- package/dist/engine/registry.js.map +1 -1
- package/dist/engine/types.d.ts +3 -3
- package/dist/engine/types.d.ts.map +1 -1
- package/dist/engine/types.js +2 -0
- package/dist/engine/types.js.map +1 -1
- package/dist/prompts.d.ts +19 -0
- package/dist/prompts.d.ts.map +1 -0
- package/dist/prompts.js +73 -0
- package/dist/prompts.js.map +1 -0
- package/dist/setup-wizard.d.ts +4 -0
- package/dist/setup-wizard.d.ts.map +1 -0
- package/dist/setup-wizard.js +258 -0
- package/dist/setup-wizard.js.map +1 -0
- package/package.json +6 -2
package/README.md
CHANGED
|
@@ -8,54 +8,63 @@ A Telegram bot that provides access to AI coding agents as a personal assistant.
|
|
|
8
8
|
|
|
9
9
|
## Features
|
|
10
10
|
|
|
11
|
-
- **Multi-engine support** — use Claude Code, GitHub Copilot, Codex, or OpenCode per project
|
|
12
|
-
- **Multi-project support** — run multiple bots from a single config, each connected to a different directory
|
|
13
11
|
- Chat with your AI coding agent via Telegram
|
|
14
12
|
- Send images and documents for analysis
|
|
15
13
|
- **Voice message support** with local Whisper transcription
|
|
16
14
|
- **File sending** — the engine can send files back to you
|
|
17
|
-
- **
|
|
15
|
+
- **Multi-engine support** — use Claude Code, GitHub Copilot, Codex, OpenCode, or Antigravity per project
|
|
16
|
+
- **Multi-project support** — run multiple bots from a single config, each connected to a different independent directory
|
|
17
|
+
- **Context injection** — every message includes system metadata (timestamps, user info, custom values) and supports custom injections via config and per-project hooks (`.mjs`) with hot-reload
|
|
18
18
|
- **Custom slash commands** — add `.mjs` command files per-project or globally; hot-reloaded so the engine can create new commands at runtime
|
|
19
|
-
- **Skills** — `.
|
|
20
|
-
- Persistent conversation sessions per user
|
|
19
|
+
- **Skills** — `.agents/skills/` entries are automatically exposed as Telegram slash commands; no extra setup needed
|
|
20
|
+
- Persistent conversation sessions per user (availability based on engine)
|
|
21
21
|
- Per-project access control, rate limiting, and logging
|
|
22
|
-
- Log persistence to file with daily rotation support
|
|
23
22
|
|
|
24
23
|
## How It Works
|
|
25
24
|
|
|
26
|
-
This tool runs one AI coding agent subprocess per project, each in its configured working directory.
|
|
25
|
+
This tool runs one AI coding agent subprocess per project, each in its configured working directory. You can choose your [favourite engine](./docs/engines/README.md) globally, or each project can use a different engine.
|
|
27
26
|
|
|
28
27
|
The engine reads its standard config files from the project directory:
|
|
29
28
|
|
|
30
|
-
- `
|
|
29
|
+
- `AGENTS.md` — Project-specific instructions and context (filename may depend on engine)
|
|
30
|
+
- `.agents/skills/` — Custom skills and slash commands (pattern may depend on engine)
|
|
31
31
|
- `.claude/settings.json` — Permissions and tool settings (Claude Code)
|
|
32
|
-
- `.claude/commands/` — Custom slash commands
|
|
33
32
|
- `.mcp.json` — MCP server configurations
|
|
34
33
|
|
|
35
34
|
You get the full power of your chosen AI coding agent — file access, code execution, configured MCP tools — all accessible through Telegram.
|
|
36
35
|
|
|
37
|
-
|
|
36
|
+
## Prerequisites
|
|
38
37
|
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
| **Codex** | `codex` | Full support | `AGENTS.md` |
|
|
44
|
-
| **OpenCode** | `opencode` | Stub (basic prompt/response) | `AGENTS.md` |
|
|
38
|
+
- Node.js 18+
|
|
39
|
+
- At least one supported AI coding CLI installed and authenticated - see [engines](docs/engines/README.md)
|
|
40
|
+
- A Telegram bot token per project (from [@BotFather](https://t.me/BotFather)) — see [Telegram](docs/telegram/README.md#creating-a-telegram-bot)
|
|
41
|
+
- **ffmpeg** (required for voice messages) — `brew install ffmpeg` on macOS
|
|
45
42
|
|
|
46
|
-
|
|
43
|
+
## Supported Engines 🤖
|
|
47
44
|
|
|
48
|
-
|
|
45
|
+
This is the list of the currently supported engines (Claude Code, Copilot, Codex, OpenCode, Cursor, Antigravity):
|
|
49
46
|
|
|
50
|
-
|
|
47
|
+
- [OpenCode](docs/engines/opencode/README.md)
|
|
48
|
+
- [Codex](docs/engines/codex/README.md)
|
|
49
|
+
- [Claude Code](docs/engines/claude/README.md)
|
|
50
|
+
- [Copilot](docs/engines/copilot/README.md)
|
|
51
|
+
- [Cursor](docs/engines/cursor/README.md)
|
|
52
|
+
- [Antigravity](docs/engines/antigravity/README.md)
|
|
51
53
|
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
54
|
+
Each engine has pros/cons and some limitations.
|
|
55
|
+
Here we try to keep updated a feature comparison table:
|
|
56
|
+
|
|
57
|
+
| Feature | [OpenCode](docs/engines/opencode/README.md) | [Codex](docs/engines/codex/README.md) | [Claude Code](docs/engines/claude/README.md) | [Copilot](docs/engines/copilot/README.md) | [Cursor](docs/engines/cursor/README.md) | [Antigravity](docs/engines/antigravity/README.md) |
|
|
58
|
+
|--------|:--------:|:-----:|:------:|:-------:|:------:|:------------:|
|
|
59
|
+
| **Instruction file** | `AGENTS.md` | `AGENTS.md` | `CLAUDE.md` | `AGENTS.md` | `AGENTS.md` | `GEMINI.md` |
|
|
60
|
+
| **Main skills folder** | `.agents/skills/` | `.agents/skills/` | `.claude/skills/` | `.agents/skills/` | `.agents/skills/` | `.agent/skills/` |
|
|
61
|
+
| **Per-user session** | ✗ | ✗ | ✓ | ✗ | ✗ | ✓ |
|
|
62
|
+
| **Network access** | — | ✓ | — | — | — | — |
|
|
63
|
+
| **Full disk access** | — | ✓ | — | — | — | — |
|
|
64
|
+
| **YOLO mode** | — | ✓ | — | — | — | ✓ |
|
|
65
|
+
| **Streaming progress** | ✗ | ✗ | ✓ | ✗ | ✗ | ✓ |
|
|
66
|
+
|
|
67
|
+
Read more in the [engine docs](docs/engines/README.md).
|
|
59
68
|
|
|
60
69
|
## Quick Start
|
|
61
70
|
|
|
@@ -63,29 +72,40 @@ See [Claude Code documentation](https://docs.anthropic.com/en/docs/claude-code),
|
|
|
63
72
|
# Create hal.config.json in the current directory
|
|
64
73
|
npx @marcopeg/hal init
|
|
65
74
|
|
|
66
|
-
#
|
|
75
|
+
# Or in a specific folder (config and bots will use that directory)
|
|
76
|
+
npx @marcopeg/hal init --cwd ./workspace
|
|
77
|
+
|
|
78
|
+
# Optional: pick engine at init
|
|
67
79
|
npx @marcopeg/hal init --engine copilot
|
|
80
|
+
npx @marcopeg/hal init --cwd ./workspace --engine copilot
|
|
68
81
|
|
|
69
|
-
# Edit hal.config.json: add your bot token and project path
|
|
70
|
-
# then start all bots
|
|
82
|
+
# Edit hal.config.json: add your bot token and project path, then start
|
|
71
83
|
npx @marcopeg/hal
|
|
84
|
+
npx @marcopeg/hal --cwd ./workspace
|
|
72
85
|
```
|
|
73
86
|
|
|
74
|
-
##
|
|
87
|
+
## Telegram
|
|
75
88
|
|
|
76
|
-
|
|
77
|
-
# Initialize config in a specific directory
|
|
78
|
-
npx @marcopeg/hal init --cwd ./workspace
|
|
89
|
+
Before running HAL you need a Telegram bot token and your own Telegram user ID. Both are required to set up your first project.
|
|
79
90
|
|
|
80
|
-
#
|
|
81
|
-
|
|
82
|
-
```
|
|
91
|
+
- **[Register a bot](docs/telegram/README.md#creating-a-telegram-bot)** — Get a bot token from BotFather and add it to your config.
|
|
92
|
+
- **[Find your user ID](docs/telegram/README.md#finding-your-telegram-user-id)** — Get your numeric user ID and add it to `allowedUserIds`.
|
|
83
93
|
|
|
84
94
|
## Configuration
|
|
85
95
|
|
|
86
|
-
|
|
96
|
+
HAL is configured via a config file in the directory where you run the CLI. Three formats are supported — JSON, [JSONC](https://code.visualstudio.com/docs/languages/json#_json-with-comments) (JSON with comments and trailing commas), and YAML. Only one format per file is allowed. Full reference:
|
|
97
|
+
|
|
98
|
+
- **[Configuration](docs/config/README.md)** — config files, env vars, `globals`, `projects[]`, dataDir, log files, directory structure
|
|
99
|
+
- **[Context](docs/config/context/README.md)** — context injection (implicit keys, custom context, hooks)
|
|
100
|
+
- **[Commands](docs/config/commands/README.md)** — built-in command config (`/start`, `/help`, `/reset`, `/clean`, `/model`, `/git`)
|
|
101
|
+
- **[Engines](docs/engines/README.md)** — supported engines, engine config, model list, model defaults, per-engine setup
|
|
102
|
+
- **[Logging](docs/config/logging/README.md)** — log level, flow, persist, log file paths
|
|
103
|
+
- **[Rate limit](docs/config/rate-limit/README.md)** — max messages per user per time window
|
|
104
|
+
|
|
105
|
+
<details>
|
|
106
|
+
<summary>Minimal config example (JSON)</summary>
|
|
87
107
|
|
|
88
|
-
Create a `hal.config.json` in your workspace
|
|
108
|
+
Create a `hal.config.json` in your workspace. Use `${VAR_NAME}` for secrets and set them in `.env.local`.
|
|
89
109
|
|
|
90
110
|
```json
|
|
91
111
|
{
|
|
@@ -93,474 +113,56 @@ Create a `hal.config.json` in your workspace directory (where you run the CLI fr
|
|
|
93
113
|
"engine": { "name": "claude" },
|
|
94
114
|
"logging": { "level": "info", "flow": true, "persist": false },
|
|
95
115
|
"rateLimit": { "max": 10, "windowMs": 60000 },
|
|
96
|
-
"access": { "allowedUserIds": [] }
|
|
116
|
+
"access": { "allowedUserIds": [123456789] }
|
|
97
117
|
},
|
|
98
118
|
"projects": [
|
|
99
119
|
{
|
|
100
120
|
"name": "backend",
|
|
101
121
|
"cwd": "./backend",
|
|
102
122
|
"telegram": { "botToken": "${BACKEND_BOT_TOKEN}" },
|
|
103
|
-
"access": { "allowedUserIds": [123456789] },
|
|
104
123
|
"logging": { "persist": true }
|
|
105
124
|
},
|
|
106
125
|
{
|
|
107
126
|
"name": "frontend",
|
|
108
127
|
"cwd": "./frontend",
|
|
109
128
|
"engine": { "name": "copilot", "model": "gpt-5-mini" },
|
|
110
|
-
"telegram": { "botToken": "${FRONTEND_BOT_TOKEN}" }
|
|
111
|
-
"access": { "allowedUserIds": [123456789] }
|
|
112
|
-
}
|
|
113
|
-
]
|
|
114
|
-
}
|
|
115
|
-
```
|
|
116
|
-
|
|
117
|
-
### hal.config.local.json
|
|
118
|
-
|
|
119
|
-
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.
|
|
120
|
-
|
|
121
|
-
Every field is optional. Project entries are matched to base projects by `name` (preferred) or `cwd` — they cannot introduce new projects.
|
|
122
|
-
|
|
123
|
-
```json
|
|
124
|
-
{
|
|
125
|
-
"projects": [
|
|
126
|
-
{
|
|
127
|
-
"name": "backend",
|
|
128
|
-
"telegram": { "botToken": "7123456789:AAHActual-token-here" },
|
|
129
|
-
"logging": { "persist": true }
|
|
130
|
-
}
|
|
131
|
-
]
|
|
132
|
-
}
|
|
133
|
-
```
|
|
134
|
-
|
|
135
|
-
### Environment variable substitution
|
|
136
|
-
|
|
137
|
-
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):
|
|
138
|
-
|
|
139
|
-
1. `{config-dir}/.env.local` _(gitignored)_
|
|
140
|
-
2. `{config-dir}/.env`
|
|
141
|
-
3. `{project-cwd}/.env.local` _(gitignored)_
|
|
142
|
-
4. `{project-cwd}/.env`
|
|
143
|
-
5. Shell environment (`process.env`)
|
|
144
|
-
|
|
145
|
-
```bash
|
|
146
|
-
# .env (safe to commit — no real secrets)
|
|
147
|
-
BACKEND_BOT_TOKEN=
|
|
148
|
-
FRONTEND_BOT_TOKEN=
|
|
149
|
-
|
|
150
|
-
# .env.local (gitignored — real secrets go here)
|
|
151
|
-
BACKEND_BOT_TOKEN=7123456789:AAHActual-token-here
|
|
152
|
-
FRONTEND_BOT_TOKEN=7987654321:AAHAnother-token-here
|
|
153
|
-
```
|
|
154
|
-
|
|
155
|
-
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.
|
|
156
|
-
|
|
157
|
-
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.
|
|
158
|
-
|
|
159
|
-
### Context Injection
|
|
160
|
-
|
|
161
|
-
Every message sent to the engine is automatically enriched with a structured context header. This provides metadata (message info, timestamps, custom values) so the AI can reason about the current request without extra tool calls.
|
|
162
|
-
|
|
163
|
-
#### Implicit context (always-on)
|
|
164
|
-
|
|
165
|
-
These keys are injected for every message, even without any `context` configuration:
|
|
166
|
-
|
|
167
|
-
| Key | Description |
|
|
168
|
-
|-----|-------------|
|
|
169
|
-
| `bot.messageId` | Telegram message ID |
|
|
170
|
-
| `bot.timestamp` | Message Unix timestamp (seconds) |
|
|
171
|
-
| `bot.datetime` | Message datetime, ISO 8601 |
|
|
172
|
-
| `bot.userId` | Sender's Telegram user ID |
|
|
173
|
-
| `bot.username` | Sender's @username (if set) |
|
|
174
|
-
| `bot.firstName` | Sender's first name |
|
|
175
|
-
| `bot.chatId` | Chat ID |
|
|
176
|
-
| `bot.messageType` | `text` / `photo` / `document` / `voice` |
|
|
177
|
-
| `project.name` | Project name (falls back to internal slug if not set) |
|
|
178
|
-
| `project.cwd` | Resolved absolute project working directory |
|
|
179
|
-
| `project.slug` | Project slug (full path with `/` → `-`) |
|
|
180
|
-
| `sys.datetime` | Current local datetime with timezone |
|
|
181
|
-
| `sys.date` | Current date, `YYYY-MM-DD` |
|
|
182
|
-
| `sys.time` | Current time, `HH:MM:SS` |
|
|
183
|
-
| `sys.ts` | Current Unix timestamp (seconds) |
|
|
184
|
-
| `sys.tz` | Timezone name (e.g. `Europe/Berlin`) |
|
|
185
|
-
|
|
186
|
-
#### Custom context via config
|
|
187
|
-
|
|
188
|
-
Add a `context` object at the root level of `hal.config.json` (applies to all projects) or inside individual projects (overrides root per key):
|
|
189
|
-
|
|
190
|
-
```json
|
|
191
|
-
{
|
|
192
|
-
"globals": { ... },
|
|
193
|
-
"context": {
|
|
194
|
-
"messageId": "${bot.messageId}",
|
|
195
|
-
"currentTime": "${sys.datetime}",
|
|
196
|
-
"buildVersion": "#{git rev-parse --short HEAD}"
|
|
197
|
-
},
|
|
198
|
-
"projects": [
|
|
199
|
-
{
|
|
200
|
-
"name": "backend",
|
|
201
|
-
"cwd": "./backend",
|
|
202
|
-
"telegram": { "botToken": "${BACKEND_BOT_TOKEN}" },
|
|
203
|
-
"context": {
|
|
204
|
-
"project": "backend",
|
|
205
|
-
"liveTimestamp": "@{date +\"%Y-%m-%d %H:%M:%S\"}"
|
|
206
|
-
}
|
|
129
|
+
"telegram": { "botToken": "${FRONTEND_BOT_TOKEN}" }
|
|
207
130
|
}
|
|
208
131
|
]
|
|
209
132
|
}
|
|
210
133
|
```
|
|
211
134
|
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
#### Variable substitution patterns
|
|
215
|
-
|
|
216
|
-
Three patterns are supported in context values:
|
|
217
|
-
|
|
218
|
-
| Pattern | Evaluated | Description |
|
|
219
|
-
|---------|-----------|-------------|
|
|
220
|
-
| `${expr}` | Per message | Looks up `expr` in implicit context (`bot.*`, `sys.*`), then env vars |
|
|
221
|
-
| `#{cmd}` | Once at boot | Runs shell command, caches result for all messages |
|
|
222
|
-
| `@{cmd}` | Per message | Runs shell command fresh for each message |
|
|
223
|
-
|
|
224
|
-
#### Context hooks
|
|
225
|
-
|
|
226
|
-
For advanced enrichment, you can provide a `context.mjs` hook file that transforms the context object with arbitrary JavaScript. Two hook locations are supported:
|
|
227
|
-
|
|
228
|
-
| Location | Scope |
|
|
229
|
-
|----------|-------|
|
|
230
|
-
| `{configDir}/.hal/hooks/context.mjs` | Global — runs for all projects |
|
|
231
|
-
| `{project.cwd}/.hal/hooks/context.mjs` | Project — runs for that project only |
|
|
232
|
-
|
|
233
|
-
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 the AI engine itself can create or modify hooks at runtime.
|
|
234
|
-
|
|
235
|
-
```js
|
|
236
|
-
// .hal/hooks/context.mjs
|
|
237
|
-
export default async (context) => ({
|
|
238
|
-
...context,
|
|
239
|
-
project: "my-tracker",
|
|
240
|
-
user: await fetchUserProfile(context["bot.userId"])
|
|
241
|
-
})
|
|
242
|
-
```
|
|
243
|
-
|
|
244
|
-
- **Input**: fully-resolved `Record\<string, string\>` context
|
|
245
|
-
- **Output**: a `Record\<string, string\>` — the final context passed to the engine
|
|
246
|
-
- If a hook throws, the bot logs the error and falls back to the pre-hook context
|
|
247
|
-
|
|
248
|
-
#### Prompt format
|
|
249
|
-
|
|
250
|
-
The resolved context is prepended to the user message before passing to the engine:
|
|
251
|
-
|
|
252
|
-
```
|
|
253
|
-
# Context
|
|
254
|
-
- bot.messageId: 12345
|
|
255
|
-
- sys.datetime: 2026-02-26 14:30:00 UTC+1
|
|
256
|
-
- project: backend
|
|
257
|
-
|
|
258
|
-
# User Message
|
|
259
|
-
What files changed today?
|
|
260
|
-
```
|
|
261
|
-
|
|
262
|
-
### `globals`
|
|
263
|
-
|
|
264
|
-
Default settings applied to all projects. Any setting defined in a project overrides its global counterpart.
|
|
265
|
-
|
|
266
|
-
| Key | Description | Default |
|
|
267
|
-
|-----|-------------|---------|
|
|
268
|
-
| `globals.engine.name` | Engine: `claude`, `copilot`, `codex`, `opencode` | `"claude"` |
|
|
269
|
-
| `globals.engine.command` | Override the CLI command path | _(engine name)_ |
|
|
270
|
-
| `globals.engine.model` | Override the AI model | _(engine default)_ |
|
|
271
|
-
| `globals.engine.session` | Use persistent sessions (`--resume` / `--continue`) | `true` |
|
|
272
|
-
| `globals.engine.sessionMsg` | Message sent when renewing session (e.g. `/clean`) | `"hi!"` |
|
|
273
|
-
| `globals.logging.level` | Log level: `debug`, `info`, `warn`, `error` | `"info"` |
|
|
274
|
-
| `globals.logging.flow` | Write logs to terminal | `true` |
|
|
275
|
-
| `globals.logging.persist` | Write logs to file | `false` |
|
|
276
|
-
| `globals.rateLimit.max` | Max messages per window per user | `10` |
|
|
277
|
-
| `globals.rateLimit.windowMs` | Rate limit window in ms | `60000` |
|
|
278
|
-
| `globals.access.allowedUserIds` | Telegram user IDs allowed by default | `[]` |
|
|
279
|
-
| `globals.dataDir` | Default user data directory | _(see below)_ |
|
|
280
|
-
| `globals.transcription.model` | Whisper model for voice | `"base.en"` |
|
|
281
|
-
| `globals.transcription.showTranscription` | Show transcribed text | `true` |
|
|
282
|
-
| `globals.commands` | Default `/start`, `/help`, `/reset`, `/clean` messages for all projects | _(see [`commands`](#commands))_ |
|
|
283
|
-
|
|
284
|
-
### `projects[]`
|
|
285
|
-
|
|
286
|
-
Each project entry creates one Telegram bot connected to one directory.
|
|
287
|
-
|
|
288
|
-
| Key | Required | Description |
|
|
289
|
-
|-----|----------|-------------|
|
|
290
|
-
| `name` | No | Unique identifier used as a slug for logs/data paths |
|
|
291
|
-
| `active` | No | Set to `false` to skip this project at boot (default: `true`) |
|
|
292
|
-
| `cwd` | **Yes** | Path to the project directory (relative to config file, or absolute) |
|
|
293
|
-
| `telegram.botToken` | **Yes** | Telegram bot token from BotFather |
|
|
294
|
-
| `access.allowedUserIds` | No | Override the global user whitelist for this bot |
|
|
295
|
-
| `engine.name` | No | Override the engine for this project |
|
|
296
|
-
| `engine.command` | No | Override the CLI command path |
|
|
297
|
-
| `engine.model` | No | Override the AI model |
|
|
298
|
-
| `engine.session` | No | Use persistent sessions for this project |
|
|
299
|
-
| `engine.sessionMsg` | No | Message used when renewing session |
|
|
300
|
-
| `transcription.showTranscription` | No | Override transcription display |
|
|
301
|
-
| `dataDir` | No | Override user data directory (see below) |
|
|
302
|
-
| `context` | No | Per-project context overrides (see [Context Injection](#context-injection)) |
|
|
303
|
-
| `commands` | No | Customize `/start`, `/help`, `/reset`, `/clean` messages (see [`commands`](#commands)) |
|
|
304
|
-
|
|
305
|
-
### `commands`
|
|
306
|
-
|
|
307
|
-
Customize the built-in `/start`, `/help`, `/reset`, and `/clean` command messages. Can be set under `globals` (shared default for all projects) or per project (overrides globals). Project-level settings take precedence.
|
|
308
|
-
|
|
309
|
-
```json
|
|
310
|
-
{
|
|
311
|
-
"globals": {
|
|
312
|
-
"commands": {
|
|
313
|
-
"start": {
|
|
314
|
-
"session": { "reset": true },
|
|
315
|
-
"message": { "text": "Welcome, ${bot.firstName}!" }
|
|
316
|
-
},
|
|
317
|
-
"help": {
|
|
318
|
-
"message": { "from": "./HELP.md" }
|
|
319
|
-
},
|
|
320
|
-
"reset": {
|
|
321
|
-
"message": { "text": "All user data wiped. Starting fresh." }
|
|
322
|
-
},
|
|
323
|
-
"clean": {
|
|
324
|
-
"message": { "text": "Session reset. Ready for a new conversation." }
|
|
325
|
-
}
|
|
326
|
-
}
|
|
327
|
-
}
|
|
328
|
-
}
|
|
329
|
-
```
|
|
330
|
-
|
|
331
|
-
Each command supports a `message` object with exactly one of:
|
|
332
|
-
|
|
333
|
-
| Field | Description |
|
|
334
|
-
|-------|-------------|
|
|
335
|
-
| `message.text` | Inline message string |
|
|
336
|
-
| `message.from` | Path to a file (relative to project `cwd`) whose content is used as the message |
|
|
337
|
-
|
|
338
|
-
Setting both `text` and `from`, or neither, is a configuration error.
|
|
339
|
-
|
|
340
|
-
The `/start` command additionally supports `session.reset` (boolean, default `false`). When `true`, the session is reset after sending the welcome message (same effect as `/clean`).
|
|
341
|
-
|
|
342
|
-
The `/reset` command always wipes all user data (uploads, downloads, session) regardless of configuration — the custom message only changes what the user sees afterward.
|
|
343
|
-
|
|
344
|
-
The `/clean` command always resets the LLM session regardless of configuration — user files (uploads, downloads) are preserved. The custom message only changes what the user sees afterward.
|
|
345
|
-
|
|
346
|
-
**Defaults** (when no `commands` config is set):
|
|
347
|
-
|
|
348
|
-
| Command | Default message |
|
|
349
|
-
|---------|-----------------|
|
|
350
|
-
| `/start` | `Welcome to ${project.name}!` followed by the command list |
|
|
351
|
-
| `/help` | The command list |
|
|
352
|
-
| `/reset` | `All user data wiped and session reset. Your next message starts fresh.` |
|
|
353
|
-
| `/clean` | `Session reset. Your next message starts a new conversation.` |
|
|
354
|
-
|
|
355
|
-
Messages are sent with Telegram's legacy Markdown formatting. Supported syntax: `*bold*`, `_italic_`, `` `inline code` ``, ` ```code blocks``` `, `[link text](url)`.
|
|
356
|
-
|
|
357
|
-
#### Variable substitution in command messages
|
|
358
|
-
|
|
359
|
-
All `message.text` values and file contents from `message.from` support the same placeholder patterns used elsewhere:
|
|
360
|
-
|
|
361
|
-
| Pattern | Description |
|
|
362
|
-
|---------|-------------|
|
|
363
|
-
| `${varName}` | Implicit context (`bot.firstName`, `sys.date`, `project.name`, etc.) and env vars |
|
|
364
|
-
| `@{cmd}` | Message-time shell command |
|
|
365
|
-
|
|
366
|
-
Additionally, the special `${HAL_COMMANDS}` placeholder expands to a formatted list of all available commands, divided into three sections:
|
|
367
|
-
|
|
368
|
-
- **Commands** — built-in commands (`/start`, `/help`, `/reset`, `/clean`)
|
|
369
|
-
- **Custom Commands** — programmatic `.mjs` commands from global and project directories
|
|
370
|
-
- **Skills** — engine skills marked with `public: true` in their `SKILL.md` frontmatter
|
|
371
|
-
|
|
372
|
-
Example `WELCOME.md`:
|
|
373
|
-
|
|
374
|
-
```markdown
|
|
375
|
-
Welcome to ${project.name}, ${bot.firstName}!
|
|
376
|
-
|
|
377
|
-
${HAL_COMMANDS}
|
|
378
|
-
```
|
|
379
|
-
|
|
380
|
-
#### Making skills visible in the command list
|
|
135
|
+
</details>
|
|
381
136
|
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
```yaml
|
|
385
|
-
---
|
|
386
|
-
name: crm
|
|
387
|
-
description: Manage your contacts
|
|
388
|
-
public: true
|
|
389
|
-
---
|
|
390
|
-
```
|
|
391
|
-
|
|
392
|
-
### Project Slug
|
|
393
|
-
|
|
394
|
-
The slug is used as a folder name for log and data paths. It is derived from:
|
|
395
|
-
1. The `name` field, if provided
|
|
396
|
-
2. Otherwise, the `cwd` value slugified (e.g. `./foo/bar` → `foo-bar`)
|
|
397
|
-
|
|
398
|
-
### `dataDir` Values
|
|
399
|
-
|
|
400
|
-
| Value | Resolved Path |
|
|
401
|
-
|-------|---------------|
|
|
402
|
-
| _(empty)_ | `{project-cwd}/.hal/users` |
|
|
403
|
-
| `~` | `{config-dir}/.hal/{slug}/data` |
|
|
404
|
-
| Relative path (e.g. `.mydata`) | `{project-cwd}/{value}` |
|
|
405
|
-
| Absolute path | Used as-is |
|
|
406
|
-
|
|
407
|
-
### Log Files
|
|
408
|
-
|
|
409
|
-
When `logging.persist: true`, logs are written to:
|
|
410
|
-
```
|
|
411
|
-
{config-dir}/.hal/logs/{project-slug}/YYYY-MM-DD.txt
|
|
412
|
-
```
|
|
137
|
+
<details>
|
|
138
|
+
<summary>JSONC config example (with comments and trailing commas)</summary>
|
|
413
139
|
|
|
414
|
-
|
|
140
|
+
Use `hal.config.jsonc` to add inline comments and trailing commas. See [`examples/hal.config.jsonc`](examples/hal.config.jsonc) for a full example.
|
|
415
141
|
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
```json
|
|
142
|
+
```jsonc
|
|
419
143
|
{
|
|
144
|
+
// Global settings shared across all projects
|
|
420
145
|
"globals": {
|
|
421
|
-
"engine": {
|
|
146
|
+
"engine": {
|
|
147
|
+
"name": "claude", // claude, copilot, codex, opencode, cursor, antigravity
|
|
148
|
+
"session": true,
|
|
149
|
+
},
|
|
150
|
+
"access": { "allowedUserIds": [123456789] },
|
|
151
|
+
/* Rate limiting (per-user)
|
|
152
|
+
"rateLimit": { "max": 10, "windowMs": 60000 },
|
|
153
|
+
*/
|
|
422
154
|
},
|
|
423
155
|
"projects": [
|
|
424
156
|
{
|
|
425
|
-
"name": "
|
|
426
|
-
"cwd": "./
|
|
427
|
-
"telegram": { "botToken": "${
|
|
428
|
-
},
|
|
429
|
-
{
|
|
430
|
-
"name": "frontend",
|
|
431
|
-
"cwd": "./frontend",
|
|
432
|
-
"engine": { "name": "copilot", "model": "gpt-5-mini" },
|
|
433
|
-
"telegram": { "botToken": "${FRONTEND_BOT_TOKEN}" }
|
|
157
|
+
"name": "my-project",
|
|
158
|
+
"cwd": "./my-project",
|
|
159
|
+
"telegram": { "botToken": "${MY_BOT_TOKEN}" },
|
|
434
160
|
},
|
|
435
|
-
|
|
436
|
-
"name": "legacy",
|
|
437
|
-
"active": false,
|
|
438
|
-
"cwd": "./legacy",
|
|
439
|
-
"telegram": { "botToken": "${LEGACY_BOT_TOKEN}" }
|
|
440
|
-
}
|
|
441
|
-
]
|
|
161
|
+
],
|
|
442
162
|
}
|
|
443
163
|
```
|
|
444
164
|
|
|
445
|
-
|
|
446
|
-
- **backend** inherits the global engine (Claude Code, default model)
|
|
447
|
-
- **frontend** uses GitHub Copilot with the `gpt-5-mini` model
|
|
448
|
-
- **legacy** is inactive and will be skipped at boot
|
|
449
|
-
|
|
450
|
-
The `engine` object supports five fields:
|
|
451
|
-
|
|
452
|
-
| Field | Description | Default |
|
|
453
|
-
|-------|-------------|---------|
|
|
454
|
-
| `name` | Engine identifier: `claude`, `copilot`, `codex`, `opencode` | `"claude"` |
|
|
455
|
-
| `command` | Custom path to the CLI binary | _(engine name)_ |
|
|
456
|
-
| `model` | AI model override (omit to use the engine's default) | _(engine default)_ |
|
|
457
|
-
| `session` | Use persistent sessions (`--resume` / `--continue`) | `true` |
|
|
458
|
-
| `sessionMsg` | Message sent when renewing session (e.g. `/clean`) | `"hi!"` |
|
|
459
|
-
|
|
460
|
-
#### Claude Code
|
|
461
|
-
|
|
462
|
-
- **CLI:** `claude` — install and authenticate via [Claude Code CLI](https://github.com/anthropics/claude-code) (see [Prerequisites](#prerequisites)).
|
|
463
|
-
- **Project files:** `CLAUDE.md`, `.claude/settings.json` (see [How It Works](#how-it-works)).
|
|
464
|
-
- **Config:** `engine.name: "claude"`. Optional: `engine.command`, `engine.model` (passed as `--model`), `engine.session`, `engine.sessionMsg`.
|
|
465
|
-
- **Sessions:** When `engine.session` is `true`, the CLI is invoked with `--resume {sessionId}`. `/clean` clears the stored session and replies with a static message (no engine call).
|
|
466
|
-
|
|
467
|
-
#### GitHub Copilot
|
|
468
|
-
|
|
469
|
-
- **CLI:** `copilot` — install and authenticate via [GitHub Copilot CLI](https://docs.github.com/en/copilot/concepts/agents/copilot-cli) (see [Prerequisites](#prerequisites)).
|
|
470
|
-
- **Project file:** `AGENTS.md`.
|
|
471
|
-
- **Config:** `engine.name: "copilot"`. Optional: `engine.command`, `engine.model` (see table below), `engine.session`, `engine.sessionMsg`.
|
|
472
|
-
- **Sessions:** When `engine.session` is `true`, the CLI is invoked with `--continue`. `/clean` sends `engine.sessionMsg` to the engine without `--continue` to start a fresh session; the engine’s reply is sent to the user.
|
|
473
|
-
|
|
474
|
-
#### Codex
|
|
475
|
-
|
|
476
|
-
- **CLI:** `codex` — install and authenticate via [Codex CLI](https://github.com/openai/codex-cli) (see [Prerequisites](#prerequisites)).
|
|
477
|
-
- **Project file:** `AGENTS.md`.
|
|
478
|
-
- **Config:** `engine.name: "codex"`. Optional: `engine.command`, `engine.model` (e.g. `gpt-5.1-codex-mini`), `engine.session`, `engine.sessionMsg`.
|
|
479
|
-
- **Sessions:** When `engine.session` is `true`, the CLI is invoked with `codex exec resume --last` to continue the most recent session; otherwise `codex exec` starts a fresh run. `/clean` sends `engine.sessionMsg` without resuming, so the engine starts a new session; the engine's reply is sent to the user (same behaviour as Copilot).
|
|
480
|
-
|
|
481
|
-
#### GitHub Copilot Models
|
|
482
|
-
|
|
483
|
-
When using the `copilot` engine, the following models are available via `engine.model`:
|
|
484
|
-
|
|
485
|
-
| Model | Description |
|
|
486
|
-
|-------|-------------|
|
|
487
|
-
| `claude-sonnet-4.6` | Anthropic Claude Sonnet 4.6 |
|
|
488
|
-
| `claude-sonnet-4.5` | Anthropic Claude Sonnet 4.5 |
|
|
489
|
-
| `claude-haiku-4.5` | Anthropic Claude Haiku 4.5 |
|
|
490
|
-
| `claude-opus-4.6` | Anthropic Claude Opus 4.6 |
|
|
491
|
-
| `claude-opus-4.6-fast` | Anthropic Claude Opus 4.6 (fast) |
|
|
492
|
-
| `claude-opus-4.5` | Anthropic Claude Opus 4.5 |
|
|
493
|
-
| `claude-sonnet-4` | Anthropic Claude Sonnet 4 |
|
|
494
|
-
| `gemini-3-pro-preview` | Google Gemini 3 Pro (preview) |
|
|
495
|
-
| `gpt-5.3-codex` | OpenAI GPT-5.3 Codex |
|
|
496
|
-
| `gpt-5.2-codex` | OpenAI GPT-5.2 Codex |
|
|
497
|
-
| `gpt-5.2` | OpenAI GPT-5.2 |
|
|
498
|
-
| `gpt-5.1-codex-max` | OpenAI GPT-5.1 Codex Max |
|
|
499
|
-
| `gpt-5.1-codex` | OpenAI GPT-5.1 Codex |
|
|
500
|
-
| `gpt-5.1` | OpenAI GPT-5.1 |
|
|
501
|
-
| `gpt-5.1-codex-mini` | OpenAI GPT-5.1 Codex Mini |
|
|
502
|
-
| `gpt-5-mini` | OpenAI GPT-5 Mini |
|
|
503
|
-
| `gpt-4.1` | OpenAI GPT-4.1 |
|
|
504
|
-
|
|
505
|
-
If `engine.model` is omitted, the engine uses its own default model.
|
|
506
|
-
|
|
507
|
-
## Directory Structure
|
|
508
|
-
|
|
509
|
-
With a config at `~/workspace/hal.config.json`:
|
|
510
|
-
|
|
511
|
-
```
|
|
512
|
-
~/workspace/
|
|
513
|
-
├── hal.config.json
|
|
514
|
-
├── hal.config.local.json (gitignored — local overrides / secrets)
|
|
515
|
-
├── .hal/
|
|
516
|
-
│ ├── hooks/
|
|
517
|
-
│ │ └── context.mjs (global context hook, optional)
|
|
518
|
-
│ ├── commands/
|
|
519
|
-
│ │ └── mycommand.mjs (global command, available to all projects)
|
|
520
|
-
│ └── logs/
|
|
521
|
-
│ ├── backend/
|
|
522
|
-
│ │ └── 2026-02-26.txt (when persist: true)
|
|
523
|
-
│ └── frontend/
|
|
524
|
-
│ └── 2026-02-26.txt
|
|
525
|
-
├── .env (variable declarations, safe to commit)
|
|
526
|
-
├── .env.local (gitignored — actual secret values)
|
|
527
|
-
├── backend/
|
|
528
|
-
│ ├── CLAUDE.md
|
|
529
|
-
│ ├── .claude/
|
|
530
|
-
│ │ ├── settings.json
|
|
531
|
-
│ │ └── skills/
|
|
532
|
-
│ │ └── deploy/
|
|
533
|
-
│ │ └── SKILL.md (skill exposed as /deploy command)
|
|
534
|
-
│ └── .hal/
|
|
535
|
-
│ ├── hooks/
|
|
536
|
-
│ │ └── context.mjs (project context hook, optional)
|
|
537
|
-
│ ├── commands/
|
|
538
|
-
│ │ └── deploy.mjs (project-specific command, optional)
|
|
539
|
-
│ └── users/
|
|
540
|
-
│ └── {userId}/
|
|
541
|
-
│ ├── uploads/ # Files FROM user (to Claude)
|
|
542
|
-
│ ├── downloads/ # Files TO user (from Claude)
|
|
543
|
-
│ └── session.json # Session data
|
|
544
|
-
└── frontend/
|
|
545
|
-
├── CLAUDE.md
|
|
546
|
-
└── .hal/
|
|
547
|
-
└── users/
|
|
548
|
-
```
|
|
549
|
-
|
|
550
|
-
## CLI Commands
|
|
551
|
-
|
|
552
|
-
```bash
|
|
553
|
-
# Show help
|
|
554
|
-
npx @marcopeg/hal --help
|
|
555
|
-
|
|
556
|
-
# Initialize config file
|
|
557
|
-
npx @marcopeg/hal init
|
|
558
|
-
npx @marcopeg/hal init --cwd ./workspace
|
|
559
|
-
|
|
560
|
-
# Start all bots
|
|
561
|
-
npx @marcopeg/hal
|
|
562
|
-
npx @marcopeg/hal --cwd ./workspace
|
|
563
|
-
```
|
|
165
|
+
</details>
|
|
564
166
|
|
|
565
167
|
## Bot Commands
|
|
566
168
|
|
|
@@ -571,242 +173,16 @@ npx @marcopeg/hal --cwd ./workspace
|
|
|
571
173
|
| `/reset` | Wipes out all user data and resets the LLM session |
|
|
572
174
|
| `/clean` | Resets the LLM session |
|
|
573
175
|
|
|
574
|
-
## Custom
|
|
575
|
-
|
|
576
|
-
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.
|
|
577
|
-
|
|
578
|
-
### File locations
|
|
579
|
-
|
|
580
|
-
| Location | Scope |
|
|
581
|
-
|----------|-------|
|
|
582
|
-
| `{project.cwd}/.hal/commands/{name}.mjs` | Project-specific |
|
|
583
|
-
| `{configDir}/.hal/commands/{name}.mjs` | Global — available to all projects |
|
|
584
|
-
|
|
585
|
-
Project-specific commands take precedence over global ones on name collision.
|
|
586
|
-
|
|
587
|
-
### Command file format
|
|
588
|
-
|
|
589
|
-
```js
|
|
590
|
-
// .hal/commands/deploy.mjs
|
|
591
|
-
export const description = 'Deploy the project'; // shown in Telegram's / menu
|
|
592
|
-
|
|
593
|
-
export default async function({ args, ctx, projectCtx }) {
|
|
594
|
-
const env = args[0] ?? 'staging';
|
|
595
|
-
return `Deploying to ${env}...`;
|
|
596
|
-
}
|
|
597
|
-
```
|
|
598
|
-
|
|
599
|
-
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`).
|
|
600
|
-
|
|
601
|
-
### Handler arguments
|
|
602
|
-
|
|
603
|
-
#### `args: string[]`
|
|
604
|
-
|
|
605
|
-
Tokens following the command name, split on whitespace.
|
|
606
|
-
|
|
607
|
-
```
|
|
608
|
-
/deploy staging eu-west → args = ['staging', 'eu-west']
|
|
609
|
-
/status → args = []
|
|
610
|
-
```
|
|
176
|
+
## Custom commands and skills
|
|
611
177
|
|
|
612
|
-
|
|
178
|
+
Add your own slash commands as `.mjs` files (project or global), or use engine skill folders that HAL exposes as commands. Custom commands can override a skill with the same name. Both are hot-reloaded.
|
|
613
179
|
|
|
614
|
-
|
|
180
|
+
- **[Custom commands](docs/custom-commands/README.md)** — file locations, handler arguments (`args`, `ctx`, `gram`, `agent`, `projectCtx`), examples.
|
|
181
|
+
- **[Skills](docs/skills/README.md)** — SKILL.md format, per-engine directories, precedence.
|
|
615
182
|
|
|
616
|
-
|
|
617
|
-
|-----------|-------------|
|
|
618
|
-
| `bot.*` | `bot.userId`, `bot.username`, `bot.firstName`, `bot.chatId`, `bot.messageId`, `bot.timestamp`, `bot.datetime`, `bot.messageType` |
|
|
619
|
-
| `sys.*` | `sys.date`, `sys.time`, `sys.datetime`, `sys.ts`, `sys.tz` |
|
|
620
|
-
| `project.*` | `project.name`, `project.cwd`, `project.slug` |
|
|
621
|
-
| custom | Any keys defined in `context` config blocks, after `${}` / `#{}` / `@{}` substitution and context hook transforms |
|
|
183
|
+
## Voice messages
|
|
622
184
|
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
#### `gram: Grammy Context`
|
|
626
|
-
|
|
627
|
-
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.
|
|
628
|
-
|
|
629
|
-
Common patterns:
|
|
630
|
-
|
|
631
|
-
```js
|
|
632
|
-
// Send a temporary status message, then delete it
|
|
633
|
-
const status = await gram.reply('Working...');
|
|
634
|
-
// ... do work ...
|
|
635
|
-
await gram.api.deleteMessage(gram.chat.id, status.message_id);
|
|
636
|
-
|
|
637
|
-
// Edit the status message while working
|
|
638
|
-
await gram.api.editMessageText(gram.chat.id, status.message_id, 'Still working...');
|
|
639
|
-
|
|
640
|
-
// React to the original message
|
|
641
|
-
await gram.react([{ type: 'emoji', emoji: '👍' }]);
|
|
642
|
-
|
|
643
|
-
// Send a file
|
|
644
|
-
await gram.replyWithDocument(new InputFile('/path/to/file.pdf'));
|
|
645
|
-
```
|
|
646
|
-
|
|
647
|
-
When using `gram` to send your own reply, return `null` or `undefined` to suppress the default text reply:
|
|
648
|
-
|
|
649
|
-
```js
|
|
650
|
-
export default async function({ gram }) {
|
|
651
|
-
await gram.reply('Done!');
|
|
652
|
-
return null;
|
|
653
|
-
}
|
|
654
|
-
```
|
|
655
|
-
|
|
656
|
-
#### `agent: Agent`
|
|
657
|
-
|
|
658
|
-
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.
|
|
659
|
-
|
|
660
|
-
```ts
|
|
661
|
-
interface Agent {
|
|
662
|
-
call(
|
|
663
|
-
prompt: string,
|
|
664
|
-
options?: { onProgress?: (message: string) => void }
|
|
665
|
-
): Promise\<string\>;
|
|
666
|
-
}
|
|
667
|
-
```
|
|
668
|
-
|
|
669
|
-
Unlike regular user messages, agent calls have no session history and no context header prepended — the prompt is sent to the engine as-is.
|
|
670
|
-
|
|
671
|
-
| Option | Type | Description |
|
|
672
|
-
|--------|------|-------------|
|
|
673
|
-
| `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. |
|
|
674
|
-
|
|
675
|
-
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}`.
|
|
676
|
-
|
|
677
|
-
```js
|
|
678
|
-
export default async function({ args, gram, agent }) {
|
|
679
|
-
const status = await gram.reply('Thinking...');
|
|
680
|
-
|
|
681
|
-
const answer = await agent.call(`Summarise: ${args.join(' ')}`, {
|
|
682
|
-
onProgress: async (activity) => {
|
|
683
|
-
try {
|
|
684
|
-
await gram.api.editMessageText(gram.chat.id, status.message_id, `⏳ ${activity}`);
|
|
685
|
-
} catch { /* ignore if message was already edited */ }
|
|
686
|
-
},
|
|
687
|
-
});
|
|
688
|
-
|
|
689
|
-
await gram.api.deleteMessage(gram.chat.id, status.message_id);
|
|
690
|
-
return answer;
|
|
691
|
-
}
|
|
692
|
-
```
|
|
693
|
-
|
|
694
|
-
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.
|
|
695
|
-
|
|
696
|
-
#### `projectCtx: ProjectContext`
|
|
697
|
-
|
|
698
|
-
The project-level context object. Useful fields:
|
|
699
|
-
|
|
700
|
-
| Field | Type | Description |
|
|
701
|
-
|-------|------|-------------|
|
|
702
|
-
| `projectCtx.config.name` | `string \| undefined` | Project name from config |
|
|
703
|
-
| `projectCtx.config.slug` | `string` | Internal slug (used for log/data paths) |
|
|
704
|
-
| `projectCtx.config.cwd` | `string` | Absolute path to the project directory |
|
|
705
|
-
| `projectCtx.config.configDir` | `string` | Absolute path to the directory containing `hal.config.json` |
|
|
706
|
-
| `projectCtx.config.dataDir` | `string` | Absolute path to user data storage root |
|
|
707
|
-
| `projectCtx.config.context` | `Record\<string, string\> \| undefined` | Raw config-level context values (pre-hook) |
|
|
708
|
-
| `projectCtx.logger` | Pino logger | Structured logger — use for debug output that ends up in log files |
|
|
709
|
-
|
|
710
|
-
### Examples
|
|
711
|
-
|
|
712
|
-
- [`examples/obsidian/.hal/commands/status.mjs`](examples/obsidian/.hal/commands/status.mjs) — project-specific command using `projectCtx.config`
|
|
713
|
-
- [`examples/.hal/commands/context.mjs`](examples/.hal/commands/context.mjs) — global command that dumps the full resolved context
|
|
714
|
-
- [`examples/.hal/commands/joke.mjs`](examples/.hal/commands/joke.mjs) — global command using `agent.call` with live status cycling and `onProgress` updates
|
|
715
|
-
|
|
716
|
-
### Skills
|
|
717
|
-
|
|
718
|
-
[Claude Code skills](https://docs.anthropic.com/en/docs/claude-code/skills) live in `.claude/skills/` inside the project directory (shared across all engines). Each skill is a folder containing a `SKILL.md` file with a YAML frontmatter block and a prompt body:
|
|
719
|
-
|
|
720
|
-
```
|
|
721
|
-
{project-cwd}/
|
|
722
|
-
└── .claude/
|
|
723
|
-
└── skills/
|
|
724
|
-
└── chuck/
|
|
725
|
-
└── SKILL.md
|
|
726
|
-
```
|
|
727
|
-
|
|
728
|
-
```markdown
|
|
729
|
-
---
|
|
730
|
-
name: chuck
|
|
731
|
-
description: Tells a joke about Chuck Norris.
|
|
732
|
-
---
|
|
733
|
-
|
|
734
|
-
Tell a short, funny joke about Chuck Norris.
|
|
735
|
-
```
|
|
736
|
-
|
|
737
|
-
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.
|
|
738
|
-
|
|
739
|
-
When a user invokes a skill command (e.g. `/chuck`) the bot:
|
|
740
|
-
1. Reads the `SKILL.md` prompt body
|
|
741
|
-
2. Appends any user arguments as `User input: {args}` if present
|
|
742
|
-
3. Calls the AI engine with that prompt via the engine-agnostic `agent.call()` interface
|
|
743
|
-
4. Sends the response back to the user
|
|
744
|
-
|
|
745
|
-
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.
|
|
746
|
-
|
|
747
|
-
**Command precedence** (highest wins):
|
|
748
|
-
|
|
749
|
-
```
|
|
750
|
-
project .hal/commands/{name}.mjs > global .hal/commands/{name}.mjs > .claude/skills/{name}/
|
|
751
|
-
```
|
|
752
|
-
|
|
753
|
-
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.
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
### Hot-reload
|
|
757
|
-
|
|
758
|
-
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 the AI engine can write new command or skill files as part of a task and users see them in the `/` menu immediately.
|
|
759
|
-
|
|
760
|
-
## Creating a Telegram Bot
|
|
761
|
-
|
|
762
|
-
1. Message [@BotFather](https://t.me/BotFather) on Telegram
|
|
763
|
-
2. Send `/newbot`
|
|
764
|
-
3. Choose a display name (e.g. "My Backend Assistant")
|
|
765
|
-
4. Choose a username ending in `bot` (e.g. `my_backend_assistant_bot`)
|
|
766
|
-
5. Add the token to `.env.local` and reference it via `${VAR_NAME}` in `hal.config.json`
|
|
767
|
-
|
|
768
|
-
For each project you need a separate bot and token.
|
|
769
|
-
|
|
770
|
-
## Finding Your Telegram User ID
|
|
771
|
-
|
|
772
|
-
1. Message [@userinfobot](https://t.me/userinfobot) on Telegram
|
|
773
|
-
2. It will reply with your numeric user ID
|
|
774
|
-
3. Add it to `allowedUserIds`
|
|
775
|
-
|
|
776
|
-
## Voice Messages
|
|
777
|
-
|
|
778
|
-
Voice messages are transcribed locally using [Whisper](https://github.com/openai/whisper) via the `nodejs-whisper` package. No audio is sent to external services.
|
|
779
|
-
|
|
780
|
-
### Setup
|
|
781
|
-
|
|
782
|
-
1. **ffmpeg** — for audio conversion
|
|
783
|
-
```bash
|
|
784
|
-
brew install ffmpeg # macOS
|
|
785
|
-
sudo apt install ffmpeg # Ubuntu/Debian
|
|
786
|
-
```
|
|
787
|
-
|
|
788
|
-
2. **CMake** — for building the Whisper executable
|
|
789
|
-
```bash
|
|
790
|
-
brew install cmake # macOS
|
|
791
|
-
sudo apt install cmake # Ubuntu/Debian
|
|
792
|
-
```
|
|
793
|
-
|
|
794
|
-
3. **Download and build Whisper** — run once after installation:
|
|
795
|
-
```bash
|
|
796
|
-
npx nodejs-whisper download
|
|
797
|
-
```
|
|
798
|
-
|
|
799
|
-
### Whisper Models
|
|
800
|
-
|
|
801
|
-
| Model | Size | Speed | Quality |
|
|
802
|
-
|-------|------|-------|---------|
|
|
803
|
-
| `tiny` | ~75 MB | Fastest | Basic |
|
|
804
|
-
| `tiny.en` | ~75 MB | Fastest | English-only |
|
|
805
|
-
| `base` | ~142 MB | Fast | Good |
|
|
806
|
-
| `base.en` | ~142 MB | Fast | English-only (default) |
|
|
807
|
-
| `small` | ~466 MB | Medium | Good multilingual |
|
|
808
|
-
| `medium` | ~1.5 GB | Slower | Very good multilingual |
|
|
809
|
-
| `large-v3-turbo` | ~1.5 GB | Fast | Near-large quality |
|
|
185
|
+
Voice messages are transcribed locally with [Whisper](https://github.com/openai/whisper) (no audio sent to external services). **[Voice messages](docs/voice/README.md)** — setup (ffmpeg, CMake, nodejs-whisper), model options.
|
|
810
186
|
|
|
811
187
|
## Sending Files to Users
|
|
812
188
|
|
|
@@ -817,39 +193,6 @@ The engine can send files back through Telegram. Each user has a `downloads/` fo
|
|
|
817
193
|
3. The file is sent via Telegram (as a document)
|
|
818
194
|
4. The file is deleted from the server after delivery
|
|
819
195
|
|
|
820
|
-
## Migration from v1 (Single-Project Config)
|
|
821
|
-
|
|
822
|
-
The old single-project config format is no longer supported. Migrate by wrapping your config:
|
|
823
|
-
|
|
824
|
-
**Before:**
|
|
825
|
-
```json
|
|
826
|
-
{
|
|
827
|
-
"telegram": { "botToken": "..." },
|
|
828
|
-
"access": { "allowedUserIds": [123] },
|
|
829
|
-
"claude": { "command": "claude" },
|
|
830
|
-
"logging": { "level": "info" }
|
|
831
|
-
}
|
|
832
|
-
```
|
|
833
|
-
|
|
834
|
-
**After:**
|
|
835
|
-
```json
|
|
836
|
-
{
|
|
837
|
-
"globals": {
|
|
838
|
-
"engine": { "name": "claude" },
|
|
839
|
-
"logging": { "level": "info" }
|
|
840
|
-
},
|
|
841
|
-
"projects": [
|
|
842
|
-
{
|
|
843
|
-
"cwd": ".",
|
|
844
|
-
"telegram": { "botToken": "..." },
|
|
845
|
-
"access": { "allowedUserIds": [123] }
|
|
846
|
-
}
|
|
847
|
-
]
|
|
848
|
-
}
|
|
849
|
-
```
|
|
850
|
-
|
|
851
|
-
> **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).
|
|
852
|
-
|
|
853
196
|
## Security Notice
|
|
854
197
|
|
|
855
198
|
**Important**: Conversations with this bot are not end-to-end encrypted. Messages pass through Telegram's servers. Do not share:
|
|
@@ -863,4 +206,8 @@ This bot is intended for development assistance only. Treat all conversations as
|
|
|
863
206
|
|
|
864
207
|
## License
|
|
865
208
|
|
|
866
|
-
|
|
209
|
+
MIT
|
|
210
|
+
|
|
211
|
+
---
|
|
212
|
+
|
|
213
|
+
This project is forked by the CCP at Telegram.
|