@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.
Files changed (163) hide show
  1. package/README.md +90 -743
  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/git/callback.d.ts +8 -0
  7. package/dist/bot/commands/git/callback.d.ts.map +1 -0
  8. package/dist/bot/commands/git/callback.js +72 -0
  9. package/dist/bot/commands/git/callback.js.map +1 -0
  10. package/dist/bot/commands/git/clean.d.ts +4 -0
  11. package/dist/bot/commands/git/clean.d.ts.map +1 -0
  12. package/dist/bot/commands/git/clean.js +50 -0
  13. package/dist/bot/commands/git/clean.js.map +1 -0
  14. package/dist/bot/commands/git/commit.d.ts +4 -0
  15. package/dist/bot/commands/git/commit.d.ts.map +1 -0
  16. package/dist/bot/commands/git/commit.js +72 -0
  17. package/dist/bot/commands/git/commit.js.map +1 -0
  18. package/dist/bot/commands/git/exec.d.ts +6 -0
  19. package/dist/bot/commands/git/exec.d.ts.map +1 -0
  20. package/dist/bot/commands/git/exec.js +16 -0
  21. package/dist/bot/commands/git/exec.js.map +1 -0
  22. package/dist/bot/commands/git/index.d.ts +6 -0
  23. package/dist/bot/commands/git/index.d.ts.map +1 -0
  24. package/dist/bot/commands/git/index.js +6 -0
  25. package/dist/bot/commands/git/index.js.map +1 -0
  26. package/dist/bot/commands/git/init.d.ts +4 -0
  27. package/dist/bot/commands/git/init.d.ts.map +1 -0
  28. package/dist/bot/commands/git/init.js +22 -0
  29. package/dist/bot/commands/git/init.js.map +1 -0
  30. package/dist/bot/commands/git/status.d.ts +4 -0
  31. package/dist/bot/commands/git/status.d.ts.map +1 -0
  32. package/dist/bot/commands/git/status.js +18 -0
  33. package/dist/bot/commands/git/status.js.map +1 -0
  34. package/dist/bot/commands/help.js +1 -1
  35. package/dist/bot/commands/help.js.map +1 -1
  36. package/dist/bot/commands/loader.d.ts +14 -6
  37. package/dist/bot/commands/loader.d.ts.map +1 -1
  38. package/dist/bot/commands/loader.js +126 -34
  39. package/dist/bot/commands/loader.js.map +1 -1
  40. package/dist/bot/commands/message.d.ts.map +1 -1
  41. package/dist/bot/commands/message.js +51 -25
  42. package/dist/bot/commands/message.js.map +1 -1
  43. package/dist/bot/commands/model-callback.d.ts +4 -0
  44. package/dist/bot/commands/model-callback.d.ts.map +1 -0
  45. package/dist/bot/commands/model-callback.js +34 -0
  46. package/dist/bot/commands/model-callback.js.map +1 -0
  47. package/dist/bot/commands/model.d.ts +4 -0
  48. package/dist/bot/commands/model.d.ts.map +1 -0
  49. package/dist/bot/commands/model.js +83 -0
  50. package/dist/bot/commands/model.js.map +1 -0
  51. package/dist/bot/commands/reset.d.ts +3 -2
  52. package/dist/bot/commands/reset.d.ts.map +1 -1
  53. package/dist/bot/commands/reset.js +73 -10
  54. package/dist/bot/commands/reset.js.map +1 -1
  55. package/dist/bot/commands/resetPrompt.d.ts +20 -0
  56. package/dist/bot/commands/resetPrompt.d.ts.map +1 -0
  57. package/dist/bot/commands/resetPrompt.js +58 -0
  58. package/dist/bot/commands/resetPrompt.js.map +1 -0
  59. package/dist/bot/commands/session.d.ts +2 -2
  60. package/dist/bot/commands/session.d.ts.map +1 -1
  61. package/dist/bot/commands/session.js +7 -4
  62. package/dist/bot/commands/session.js.map +1 -1
  63. package/dist/bot/commands/start.js +2 -2
  64. package/dist/bot/commands/start.js.map +1 -1
  65. package/dist/bot/commands/watcher.d.ts +2 -1
  66. package/dist/bot/commands/watcher.d.ts.map +1 -1
  67. package/dist/bot/commands/watcher.js +11 -5
  68. package/dist/bot/commands/watcher.js.map +1 -1
  69. package/dist/bot/handlers/text.d.ts.map +1 -1
  70. package/dist/bot/handlers/text.js +11 -6
  71. package/dist/bot/handlers/text.js.map +1 -1
  72. package/dist/bot/middleware/auth.d.ts +0 -3
  73. package/dist/bot/middleware/auth.d.ts.map +1 -1
  74. package/dist/bot/middleware/auth.js +13 -12
  75. package/dist/bot/middleware/auth.js.map +1 -1
  76. package/dist/bot.d.ts.map +1 -1
  77. package/dist/bot.js +43 -11
  78. package/dist/bot.js.map +1 -1
  79. package/dist/cli.js +52 -29
  80. package/dist/cli.js.map +1 -1
  81. package/dist/config-watcher.d.ts +10 -0
  82. package/dist/config-watcher.d.ts.map +1 -0
  83. package/dist/config-watcher.js +55 -0
  84. package/dist/config-watcher.js.map +1 -0
  85. package/dist/config-writer.d.ts +8 -0
  86. package/dist/config-writer.d.ts.map +1 -0
  87. package/dist/config-writer.js +57 -0
  88. package/dist/config-writer.js.map +1 -0
  89. package/dist/config.d.ts +338 -53
  90. package/dist/config.d.ts.map +1 -1
  91. package/dist/config.js +337 -92
  92. package/dist/config.js.map +1 -1
  93. package/dist/context/resolver.d.ts +4 -0
  94. package/dist/context/resolver.d.ts.map +1 -1
  95. package/dist/context/resolver.js +8 -2
  96. package/dist/context/resolver.js.map +1 -1
  97. package/dist/default-models.d.ts +3 -0
  98. package/dist/default-models.d.ts.map +1 -0
  99. package/dist/default-models.js +16 -0
  100. package/dist/default-models.js.map +1 -0
  101. package/dist/engine/adapters/antigravity.d.ts +13 -0
  102. package/dist/engine/adapters/antigravity.d.ts.map +1 -0
  103. package/dist/engine/adapters/antigravity.js +230 -0
  104. package/dist/engine/adapters/antigravity.js.map +1 -0
  105. package/dist/engine/adapters/claude.js +2 -2
  106. package/dist/engine/adapters/claude.js.map +1 -1
  107. package/dist/engine/adapters/codex.d.ts.map +1 -1
  108. package/dist/engine/adapters/codex.js +27 -8
  109. package/dist/engine/adapters/codex.js.map +1 -1
  110. package/dist/engine/adapters/copilot.d.ts.map +1 -1
  111. package/dist/engine/adapters/copilot.js +6 -3
  112. package/dist/engine/adapters/copilot.js.map +1 -1
  113. package/dist/engine/adapters/cursor.d.ts +3 -0
  114. package/dist/engine/adapters/cursor.d.ts.map +1 -0
  115. package/dist/engine/adapters/cursor.js +106 -0
  116. package/dist/engine/adapters/cursor.js.map +1 -0
  117. package/dist/engine/adapters/opencode.d.ts +2 -2
  118. package/dist/engine/adapters/opencode.d.ts.map +1 -1
  119. package/dist/engine/adapters/opencode.js +33 -13
  120. package/dist/engine/adapters/opencode.js.map +1 -1
  121. package/dist/engine/detect.d.ts +26 -0
  122. package/dist/engine/detect.d.ts.map +1 -0
  123. package/dist/engine/detect.js +129 -0
  124. package/dist/engine/detect.js.map +1 -0
  125. package/dist/engine/model-cache.d.ts +25 -0
  126. package/dist/engine/model-cache.d.ts.map +1 -0
  127. package/dist/engine/model-cache.js +162 -0
  128. package/dist/engine/model-cache.js.map +1 -0
  129. package/dist/engine/model-validation.d.ts +9 -0
  130. package/dist/engine/model-validation.d.ts.map +1 -0
  131. package/dist/engine/model-validation.js +21 -0
  132. package/dist/engine/model-validation.js.map +1 -0
  133. package/dist/engine/models-data.generated.d.ts +4 -0
  134. package/dist/engine/models-data.generated.d.ts.map +1 -0
  135. package/dist/engine/models-data.generated.js +196 -0
  136. package/dist/engine/models-data.generated.js.map +1 -0
  137. package/dist/engine/models-fetch.d.ts +5 -0
  138. package/dist/engine/models-fetch.d.ts.map +1 -0
  139. package/dist/engine/models-fetch.js +54 -0
  140. package/dist/engine/models-fetch.js.map +1 -0
  141. package/dist/engine/probe-utils.d.ts +6 -0
  142. package/dist/engine/probe-utils.d.ts.map +1 -0
  143. package/dist/engine/probe-utils.js +42 -0
  144. package/dist/engine/probe-utils.js.map +1 -0
  145. package/dist/engine/prompt.d.ts.map +1 -1
  146. package/dist/engine/prompt.js +8 -0
  147. package/dist/engine/prompt.js.map +1 -1
  148. package/dist/engine/registry.d.ts.map +1 -1
  149. package/dist/engine/registry.js +4 -0
  150. package/dist/engine/registry.js.map +1 -1
  151. package/dist/engine/types.d.ts +3 -3
  152. package/dist/engine/types.d.ts.map +1 -1
  153. package/dist/engine/types.js +2 -0
  154. package/dist/engine/types.js.map +1 -1
  155. package/dist/prompts.d.ts +19 -0
  156. package/dist/prompts.d.ts.map +1 -0
  157. package/dist/prompts.js +73 -0
  158. package/dist/prompts.js.map +1 -0
  159. package/dist/setup-wizard.d.ts +4 -0
  160. package/dist/setup-wizard.d.ts.map +1 -0
  161. package/dist/setup-wizard.js +258 -0
  162. package/dist/setup-wizard.js.map +1 -0
  163. 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
- - **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,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
- Project context is merged on top of root — `backend` inherits `messageId`, `currentTime`, and `buildVersion` from root context, and adds `project` and `liveTimestamp`.
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
- By default, skills are not listed in `${HAL_COMMANDS}`. Add `public: true` to a skill's frontmatter to include it:
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
- ### Engine Configuration
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
- Set the engine globally or per-project. The engine determines which AI coding CLI is invoked for each message.
417
-
418
- ```json
142
+ ```jsonc
419
143
  {
144
+ // Global settings shared across all projects
420
145
  "globals": {
421
- "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
+ */
422
154
  },
423
155
  "projects": [
424
156
  {
425
- "name": "backend",
426
- "cwd": "./backend",
427
- "telegram": { "botToken": "${BACKEND_BOT_TOKEN}" }
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
- In this example:
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 Commands
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
- #### `ctx: Record\<string, string\>`
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
- 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:
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
- | Key group | Description |
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
- Use `/context` (the built-in global command) to inspect the exact keys available at runtime.
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
- ISC
209
+ MIT
210
+
211
+ ---
212
+
213
+ This project is forked by the CCP at Telegram.