@marcopeg/hal 1.0.20 → 1.0.23

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