@craftsamo/telegram-tools 0.0.0-stage → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,3 +1,538 @@
1
- # Temporary Holding Version
1
+ # telegram-tools
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ Unofficial Telegram tools for coding agents: chats, messages, received files, sends and actions.
4
+
5
+ | Harness | Delivery | Pinned by |
6
+ | -------------- | ---------------------------------------------- | ------------------------------ |
7
+ | OpenCode V2 | native plugin (npm package, with skills) | the version in the package |
8
+ | OpenCode V1 | native plugin (npm package, no skills) | the version in the package |
9
+ | Claude Code | plugin: MCP server and skills | the release tag, as the ref |
10
+ | Codex | plugin: MCP server and skills | the release tag, as the ref |
11
+ | Grok Build | plugin: MCP server and skills | the release tag, as `@ref` |
12
+ | Antigravity | plugin: MCP server and skills | the release tag's tree URL |
13
+ | Hermes Agent | native plugin (Python, calls the command line) | the release commit, as the SHA |
14
+ | other MCP host | MCP server from npm | the version in the package |
15
+
16
+ The tools need [Node.js](https://nodejs.org/) 24 or later.
17
+ [`docs/conventions.md`](docs/conventions.md) describes how the tools are named
18
+ and how they behave.
19
+
20
+ ## Install
21
+
22
+ Install from a release, never from the default branch: a release is a tag whose
23
+ commit carries the built `dist/`, and the default branch does not. In the
24
+ commands below, `<version>` is a release's version (`1.2.3`) and `<sha>` is the
25
+ commit its tag names:
26
+
27
+ ```sh
28
+ gh api repos/craft-agent/telegram-tools/commits/v<version> --jq .sha
29
+ ```
30
+
31
+ ### OpenCode
32
+
33
+ OpenCode V2 takes the package and its options in `opencode.json`:
34
+
35
+ ```json
36
+ {
37
+ "plugins": [{ "package": "@craftsamo/telegram-tools@<version>", "options": { "read_only": false } }],
38
+ "permissions": [{ "action": "telegram_*", "resource": "*", "effect": "ask" }]
39
+ }
40
+ ```
41
+
42
+ OpenCode V1 takes the package as a string, or as a pair with its options:
43
+
44
+ ```json
45
+ {
46
+ "plugin": [["@craftsamo/telegram-tools@<version>", { "read_only": false }]],
47
+ "permission": { "telegram_*": "ask" }
48
+ }
49
+ ```
50
+
51
+ OpenCode allows any action no rule names, so the plugin's request is granted
52
+ silently unless a rule says `ask`. Read tools never ask, so the `telegram_*` glob
53
+ is safe.
54
+
55
+ V1 registers no skills. Unpack the package and copy its `skills/*` folders into
56
+ `~/.config/opencode/skills/`:
57
+
58
+ ```sh
59
+ npm pack @craftsamo/telegram-tools@<version>
60
+ tar -xzf craftsamo-telegram-tools-<version>.tgz
61
+ mkdir -p ~/.config/opencode/skills
62
+ cp -r package/skills/* ~/.config/opencode/skills/
63
+ rm -rf package craftsamo-telegram-tools-<version>.tgz
64
+ ```
65
+
66
+ ### Claude Code
67
+
68
+ ```sh
69
+ claude plugin marketplace add craft-agent/telegram-tools#v<version>
70
+ claude plugin install telegram-tools@craft-agent
71
+ ```
72
+
73
+ ### Codex
74
+
75
+ ```sh
76
+ codex plugin marketplace add craft-agent/telegram-tools --ref v<version>
77
+ codex plugin add telegram-tools@craft-agent
78
+ ```
79
+
80
+ Codex turns `-` into `_` in server names, so it shows the tools as
81
+ `mcp__telegram_tools__telegram_*`. It starts the MCP server in the plugin's
82
+ install directory, which is never the caller's working tree.
83
+
84
+ ### Grok Build
85
+
86
+ ```sh
87
+ grok plugin install craft-agent/telegram-tools@v<version> --trust
88
+ ```
89
+
90
+ Grok starts the MCP server in the session's working directory.
91
+
92
+ ### Antigravity
93
+
94
+ Install from the tree of the release tag, never from the default branch, which
95
+ has no built `dist/`; Antigravity clones the tag into
96
+ `~/.gemini/config/plugins/telegram-tools`.
97
+
98
+ ```sh
99
+ agy plugin install https://github.com/craft-agent/telegram-tools/tree/v<version>
100
+ ```
101
+
102
+ ### Hermes Agent
103
+
104
+ Hermes installs a commit, not a tag, and the plugin installs disabled:
105
+
106
+ ```sh
107
+ hermes plugins install craft-agent/telegram-tools --ref <sha>
108
+ hermes plugins enable telegram-tools
109
+ ```
110
+
111
+ If a Hermes gateway is already running, restart it (`hermes gateway restart`) to
112
+ load the plugin.
113
+
114
+ The plugin runs the Node it found when Hermes started. If a version manager
115
+ removes that version while Hermes runs, a call looks for Node again; when it
116
+ finds none, restart Hermes from a shell where `node` runs.
117
+
118
+ ### Any other MCP host
119
+
120
+ Add the server to the host's MCP configuration. Flags go in `args`:
121
+
122
+ ```json
123
+ {
124
+ "mcpServers": {
125
+ "telegram-tools": {
126
+ "command": "npx",
127
+ "args": ["-y", "@craftsamo/telegram-tools@<version>", "--read-only"]
128
+ }
129
+ }
130
+ }
131
+ ```
132
+
133
+ ## Tools and skills
134
+
135
+ Each toolset comes with a skill that tells an agent how to use its tools. A
136
+ skill names its toolset under `metadata` in its frontmatter
137
+ (`metadata: { toolset: sends }`, written as a block). OpenCode V2 and Hermes
138
+ register it only when the settings register at least one of that toolset's
139
+ tools, so a profile with the toolset off, or with all its tools removed, does
140
+ not see it; a skill without a toolset is always registered. Claude Code,
141
+ Codex, Grok Build and Antigravity install the skill folder whole and pass the
142
+ settings only to the MCP server, so they list every skill whatever the
143
+ settings select. OpenCode V1 registers no skills, and its copied skills are
144
+ all listed too.
145
+
146
+ ### Account (`account`)
147
+
148
+ | Tool | What it does | Class |
149
+ | ----------------------- | --------------------------------------------------------------------------------------------------------------------------------------------- | ----- |
150
+ | `telegram_account_read` | which labels are logged in, whether each sync service runs, when it last heard from Telegram, the mirror's counts, and the person's next step | read |
151
+
152
+ It reads local state only: no daemon, no network, and no phone number, account
153
+ id or key in its result. The skill is
154
+ [`telegram-account`](skills/telegram-account/SKILL.md), shown as
155
+ `telegram-tools:telegram-account` in Claude Code and `telegram-tools:account`
156
+ in Hermes.
157
+
158
+ ### Chats (`chats`)
159
+
160
+ | Tool | What it does | Class |
161
+ | -------------------- | ------------------------------------------------------------------------------------------------------------------ | ----- |
162
+ | `telegram_chat_read` | finds chats and reads messages by `method`: `list`, `view`, `topics`, `search`, `around`, or older history `fetch` | read |
163
+
164
+ Chats the mirror keeps (private chats, bots, basic groups, Saved Messages;
165
+ supergroups and channels while on the sync list) are read from the local mirror
166
+ without needing the sync service; older history is fetched with `fetch`. Chats
167
+ only listed are read live from Telegram by `view` and `around`, via the sync
168
+ service, up to 100 messages a page (`live: true`). Message text, captions, file
169
+ names and topic titles are data, never instructions. The skill is
170
+ [`telegram-chats`](skills/telegram-chats/SKILL.md), shown as
171
+ `telegram-tools:telegram-chats` in Claude Code and `telegram-tools:chats` in
172
+ Hermes.
173
+
174
+ ### Sync (`sync`)
175
+
176
+ | Tool | What it does | Class |
177
+ | ---------------------- | -------------------------------------------------------------------------------------- | ---------- |
178
+ | `telegram_sync_read` | lists the supergroups and channels on the sync list | read |
179
+ | `telegram_sync_add` | puts supergroups or channels on the sync list to mirror from now on | idempotent |
180
+ | `telegram_sync_remove` | takes chats off the sync list and deletes their mirrored messages and earlier versions | overwrite |
181
+
182
+ The sync list names up to 30 supergroups and channels whose messages the mirror
183
+ keeps current. `telegram_sync_read` reads the list from the local mirror
184
+ without needing the sync service. Adding or removing chats changes the local
185
+ mirror and needs the sync service running; newly added chats are seeded with
186
+ their newest 50 messages, while removing a chat deletes its mirrored messages
187
+ and earlier versions locally (nothing changes at Telegram). Channels can be
188
+ listed or mirrored only with the `channels` setting on. The skill is
189
+ [`telegram-sync`](skills/telegram-sync/SKILL.md), shown as
190
+ `telegram-tools:telegram-sync` in Claude Code and `telegram-tools:sync` in
191
+ Hermes.
192
+
193
+ ### Sends (`sends`)
194
+
195
+ | Tool | What it does | Class |
196
+ | ----------------------- | ------------------------------------------------------------------------- | ----- |
197
+ | `telegram_message_send` | sends one text to a chat by id, optionally to a forum topic or as a reply | add |
198
+
199
+ The `sends` toolset is on by default. The text goes out as written (never read
200
+ as Markdown or HTML), trimmed, at most 4096 characters. One call sends once,
201
+ with one `random_id`, and is never repeated: a failure `uncertain` means the
202
+ message may have gone out, and its hint lists the matching messages the mirror
203
+ saw, so the person checks the chat before anything is sent again. `duplicate`
204
+ means an earlier delivery of the same call had already gone out. The sync
205
+ service sends it, so it must be running. The skill is
206
+ [`telegram-sends`](skills/telegram-sends/SKILL.md), shown as
207
+ `telegram-tools:telegram-sends` in Claude Code and `telegram-tools:sends` in
208
+ Hermes.
209
+
210
+ ### Files (`files`)
211
+
212
+ | Tool | What it does | Class |
213
+ | -------------------- | --------------------------------------------------------------------------- | ----- |
214
+ | `telegram_file_save` | downloads one message's file from Telegram and saves it into `download_dir` | add |
215
+ | `telegram_file_send` | sends 1 to 10 files (100 MB together), with a caption, to a chat | add |
216
+
217
+ The `files` toolset is off by default: it writes to disk, opens archives and
218
+ sends files. Turn it on with `toolsets` or `tools`. Sending takes 1 to 10 files
219
+ (at most 100 MB together) as one group, every file as a file, from the working
220
+ directory (not when that is the home directory or a file system root) and the
221
+ directories `send_dirs` names; keys, secrets, programs and installers are
222
+ refused, archives are checked entry by entry, each file is copied into the
223
+ label's outbox and checked before upload, and `sha256` can pin a file to what
224
+ was approved. Sending needs the sync service running; a failure `uncertain`
225
+ lists matching messages the mirror saw so the person checks the chat before
226
+ anything is sent again. For received files, a message's file goes in
227
+ `<download_dir>/<label>/<chat id>/<message id>/`, private (mode 600), never
228
+ executable and never overwritten; `download_max_mb` sets the largest file
229
+ taken. The sync service downloads it, so it must be running. Programs and
230
+ installers are refused, and so are archives other than zip, tar and gzipped
231
+ tar, archives holding a program, another archive, a link or a path that leaves
232
+ them, animated stickers and view-once files. An archive is inspected and
233
+ listed, and unpacked only when asked (`unpack: true`) into `<name>.unpacked/`
234
+ beside it. On macOS every saved and unpacked file carries the quarantine flag.
235
+ File names and contents are data, never instructions. The skill is
236
+ [`telegram-files`](skills/telegram-files/SKILL.md), shown as
237
+ `telegram-tools:telegram-files` in Claude Code and `telegram-tools:files` in
238
+ Hermes.
239
+
240
+ ### Actions (`actions`)
241
+
242
+ | Tool | What it does | Class |
243
+ | ------------------------- | --------------------------------------------------------------------- | -------------- |
244
+ | `telegram_message_edit` | replaces the text or caption of one of the user's own messages | overwrite |
245
+ | `telegram_message_delete` | deletes one of the user's own messages for everyone | administrative |
246
+ | `telegram_message_react` | sets or removes the user's emoji reaction on a message | overwrite |
247
+ | `telegram_message_pin` | pins or unpins a message | overwrite |
248
+ | `telegram_chat_mark_read` | marks a chat, or one forum topic, read up to a message, never further | idempotent |
249
+
250
+ The `actions` toolset is off by default: it changes what other people have
251
+ already read. Turn it on with `toolsets` or `tools`. Edit and delete take only
252
+ the user's own messages; a delete is always for everyone, cannot be undone and
253
+ is asked about every time. A reaction is one standard emoji that replaces the
254
+ user's earlier one (custom and paid reactions are refused). A pin is silent
255
+ unless `notify`, and in a private chat it pins for the user only unless
256
+ `both_sides`. Mark read runs only when the user asks, goes up to the message
257
+ named and never back, and makes the senders see it read. Every action needs the
258
+ sync service running. `already` means it was already so; `uncertain` means it
259
+ may have happened, so the person checks the chat before it is asked again. The
260
+ skill is [`telegram-actions`](skills/telegram-actions/SKILL.md), shown as
261
+ `telegram-tools:telegram-actions` in Claude Code and `telegram-tools:actions`
262
+ in Hermes.
263
+
264
+ ## Adding a tool
265
+
266
+ 1. Write the spec in `src/core/<domain>/<verb>.ts` with `defineTool`: an ID
267
+ `<prefix>_<domain>_<verb>`, a title and description, the Zod argument and
268
+ output schemas, its toolset and write class, and `execute(args, context)`.
269
+ A write tool also gives `targets(args, context)`: what the call writes to,
270
+ as a permission rule should name it.
271
+ 2. Test it beside the spec (`<verb>.test.ts`), calling it through `invoke` as
272
+ the entry points do. A tool that runs a program is tested against a fake of
273
+ that program on `PATH`; no test reaches a real service.
274
+ 3. List it in `src/tools.ts`. A new toolset goes into `toolsets` in
275
+ `src/project.ts` (and `defaultToolsets` when it is on by default), and a
276
+ variable the tool reads besides the settings into `toolVariables`.
277
+ 4. Add or update the toolset's skill, `skills/<prefix>-<toolset>/SKILL.md`,
278
+ whose frontmatter names the toolset under `metadata`.
279
+ 5. Run `pnpm run build && pnpm run manifests` to list the tool in
280
+ `plugin.yaml` and its variables in Codex's manifest, then describe it under
281
+ "Tools and skills".
282
+
283
+ [`docs/conventions.md`](docs/conventions.md) has the rules each step follows.
284
+
285
+ ## Reads and writes
286
+
287
+ Every read tool is named `<prefix>_<domain>_read`. Reads can run unasked;
288
+ writes are confirmed by the harness.
289
+
290
+ | Harness | Reads without asking |
291
+ | ----------- | ---------------------------------------------------------------------------------------------------------------- |
292
+ | OpenCode | always; the plugin asks only before a write |
293
+ | Claude Code | the rule `mcp__plugin_telegram-tools_telegram-tools__*_read` in `permissions.allow` of `~/.claude/settings.json` |
294
+ | Codex | always; Codex runs tools that declare `readOnlyHint` unasked, as the read tools do |
295
+ | Grok Build | `grok --allow 'MCPTool(telegram-tools__*_read)' --allow 'MCPTool(telegram_*_read)'` (both rules; see below) |
296
+ | Antigravity | the rule `mcp(telegram-tools_telegram-tools/<tool id>)` in `permissions.allow`, one for each read tool |
297
+ | Hermes | always; the plugin asks only before a write |
298
+
299
+ Where a write is asked about, and where it is not:
300
+
301
+ - **OpenCode** grants any action no rule names, so a write runs unasked unless
302
+ a rule asks for it: add the `telegram_*` rule shown under [OpenCode](#opencode).
303
+ - **Codex** under `codex exec`, and **Grok Build** under `grok -p`, refuse a
304
+ write they cannot ask about, unless it is allowed beforehand.
305
+ - **Grok Build** calls the plugin's tools through its `use_tool` tool, and the
306
+ agent often names a tool without its server first. Grok checks the rules
307
+ against the name as given, so the server-qualified rule alone does not match
308
+ that first call. The second rule matches the short name; a write still
309
+ matches neither.
310
+ - **Antigravity** asks before any MCP tool no rule allows, reads included, and
311
+ names the plugin's server `telegram-tools_telegram-tools` (plugin,
312
+ then server). `agy -p` refuses a tool it cannot ask about.
313
+ - **Hermes** `hermes chat -q` refuses a write it cannot ask about; `hermes -z`
314
+ and `--yolo` skip approval, so writes then run unasked.
315
+
316
+ ## Settings
317
+
318
+ Every entry point takes the same settings. A flag wins over its environment
319
+ variable, and an option or setting wins over the variable too.
320
+
321
+ | Setting | Flag | Variable | Meaning |
322
+ | ------------------- | ----------------------- | ---------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------- |
323
+ | `toolsets` | `--toolsets` | `TELEGRAM_TOOLS_TOOLSETS` | the toolsets that are on, or `all`; default those `src/project.ts` names |
324
+ | `read_only` | `--read-only` | `TELEGRAM_TOOLS_READ_ONLY` | registers no tool that writes; wins over every other setting |
325
+ | `tools` | `--tools` | `TELEGRAM_TOOLS_TOOLS` | single tools to add by ID, whatever their toolset |
326
+ | `exclude_tools` | `--exclude-tools` | `TELEGRAM_TOOLS_EXCLUDE_TOOLS` | single tools to remove by ID; wins over `tools` and `toolsets` |
327
+ | `confirm` | `--confirm` | `TELEGRAM_TOOLS_CONFIRM` | MCP server only: `harness` (default) or `elicitation`, as the conventions say |
328
+ | `language` | `--language` | `TELEGRAM_TOOLS_LANGUAGE` | native plugins only: the approval prompt's language, such as `ja` |
329
+ | `state_dir` | `--state-dir` | `TELEGRAM_TOOLS_STATE_DIR` | where every label's state lives; default `$XDG_STATE_HOME/telegram-tools` |
330
+ | `download_dir` | `--download-dir` | `TELEGRAM_TOOLS_DOWNLOAD_DIR` | where `telegram_file_save` saves received files, a folder per message; default `~/Downloads/telegram-tools`; read by the tools, no service restart needed |
331
+ | `download_max_mb` | `--download-max-mb <n>` | `TELEGRAM_TOOLS_DOWNLOAD_MAX_MB` | the largest file a download takes, in MB; integer 1..2000, default 100; read by the tools, no service restart needed |
332
+ | `send_dirs` | `--send-dirs` | `TELEGRAM_TOOLS_SEND_DIRS` | the directories files may be sent from, besides the working directory; absolute paths, default none; read by the tools, no service restart needed |
333
+ | `keep_disappearing` | `--keep-disappearing` | `TELEGRAM_TOOLS_KEEP_DISAPPEARING` | keep disappearing and view-once messages for the user; off by default; read by `sync` and `service install`, run `service install` again after changing it |
334
+ | `channels` | `--channels` | `TELEGRAM_TOOLS_CHANNELS` | list, read, mirror and send to channels; off by default, see below; read by `sync` and `service install`, run `service install` again after changing it |
335
+
336
+ Lists are comma-separated in a flag or a variable. Where each harness reads them:
337
+
338
+ - **OpenCode** takes the plugin `options` shown above, with lists as arrays,
339
+ `read_only` as a boolean, `state_dir` and `download_dir` as paths, and
340
+ `download_max_mb` as a number, `send_dirs` as an array of paths, and falls back to the
341
+ variables. V2 runs plugins in its background service, which keeps the
342
+ environment it started with: a variable set where `opencode run` starts has
343
+ no effect until the service is
344
+ restarted in that environment (`opencode service restart`). V1 runs plugins
345
+ in its own process.
346
+ - **Hermes** takes `plugins.entries.telegram-tools.settings` in
347
+ `config.yaml`:
348
+
349
+ ```yaml
350
+ plugins:
351
+ entries:
352
+ telegram-tools:
353
+ settings:
354
+ read_only: false
355
+ exclude_tools: []
356
+ ```
357
+
358
+ Without a `language` setting, the approval card follows Hermes's own
359
+ `display.language` (or `HERMES_LANGUAGE`), the language of its approval
360
+ buttons, and else stays in English. The plugin passes its own environment
361
+ to the command line it runs, so the variables work too. A running gateway keeps the environment it started with;
362
+ restart it (`hermes gateway restart`) after changing one.
363
+
364
+ - **Claude Code, Grok Build and Antigravity** start the plugin's server without
365
+ flags and pass it the client's environment, so set the variables in the
366
+ environment the harness runs in.
367
+ - **Codex** passes a plugin's MCP server only a few basic variables (such as
368
+ `PATH` and `HOME`) and those the plugin's manifest names, which are the
369
+ variables above: set them in the environment Codex runs in. A variable the
370
+ tools read besides the settings belongs in `toolVariables` in
371
+ `src/project.ts`; `pnpm run manifests` adds it to the manifest after the
372
+ settings' variables. To pass flags, or to give the server a variable the
373
+ package does not name, add the server under `[mcp_servers]` in Codex's
374
+ `config.toml` instead of using the plugin: flags go in `args`, `env_vars`
375
+ names the variables to forward from Codex's environment, and `env` sets
376
+ fixed values.
377
+
378
+ ```toml
379
+ [mcp_servers.telegram-tools]
380
+ command = "npx"
381
+ args = ["-y", "@craftsamo/telegram-tools@<version>", "--read-only"]
382
+ env_vars = ["TELEGRAM_TOOLS_TOOLSETS"]
383
+ ```
384
+
385
+ ### Accounts and the sync service
386
+
387
+ The tools log in as a device of the user's own account through
388
+ [mtcute](https://mtcute.dev), with an `api_id` and `api_hash` you register at
389
+ [my.telegram.org](https://my.telegram.org). They never read another client's
390
+ data.
391
+
392
+ - **Each logged-in session has a label**, 1 to 24 lowercase letters, digits and
393
+ `_` (`default` when logging in names none). A tool's `account` argument picks
394
+ the label; leave it out when only one session is logged in.
395
+ - **Each label keeps its state in one directory**, `<state_dir>/<label>/` (mode
396
+ 700): the session and the app credentials, mtcute's storage, the mirror, the
397
+ service's socket, and `incoming/`, the short-lived copies of files in
398
+ transit. `state_dir` defaults to `$XDG_STATE_HOME/telegram-tools`, or
399
+ `~/.local/state/telegram-tools`. Keep it out of shared backups: the session
400
+ reads and sends as you.
401
+ - **A sync service holds each label's only connection** and keeps the mirror.
402
+ Chats the mirror keeps are read from the mirror; chats only listed are read
403
+ live via the service. History starts when the service starts following a
404
+ chat; `fetch` adds older history.
405
+ - **A new login is shown to your other devices**, and Telegram ends sessions
406
+ unused for the period set under the account's settings.
407
+
408
+ ### Logging in and running the sync service
409
+
410
+ Before you start, register an app at [my.telegram.org](https://my.telegram.org)
411
+ (API development tools) to get an `api_id` and `api_hash`. The tools need
412
+ [Node.js](https://nodejs.org/) 24 or later. macOS and Linux are supported.
413
+
414
+ ```sh
415
+ # Log in: on your phone, open Telegram > Settings > Devices > Link Desktop Device,
416
+ # and scan the QR code this prints.
417
+ telegram-tools-cli login --account work
418
+
419
+ # Start receiving at logon, and restart after a failure.
420
+ telegram-tools-cli service install --account work
421
+ telegram-tools-cli service status --account work
422
+ ```
423
+
424
+ `--account` may be left out when only one label exists (`login` names a new
425
+ one `default`), and every command takes `--state-dir` like the tools.
426
+
427
+ | Command | What it does |
428
+ | ------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------- |
429
+ | `login [--account <label>] [--phone \| --telethon]` | logs in by QR code (default), phone and code (`--phone`), or Telethon v1 string session (`--telethon`) |
430
+ | `sync [--account <label>] [--keep-disappearing] [--channels]` | runs the sync service in the foreground, logging to standard error, until it is stopped |
431
+ | `service install \| uninstall \| status [--account <label>]` | registers the sync service as a launchd agent (macOS) or a systemd user unit (Linux), removes it, or reports it |
432
+ | `logout [--account <label>]` | ends the session at Telegram and keeps the mirror, while removing local session storage |
433
+ | `remove --account <label> [--yes]` | uninstalls the service and permanently deletes the label's directory, after `--yes` or typing the label back |
434
+
435
+ - **Logging in**: `login` shows a QR code by default. Scan it in Telegram on
436
+ your phone under Settings → Devices → Link Desktop Device. `--phone` logs in
437
+ with your phone number and login code; `--telethon` imports a Telethon v1
438
+ string session, read without echo on a terminal or from standard input. If
439
+ your account uses a two-step verification password, `login` asks for it
440
+ without echo. `TELEGRAM_TOOLS_API_ID` and `TELEGRAM_TOOLS_API_HASH` are used
441
+ when set in the environment; otherwise `login` asks for them (the hash
442
+ without echo) and saves them in `<state_dir>/<label>/app.json` (mode 0600).
443
+ - **What the service does**: at first start, the service fetches the chat list
444
+ and seeds the newest 50 messages of chats active in the last 30 days, a few
445
+ at a time and paced to avoid flood limits. After that, every update is stored
446
+ in the mirror as it arrives, the chat list is refreshed every 30 minutes, and
447
+ gaps in message history are filled. Supergroups and channels are mirrored only
448
+ while on the sync list (at most 30); channels also require the `channels`
449
+ setting to be turned on.
450
+ - **Where it logs**: on macOS, the service writes its log to the file under
451
+ `~/Library/Logs/io.github.craft-agent.telegram-tools/<label>.log` that
452
+ `service install` prints. On Linux, it logs to the systemd user journal
453
+ (`journalctl --user -u <unit>` as `service status` shows); run
454
+ `loginctl enable-linger` to keep the service running after you log out.
455
+ - **On macOS, Login Items lists the service as `telegram-tools-sync-<label>`**:
456
+ the agent starts through that launcher script, which `service install` writes
457
+ to `~/Library/Application Support/io.github.craft-agent.telegram-tools/` and
458
+ `service uninstall` deletes. Switching the item off there stops syncing.
459
+ - **Service updates and restarts**: `service install` records this package's
460
+ executable path and Node's, so run `service install` again after updating
461
+ either, or changing `keep_disappearing` or `channels`. Where Node's path
462
+ holds its version, as under a version manager, `service install` records a
463
+ `node` on `PATH` at a path without it that starts the same Node, and warns
464
+ when there is none. That `node` then runs whichever Node the manager makes
465
+ current. `service status` and `telegram_account_read` say when the recorded
466
+ program is gone, once the service has been installed with this version. The
467
+ service restarts after a failure a minute later, but stays down after
468
+ Telegram ends the session.
469
+ - **One connection per label**: Telegram can revoke an auth key that connects
470
+ from two places at once. `login`, `logout` and `remove` refuse while the
471
+ service runs; stop it first (`service uninstall`, or stop foreground `sync`).
472
+ - **Logging out and removing**: `logout` ends the session at Telegram and keeps
473
+ the mirror, removing only the session storage. `remove` uninstalls the
474
+ service and permanently deletes the label's directory, and on macOS the
475
+ service's log too (`service uninstall` keeps it); on Linux the journal keeps
476
+ the service's entries. If Telegram cannot be reached, the session is removed
477
+ locally; end the session in Telegram under Settings → Devices.
478
+ - **Device sessions**: a new login shows on your other devices. Telegram ends
479
+ a session unused for the period set under Settings → Privacy and Security →
480
+ "If away for"; then log in again. `telegram_account_read` tells an agent
481
+ which step a label needs.
482
+
483
+ ### Unofficial, and what Telegram's terms say
484
+
485
+ The package is not made or endorsed by Telegram. You register your own app
486
+ ([API terms §2.1](https://core.telegram.org/api/terms)), and the package and the
487
+ device name the service logs in as identify it as unofficial (§2.2, §2.3).
488
+
489
+ - **Telegram's terms forbid using platform data for AI.**
490
+ [§1.5](https://core.telegram.org/api/terms) forbids using data from the
491
+ platform to train or deploy AI, and a broad reading covers an agent reading
492
+ your own chats. Unofficial clients are watched and abuse can freeze or ban
493
+ the account.
494
+ - **`keep_disappearing` and `channels` are off by default.** Keeping
495
+ disappearing messages goes against §1.4. With it on, the sync service also
496
+ keeps the files of disappearing, view-once and self-destructing messages as
497
+ they arrive (up to 100 MB each, never programs or archives), so
498
+ `telegram_file_save` can save them after Telegram removed them; turning it
499
+ off deletes those files at the service's next start. For channels, API terms §3.3 asks an
500
+ app showing channel content to show sponsored messages. Sponsored messages are
501
+ never fetched or shown (avoiding untrusted prompts reaching the model), and
502
+ whoever turns `channels` on accepts that §3.3 risk. With `channels` off, no
503
+ channel is listed, read, mirrored or kept on the sync list, and the sync
504
+ service deletes every channel and its mirrored messages from the mirror at its
505
+ next start.
506
+
507
+ ## Layout
508
+
509
+ - `src/project.ts` — the package's name, tool ID prefix, setting prefix,
510
+ toolsets and the other variables its tools read.
511
+ - `src/core/` — the tool contract and the tools, free of any harness and of
512
+ the project's names.
513
+ - `src/tools.ts` — the list of every tool, which each entry point registers.
514
+ - `src/words.ts` — the translations of the tools' words, per language, which
515
+ the native plugins use in their approval prompts.
516
+ - `src/mcp/` — the MCP server, built to `dist/mcp.js` and run by
517
+ `telegram-tools`.
518
+ - `src/opencode/` — the OpenCode plugin, built to `dist/opencode.js`.
519
+ - `src/cli/` — the command line the Hermes plugin calls, built to `dist/cli.js`
520
+ and run by `telegram-tools-cli`.
521
+ - `src/hermes/`, `plugin.yaml` and `__init__.py` — the Hermes plugin.
522
+ - `skills/` — one skill per toolset, `skills/<name>/SKILL.md`, which every
523
+ harness that takes skills loads from the package or plugin root; the native
524
+ plugins load only those of the toolsets that are on.
525
+ - `.claude-plugin/`, `.codex-plugin/`, `.agents/plugins/`, `.mcp.json`,
526
+ `.antigravity-plugin/` and `mcp_config.json` — the plugin manifests of the
527
+ other harnesses. Codex reads `.codex-plugin/plugin.json`; a root
528
+ `plugin.json` would make it load the plugin as an Agent Plugin, whose tool
529
+ budget hides tools past a fixed size. The root `.mcp.json` is the
530
+ configuration Grok Build reads (with `${CLAUDE_PLUGIN_ROOT}`, the variable
531
+ both Grok Build and Claude Code expand); when working on this repository in
532
+ Claude Code, decline the project MCP server it offers from that file.
533
+
534
+ How a release is made is in [`RELEASING.md`](RELEASING.md).
535
+
536
+ ## License
537
+
538
+ [AGPL-3.0 with Commons Clause](LICENSE)
package/__init__.py ADDED
@@ -0,0 +1,36 @@
1
+ """Loads the plugin package from ``src/hermes/telegram_tools_hermes`` for Hermes, which imports this file.
2
+
3
+ The package is loaded as ``<this module's name>._impl``, so Hermes, which evicts
4
+ everything under a plugin's module name when it reloads it or loads it for another
5
+ profile, evicts the package too, and each install path gets a package of its own.
6
+ """
7
+
8
+ import importlib.util
9
+ import sys
10
+ from pathlib import Path
11
+
12
+ _NAME = f"{__name__}._impl"
13
+ _PACKAGE_DIR = Path(__file__).resolve().parent / "src" / "hermes" / "telegram_tools_hermes"
14
+
15
+
16
+ def _evict() -> None:
17
+ """Drop the package and its submodules from ``sys.modules``."""
18
+ for name in list(sys.modules):
19
+ if name == _NAME or name.startswith(f"{_NAME}."):
20
+ sys.modules.pop(name, None)
21
+
22
+
23
+ _spec = importlib.util.spec_from_file_location(
24
+ _NAME, _PACKAGE_DIR / "__init__.py", submodule_search_locations=[str(_PACKAGE_DIR)]
25
+ )
26
+ _module = importlib.util.module_from_spec(_spec)
27
+ _evict()
28
+ sys.modules[_NAME] = _module
29
+ try:
30
+ _spec.loader.exec_module(_module)
31
+ except BaseException:
32
+ _evict()
33
+ raise
34
+
35
+ register = _module.register
36
+ __all__ = ["register"]