Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ src/core/voice/ STT/TTS orchestration, ffmpeg conversion, cache
src/core/gateway/controller.js messenger-neutral gateway controller
src/core/opencode/ OpenCode client and server manager
src/adapters/telegram/ grammY adapter, auth, media, albums, voice
src/adapters/telegram/stickers* Telegram sticker download, cache, store, rendering helpers
tests/ Vitest tests with mocked external services by default
```

Expand All @@ -78,6 +79,7 @@ Add modules only when they reduce real complexity. Prefer the smallest correct c
- Inline callback data must use short bounded tokens, not raw long session IDs or permission IDs.
- Permission prompts must remain text-only, even when voice replies are enabled.
- Photo downloads must not expose bot tokens in persisted attachment URLs.
- Sticker cache and saved pack state must not persist bot tokens, raw download URLs, chat IDs, user IDs, or raw Telegram payloads.
- Always clean up downloaded media files in `finally` or equivalent cleanup paths.
- Keep Telegram UX in the adapter; do not move Telegram reactions, message IDs, chat actions, or grammY types into core.

Expand All @@ -87,6 +89,7 @@ Add modules only when they reduce real complexity. Prefer the smallest correct c
- `telegram.botToken` and `telegram.allowedUserId` are required and must stay private.
- Project-local `.opencode-remote/` is ignored because `config.json` contains secrets.
- App state is non-secret SQLite data in the platform app-data directory; see `DEVELOPMENT.md` for exact paths.
- Telegram sticker pack state is non-secret adapter state in `telegram-stickers.db`; reusable visuals are disposable cache under `cache/stickers`.
- Project state uses OpenCode-style identity: Git remote hash, then cached repo ID, then root commit; non-Git folders use the shared `global` identity.
- Do not add model or provider env vars until the related feature is actually implemented.

Expand Down
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,13 @@ This project follows Semantic Versioning.

## Unreleased

## [0.6.0] - 2026-05-28

### Added

- Added Telegram sticker understanding with static sticker attachments, generated visual previews for video and animated stickers, reusable sticker visual caching, and saved sticker pack management. (#20)
- Added saved sticker replies for explicit sticker requests and eligible reaction markers, including a safe description catalog built from cached sticker visuals. (#20)

### Removed

- Removed Dependabot configuration to stop automated dependency update pull requests.
Expand Down
3 changes: 3 additions & 0 deletions DEVELOPMENT.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ Development notes for `@crankshift/opencode-remote`.
- Node.js 22.18.0 or newer. Node.js 24 LTS is recommended.
- pnpm 11.3.0.
- Optional voice mode development: local `ffmpeg` for conversion and a Groq API key for live transcription smoke tests.
- Optional sticker development: local `ffmpeg` for video sticker preview smoke tests and python-lottie's `lottie_convert.py` for animated `.tgs` sticker preview smoke tests.

## Install Dependencies

Expand Down Expand Up @@ -54,6 +55,8 @@ Gateway state is app-managed and stored in a SQLite database named `opencode-rem

The database stores non-secret project state such as the active OpenCode session and `/progress` preference. It keys Git projects similarly to OpenCode: Git remote identity first, then a cached repo ID, then root commit. Non-Git folders use a shared global project identity. Generated voice files are cache under the same app-data root at `cache/voice` and can be removed with `opencode-remote cache clear`.

Telegram sticker pack state is stored separately in `telegram-stickers.db` under the same app-data directory. Sticker visual cache files live under `cache/stickers`. Sticker state stores Telegram `file_unique_id`, current reusable `file_id`, pack name, emoji, dimensions, type, optional safe visual descriptions, and cache metadata. It must not store bot tokens, raw Telegram download URLs, chat IDs, user IDs, raw update payloads, or temp paths.

Use `opencode-remote run --state-suffix dev` to use `opencode-remote-dev.db` instead of the normal state database. The source `pnpm dev` script uses this to keep development state separate from regular gateway state.

## Runtime Internals
Expand Down
11 changes: 10 additions & 1 deletion FEATURES.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Features

OpenCode Remote is currently a Telegram gateway for OpenCode with text, image, and opt-in voice support.
OpenCode Remote is currently a Telegram gateway for OpenCode with text, image, sticker, and opt-in voice support.

## Available Now

