@marcopeg/hal 1.0.11

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