Expand All @@ -18,6 +18,7 @@ OpenCode Remote is currently a Telegram gateway for OpenCode with text, image, a
- Interactive JSON config setup with project-local and global config discovery, selected-scope current defaults, highlighted arrow-key lists, and `ffmpeg` install/retry handling for voice setup.
- SQLite app-state persistence for selected OpenCode sessions and progress preferences, scoped by OpenCode project identity.
- Optional Telegram voice mode using Groq Whisper transcription, Edge TTS speech generation, and `ffmpeg` OGG Opus conversion.
- Telegram sticker understanding with static WebP sticker attachments, generated or fallback visual context for non-static stickers, saved sticker packs, and sticker replies.
- CLI config updates with `opencode-remote config set` and voice cache clearing with `opencode-remote cache clear`.

## Telegram Chat Behavior
Expand All @@ -28,6 +29,7 @@ OpenCode Remote is currently a Telegram gateway for OpenCode with text, image, a
- `/stop` requests abort for the active OpenCode session.
- `/progress` shows or sets prompt activity visibility: `off`, `new`, `all`, or `verbose`.
- `/voice` shows and controls voice mode, lists voices by required short country/locale filter, sets the active Edge TTS voice, and sends a test voice note.
- `/stickers` saves, lists, and forgets sticker packs for future sticker replies.
- `/help` shows the available bot commands.
- The Telegram slash-command menu is refreshed on gateway startup.
- Non-command text from the authorized user is sent to OpenCode as a prompt.
Expand All @@ -37,6 +39,8 @@ OpenCode Remote is currently a Telegram gateway for OpenCode with text, image, a
- OpenCode permission requests are sent as text with `Allow once`, `Always allow`, and `Deny` buttons, even when voice replies are enabled.
- Incoming text prompts get a temporary eye reaction while processing.
- OpenCode can request one Telegram emoji reaction by returning a hidden `[telegram_reaction: ...]` marker, which is removed before the user sees the reply.
- When saved sticker packs are available, eligible hidden reaction markers may be answered with a saved sticker reply instead of an emoji reaction.
- When saved sticker packs are available, explicit user requests for a sticker can be answered with a saved sticker reply through a hidden gateway marker.
- User emoji reactions to recent bot messages are sent back to OpenCode as feedback prompts.
- Telegram voice messages are transcribed and sent to OpenCode when voice mode is enabled.
- Voice replies replace text replies after voice prompts in `/voice on` mode and after text, photo, and voice prompts in `/voice all` mode, with text fallback if speech generation or sending fails.
Expand All @@ -56,6 +60,10 @@ OpenCode Remote is currently a Telegram gateway for OpenCode with text, image, a
- Album captions become the prompt text when present.
- Photos without captions use a default short reaction prompt.
- Temporary downloaded photo files are cleaned up after handling.
- Telegram sticker messages are sent to OpenCode with visual attachment context and safe sticker metadata.
- Static stickers use direct WebP image attachments. Video stickers use sampled preview sheets. Animated `.tgs` stickers use `lottie_convert.py` when available, with source-file fallback.
- Sticker visuals are cached under app-data cache storage and validated with `file_unique_id`, kind, dimensions, file size, and converter version.
- Cached sticker visuals can be summarized into short safe descriptions for the saved-sticker catalog used by future sticker replies.

## Voice Mode

Expand All @@ -71,6 +79,7 @@ OpenCode Remote is currently a Telegram gateway for OpenCode with text, image, a
- The bot ignores Telegram users outside the configured allowlist.
- Secrets are configured through private `.opencode-remote/config.json` files, not persisted settings.
- The selected active session is persisted as non-secret JSON state.
- Saved sticker packs persist only non-secret sticker identifiers and metadata.
- Telegram reaction API failures are best-effort warnings and do not block prompt delivery.
- Default tests mock Telegram and OpenCode; no live services are required for normal verification.

Expand Down
15 changes: 14 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

OpenCode Remote lets you use OpenCode from Telegram. It runs on your machine, connects to your local or remote OpenCode server, and forwards messages from one authorized Telegram user to OpenCode sessions.

This is a Telegram MVP with text prompts, photo prompts, OpenCode permission approvals, and opt-in voice input/replies. Model switching and multi-messenger support are not implemented yet.
This is a Telegram MVP with text prompts, photo prompts, sticker prompts/replies, OpenCode permission approvals, and opt-in voice input/replies. Model switching and multi-messenger support are not implemented yet.

See [Features](https://github.com/crankshift/opencode-remote/blob/main/FEATURES.md) for the full current capability list, [Contributing](https://github.com/crankshift/opencode-remote/blob/main/CONTRIBUTING.md) for contribution guidance, [Changelog](https://github.com/crankshift/opencode-remote/blob/main/CHANGELOG.md) for release notes, and [TODO](https://github.com/crankshift/opencode-remote/blob/main/TODO.md) for planned work.

Expand Down Expand Up @@ -166,6 +166,7 @@ The bot currently supports:
/stop Request stop for the active OpenCode session
/progress Show or set tool progress visibility: off, new, all, verbose
/voice Show or set voice mode
/stickers Manage saved sticker packs
/help Show available commands
```

Expand All @@ -179,6 +180,18 @@ When OpenCode requests permission during a prompt, the bot sends a text message

Telegram photo albums are handled as one OpenCode prompt when Telegram provides a shared `media_group_id`. The album caption becomes the prompt text. Separate text messages sent after an album are treated as separate prompts.

Telegram stickers are downloaded as visual prompt context for OpenCode. Static stickers are sent as WebP image attachments. Video stickers use `ffmpeg` to generate sampled preview sheets. Animated `.tgs` stickers use `lottie_convert.py` from python-lottie when it is available, with a source-file fallback if conversion is not installed. The gateway caches reusable sticker visuals under app-data cache storage, keyed by Telegram `file_unique_id` and safe visual metadata. When possible, cached sticker visuals are summarized into short saved-sticker descriptions so future sticker requests can use a compact text catalog instead of exposing cache paths or Telegram file identifiers.

Sticker pack commands:

```text
/stickers save
/stickers list
/stickers forget <pack_name>
```

Use `/stickers save` as a reply to a sticker to save that sticker pack for future sticker replies. `/stickers list` shows saved packs. `/stickers forget <pack_name>` removes a saved pack and its cached sticker previews. Incoming stickers from unsaved packs may also show a `Save pack` button. Once packs are saved, asking the bot to send a sticker lets OpenCode request one through the gateway without exposing Telegram file identifiers to the model. Saved sticker data is non-secret Telegram file metadata; bot tokens, user IDs, chat IDs, and raw download URLs are not persisted.

Voice commands:

```text
Expand Down
93 changes: 93 additions & 0 deletions docs/superpowers/plans/2026-05-28-telegram-sticker-support.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
# Telegram Sticker Support Implementation Plan

> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.

**Goal:** Add Telegram sticker visual understanding, sticker preview caching, saved sticker pack management, and optional sticker replies for OpenCode reaction markers.

**Architecture:** Keep all Telegram sticker specifics inside `src/adapters/telegram`. OpenCode continues receiving messenger-neutral prompt objects with file attachments and text context. Static stickers are attached directly as WebP images, while video/animated stickers use cached preview images.

**Tech Stack:** Node.js ESM, grammY, SQLite via `node:sqlite`, `ffmpeg` through injectable process wrappers, optional python-lottie `lottie_convert.py`, Vitest, Biome.

---

## File Structure

- Create `src/adapters/telegram/stickerCache.js` for app-data sticker cache paths, cache validation, and cached file cleanup.
- Create `src/adapters/telegram/stickerStore.js` for SQLite-backed saved packs, seen stickers, and cached preview index records.
- Create `src/adapters/telegram/stickerRenderer.js` for static/video/animated representation helpers with injectable conversion functions.
- Create `src/adapters/telegram/stickers.js` for Telegram sticker download, prompt text formatting, and attachment orchestration.
- Modify `src/adapters/telegram/bot.js` for `/stickers`, save callbacks, `message:sticker`, and sticker-vs-emoji reply behavior.
- Modify `src/core/commands/commands.js` so command registration, help text, docs, and tests share `/stickers` from the central command source.
- Modify `src/runtime/bootstrap.js` to open/close the sticker store and pass sticker dependencies into the Telegram bot.
- Modify docs: `README.md`, `FEATURES.md`, and `DEVELOPMENT.md`.
- Add tests under `tests/adapters`, `tests/core`, and `tests/runtime`.

## Tasks

### Task 1: Sticker Command Definition

- [ ] Add a failing assertion in `tests/core/commands.test.js` that `/stickers` appears in command definitions and help text.
- [ ] Run `pnpm test tests/core/commands.test.js` and verify the new assertion fails because `/stickers` is missing.
- [ ] Add `/stickers` to `src/core/commands/commands.js` with description `Manage saved sticker packs`.
- [ ] Run `pnpm test tests/core/commands.test.js` and verify it passes.

### Task 2: Sticker Cache Helpers

- [ ] Add failing tests in `tests/adapters/telegramStickerCache.test.js` for default cache directory, cache record validation, missing-file invalidation, metadata mismatch invalidation, and cached file cleanup.
- [ ] Run `pnpm test tests/adapters/telegramStickerCache.test.js` and verify failures are for missing exports.
- [ ] Implement `src/adapters/telegram/stickerCache.js` with `STICKER_CONVERTER_VERSION`, `getStickerCacheDir`, `isStickerCacheRecordUsable`, `cachedStickerFilePath`, and `removeCachedStickerFiles`.
- [ ] Run `pnpm test tests/adapters/telegramStickerCache.test.js` and verify it passes.

### Task 3: Sticker Store

- [ ] Add failing tests in `tests/adapters/telegramStickerStore.test.js` for saved-pack upsert, list, forget, match-by-emoji selection, fallback selection, seen-sticker metadata, cache records, and no secret fields.
- [ ] Run `pnpm test tests/adapters/telegramStickerStore.test.js` and verify failures are for missing exports.
- [ ] Implement `src/adapters/telegram/stickerStore.js` with `openTelegramStickerStore` and `createMemoryStickerStore` for tests and dependency injection.
- [ ] Run `pnpm test tests/adapters/telegramStickerStore.test.js` and verify it passes.

### Task 4: Sticker Renderer And Prompt Attachments

- [ ] Add failing tests in `tests/adapters/telegramStickers.test.js` for static WebP direct attachment, cached preview reuse, cache mismatch regeneration, thumbnail fallback, metadata prompt text, and temp cleanup.
- [ ] Run `pnpm test tests/adapters/telegramStickers.test.js` and verify failures are for missing exports.
- [ ] Implement `src/adapters/telegram/stickerRenderer.js` with static direct representation and injectable video/animated preview generation.
- [ ] Implement `src/adapters/telegram/stickers.js` with `downloadTelegramSticker`, `createStickerPrompt`, `formatStickerPromptText`, and `saveStickerPackFromSet`.
- [ ] Run `pnpm test tests/adapters/telegramStickers.test.js` and verify it passes.

### Task 5: Telegram Bot Sticker Input And Commands

- [ ] Add failing tests in `tests/adapters/telegramBot.test.js` for handler registration, static sticker prompt delivery, inline save-pack button, `/stickers save`, `/stickers list`, and `/stickers forget <pack_name>`.
- [ ] Run `pnpm test tests/adapters/telegramBot.test.js` and verify failures are for missing sticker behavior.
- [ ] Modify `src/adapters/telegram/bot.js` to accept sticker dependencies, register `message:sticker`, register `/stickers`, tokenize save callbacks, and call sticker helper/store methods.
- [ ] Run `pnpm test tests/adapters/telegramBot.test.js` and verify it passes.

### Task 6: Sticker Replies For Reaction Markers

- [ ] Add failing tests in `tests/adapters/telegramBot.test.js` for randomized sticker-vs-emoji selection, emoji-matched sticker preference, fallback to emoji reaction on sticker send failure, and unchanged incoming eye reaction.
- [ ] Run `pnpm test tests/adapters/telegramBot.test.js` and verify failures are for missing reply selection behavior.
- [ ] Modify `src/adapters/telegram/bot.js` so parsed reaction markers use `maybeSendStickerReaction` after visible replies, with injectable randomness for deterministic tests.
- [ ] Run `pnpm test tests/adapters/telegramBot.test.js` and verify it passes.

### Task 7: Runtime Wiring

- [ ] Add failing tests in `tests/runtime/bootstrap.test.js` that runtime opens a sticker store, passes it to `createTelegramBot`, and closes it on shutdown.
- [ ] Run `pnpm test tests/runtime/bootstrap.test.js` and verify failures are for missing runtime wiring.
- [ ] Modify `src/runtime/bootstrap.js` to create the sticker store and close it during shutdown.
- [ ] Run `pnpm test tests/runtime/bootstrap.test.js` and verify it passes.

### Task 8: Docs

- [ ] Update `README.md` to document sticker messages, inline save, and `/stickers save|list|forget`.
- [ ] Update `FEATURES.md` to list shipped sticker support and app-data sticker cache behavior.
- [ ] Update `DEVELOPMENT.md` to document sticker cache/state paths and mocked tests.
- [ ] Run `pnpm run lint` to verify docs formatting.

### Task 9: Final Verification

- [ ] Run `pnpm test`.
- [ ] Run `pnpm run lint`.
- [ ] Run `pnpm run check`.
- [ ] Inspect `git status --short` and `git diff` for unintended files, secrets, raw Telegram payloads, bot tokens, user IDs, and local machine paths.

## Self-Review

This plan covers all issue 20 acceptance criteria: sticker visual attachments, static direct handling, animated/video preview caching, saved pack identifiers, unchanged eye reactions, randomized sticker replies, text-only permission prompts, Telegram adapter boundaries, and mocked default tests. It avoids speculative multi-user behavior and keeps sticker persistence non-secret.
Loading