@llblab/pi-telegram 0.11.2 → 0.13.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/AGENTS.md +20 -15
- package/BACKLOG.md +1 -11
- package/CHANGELOG.md +41 -1
- package/README.md +15 -41
- package/api/inbound.ts +14 -0
- package/api/keyboard.ts +10 -0
- package/api/outbound.ts +11 -0
- package/api/sections.ts +17 -0
- package/api/updates.ts +11 -0
- package/api/voice.ts +24 -0
- package/docs/README.md +7 -5
- package/docs/architecture.md +162 -226
- package/docs/callback-namespaces.md +3 -3
- package/docs/command-templates.md +18 -16
- package/docs/{inbound-handlers.md → inbound.md} +14 -11
- package/docs/locks.md +3 -3
- package/docs/{outbound-handlers.md → outbound.md} +14 -11
- package/docs/public-api.md +420 -0
- package/docs/{extension-sections.md → sections.md} +34 -30
- package/docs/ui-style.md +165 -0
- package/docs/{external-handlers.md → updates.md} +33 -31
- package/docs/voice.md +27 -19
- package/index.ts +88 -242
- package/lib/bindings.ts +299 -0
- package/lib/command-templates.ts +249 -60
- package/lib/commands.ts +114 -1
- package/lib/config.ts +44 -4
- package/lib/{inbound-handlers.ts → inbound.ts} +31 -21
- package/lib/lifecycle.ts +41 -6
- package/lib/locks.ts +4 -1
- package/lib/menu-model.ts +3 -3
- package/lib/menu-queue.ts +1 -1
- package/lib/menu-settings.ts +21 -10
- package/lib/menu-status.ts +1 -1
- package/lib/menu.ts +1 -1
- package/lib/outbound-buttons.ts +226 -0
- package/lib/outbound-markup.ts +357 -0
- package/lib/outbound-voice.ts +263 -0
- package/lib/outbound.ts +908 -0
- package/lib/polling.ts +4 -3
- package/lib/preview.ts +2 -2
- package/lib/queue.ts +3 -0
- package/lib/replies.ts +4 -1
- package/lib/routing.ts +44 -3
- package/lib/{extension-sections.ts → sections.ts} +37 -8
- package/lib/status.ts +13 -0
- package/lib/{api.ts → telegram-api.ts} +4 -4
- package/lib/text-groups.ts +3 -2
- package/lib/updates.ts +121 -1
- package/lib/voice.ts +67 -21
- package/package.json +13 -3
- package/lib/external-handlers.ts +0 -166
- package/lib/outbound-handlers.ts +0 -1663
|
@@ -33,14 +33,14 @@ There is no portable `command` field. The command is derived from `template`: af
|
|
|
33
33
|
Common object fields:
|
|
34
34
|
|
|
35
35
|
- `label`: Optional human label for diagnostics and parallel branch reports.
|
|
36
|
-
- `
|
|
36
|
+
- `parallel`: Optional boolean execution flag for array templates. Default is sequential execution; `true` runs children concurrently when the host execution layer supports branch fanout.
|
|
37
|
+
- `when`: Optional boolean or condition string. Falsy values skip the node. String forms may reference a flag name, `!flag`, or a placeholder expression such as `{flag?yes:}`.
|
|
37
38
|
- `args`: Optional placeholder declarations. Untyped names remain valid; compact typed forms such as `file:path`, `timeout:int`, `speed:number`, `dry_run:bool`, `prompts:array`, and `mode:enum(check,fix)` are valid when the host supports typed tool schemas. Defaults belong in `defaults` or inline placeholder defaults; hosts may normalize interactive shorthand such as `timeout:int=60000` before persistence.
|
|
38
39
|
- `defaults`: Placeholder default values by name.
|
|
39
|
-
- `timeout`: Optional execution timeout in milliseconds. Omit it, or set `0`, to leave the command unbounded. Set an explicit positive timeout when a tool must fail closed instead of waiting indefinitely.
|
|
40
|
-
- `delay`: Optional wait in milliseconds before starting this node. Default is no delay.
|
|
40
|
+
- `timeout`: Optional execution timeout in milliseconds, as a number or placeholder-resolved string. Omit it, or set `0`, to leave the command unbounded. Set an explicit positive timeout when a tool must fail closed instead of waiting indefinitely.
|
|
41
|
+
- `delay`: Optional wait in milliseconds before starting this node, as a number or placeholder-resolved string. Default is no delay.
|
|
41
42
|
- `output`: Optional result selector. Default is `"stdout"`; runtime values such as `"ogg"` are valid.
|
|
42
|
-
- `retry`: Optional max attempts including the first. Default is `1`.
|
|
43
|
-
- `critical`: Optional boolean. Backward-compatible alias for `failure: "root"`.
|
|
43
|
+
- `retry`: Optional max attempts including the first, as a number or placeholder-resolved string. Default is `1`.
|
|
44
44
|
- `failure`: Optional failure propagation scope: `continue`, `branch`, or `root`. Default is `continue`.
|
|
45
45
|
- `recover`: Optional command template run between failed retry attempts. Recovery output is ignored; recovery failure stops retries.
|
|
46
46
|
- `template`: Required command string or ordered composition array.
|
|
@@ -68,6 +68,8 @@ Supported forms:
|
|
|
68
68
|
| `{name}` | Required value from runtime values or `defaults` |
|
|
69
69
|
| `{name=default}` | Inline default when no value is provided |
|
|
70
70
|
| `{items[index]}` | Array item selected by literal or repeat index |
|
|
71
|
+
| `{value??fallback}` | Fallback when the value is absent or falsy |
|
|
72
|
+
| `{flag?yes:no}` | Conditional text selected by flag truthiness |
|
|
71
73
|
|
|
72
74
|
Resolution order is runtime values → `defaults` → inline default → error. Default values that are themselves a single placeholder, such as `{prompt}` resolving to `{prompts[index]}`, are resolved recursively with a small depth guard. A repeat node may set `repeat` to `{items.length}` when an array arg should determine fanout width.
|
|
73
75
|
|
|
@@ -127,8 +129,8 @@ template="echo 'literal words' {text}"
|
|
|
127
129
|
|
|
128
130
|
Composition rules:
|
|
129
131
|
|
|
130
|
-
- Execute leaves in order
|
|
131
|
-
- Execute child templates concurrently when `
|
|
132
|
+
- Execute leaves in order by default
|
|
133
|
+
- Execute child templates concurrently when `parallel` is `true`
|
|
132
134
|
- Parallel composition uses soft-quorum semantics by default: failed children are reported as degraded branches unless failure propagation escalates
|
|
133
135
|
- Non-critical failures are recorded and execution continues, while `failure: "branch"` stops the current branch and `failure: "root"` aborts the root composition
|
|
134
136
|
- Treat the whole composition as one handler for selector matching and fallback
|
|
@@ -163,7 +165,7 @@ Composition rules:
|
|
|
163
165
|
|
|
164
166
|
```json
|
|
165
167
|
{
|
|
166
|
-
"
|
|
168
|
+
"parallel": true,
|
|
167
169
|
"repeat": 8,
|
|
168
170
|
"template": "render page{_(index+1)}.html --prev page{_(prev+1)}.html --next page{_(next+1)}.html --zero page{_index}.html"
|
|
169
171
|
}
|
|
@@ -198,7 +200,7 @@ Parallel nodes use the same object shape. Flags come first and `template` stays
|
|
|
198
200
|
"template": [
|
|
199
201
|
"prepare {out_dir}",
|
|
200
202
|
{
|
|
201
|
-
"
|
|
203
|
+
"parallel": true,
|
|
202
204
|
"template": [
|
|
203
205
|
{
|
|
204
206
|
"label": "gpt-5.5",
|
|
@@ -232,7 +234,7 @@ exit: 1
|
|
|
232
234
|
stderr: provider balance exhausted
|
|
233
235
|
```
|
|
234
236
|
|
|
235
|
-
|
|
237
|
+
Use `template: [...]` for ordered composition. Older local `pipe` aliases are not part of the 0.13.0 command-template standard.
|
|
236
238
|
|
|
237
239
|
## Fail-Open Default Policy
|
|
238
240
|
|
|
@@ -250,7 +252,7 @@ Use `failure` when a node should stop more aggressively:
|
|
|
250
252
|
|
|
251
253
|
```json
|
|
252
254
|
{
|
|
253
|
-
"
|
|
255
|
+
"parallel": true,
|
|
254
256
|
"template": [
|
|
255
257
|
{
|
|
256
258
|
"label": "agent-a",
|
|
@@ -276,7 +278,7 @@ Use `failure` when a node should stop more aggressively:
|
|
|
276
278
|
|
|
277
279
|
If `agent-a-validate` fails, `agent-a-push` is skipped, `agent-b` can still finish, and the parallel join reports degraded branch coverage.
|
|
278
280
|
|
|
279
|
-
`
|
|
281
|
+
Use `failure: "root"` to abort the root composition. Older local `critical: true` shapes are not part of the 0.13.0 command-template standard.
|
|
280
282
|
|
|
281
283
|
## Retry
|
|
282
284
|
|
|
@@ -334,13 +336,13 @@ The standard uses a single `template` field that grows with the user's needs:
|
|
|
334
336
|
string → leaf command
|
|
335
337
|
string[] → sequential composition
|
|
336
338
|
{ template } → leaf command object
|
|
337
|
-
{
|
|
338
|
-
{
|
|
339
|
+
{ parallel, template } → parallel subtree
|
|
340
|
+
{ parallel, when, args, defaults, delay, retry, failure, recover, output, template } → full node
|
|
339
341
|
```
|
|
340
342
|
|
|
341
|
-
Start with a string. Add composition when needed. Add `
|
|
343
|
+
Start with a string. Add composition when needed. Add `parallel: true` when independent work can run concurrently. Add `when` for conditional nodes. Add delay when launch pacing matters. Add retry when flaky. Add `failure` when propagation scope matters. Add `recover` when a retried node needs cleanup before another attempt. Same contract, growing capability, no dead weight.
|
|
342
344
|
|
|
343
|
-
`
|
|
345
|
+
`parallel: true` is the synchronous fanout shape. Saved JSON recipes and detached lifecycle concerns such as logs, cancellation, and durable state belong to host-specific recipe/async-run standards, not to command templates.
|
|
344
346
|
|
|
345
347
|
## Trust Boundary
|
|
346
348
|
|
|
@@ -49,7 +49,7 @@ Legacy `telegram.json` files may still define `attachmentHandlers` for media/fil
|
|
|
49
49
|
|
|
50
50
|
At runtime, `attachmentHandlers` is appended after `inboundHandlers`. Existing configs continue to work, while new configs should use `inboundHandlers`.
|
|
51
51
|
|
|
52
|
-
Handlers match by optional `type`, `mime`, or `match`. `mime` and `type` are independent selectors: if `mime` is present, `type` is not required. Wildcards such as `audio/*` or `text/*` are accepted. Each matching handler must provide `template`; a string is one command, and an array is ordered composition. Top-level `args` and `defaults` apply to composed steps unless a step defines private values. The command-template default timeout applies automatically.
|
|
52
|
+
Handlers match by optional `type`, `mime`, or `match`. `mime` and `type` are independent selectors: if `mime` is present, `type` is not required. Wildcards such as `audio/*` or `text/*` are accepted. Each matching handler must provide `template`; a string is one command, and an array is ordered composition. Top-level `args` and `defaults` apply to composed steps unless a step defines private values. The command-template default timeout applies automatically. Use `template: [...]` for composition; the old local `pipe` alias is removed in 0.13.0.
|
|
53
53
|
|
|
54
54
|
`defaults` may provide additional placeholder values such as `{lang}` or `{model}`. `args` is only a string-array declaration of supported placeholders; defaults belong in `defaults` or inline placeholders such as `{lang=ru}`. Examples prefer explicit flag-style CLIs such as `--file {file}` and `--lang {lang=ru}` for readability, but positional forms such as `/path/to/stt {file} {lang=ru} {model=voxtral-mini-latest}` are equally valid when the target script supports them.
|
|
55
55
|
|
|
@@ -88,9 +88,9 @@ If a matching handler fails with a non-zero exit code, the runtime records diagn
|
|
|
88
88
|
|
|
89
89
|
## Programmatic Inbound Handlers And STT Fallbacks
|
|
90
90
|
|
|
91
|
-
Extensions can register programmatic inbound handlers with `registerTelegramInboundHandler(kind, handler)` from `@llblab/pi-telegram/
|
|
91
|
+
Extensions can register programmatic inbound handlers with `registerTelegramInboundHandler(kind, handler)` from `@llblab/pi-telegram/inbound`. This is the code-level counterpart to configured `inboundHandlers`; use it for extension-owned transformations that are not voice-specific.
|
|
92
92
|
|
|
93
|
-
Voice extensions can register STT providers with `registerTelegramVoiceTranscriptionProvider()` from `@llblab/pi-telegram/
|
|
93
|
+
Voice extensions can register STT providers with `registerTelegramVoiceTranscriptionProvider()` from `@llblab/pi-telegram/voice`. This is the zero-config extension path for voice/audio input: a companion extension can transcribe Telegram voice notes without requiring the operator to write an `inboundHandlers` command template.
|
|
94
94
|
|
|
95
95
|
Priority stays explicit and predictable:
|
|
96
96
|
|
|
@@ -101,14 +101,17 @@ Priority stays explicit and predictable:
|
|
|
101
101
|
5. built-in text-file fallback for text attachments
|
|
102
102
|
|
|
103
103
|
```ts
|
|
104
|
-
import { registerTelegramInboundHandler } from "@llblab/pi-telegram/
|
|
105
|
-
import { registerTelegramVoiceTranscriptionProvider } from "@llblab/pi-telegram/
|
|
106
|
-
|
|
107
|
-
const disposeInbound = registerTelegramInboundHandler(
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
104
|
+
import { registerTelegramInboundHandler } from "@llblab/pi-telegram/inbound";
|
|
105
|
+
import { registerTelegramVoiceTranscriptionProvider } from "@llblab/pi-telegram/voice";
|
|
106
|
+
|
|
107
|
+
const disposeInbound = registerTelegramInboundHandler(
|
|
108
|
+
"document",
|
|
109
|
+
async ({ file }) => {
|
|
110
|
+
if (!file?.mimeType?.includes("pdf")) return undefined;
|
|
111
|
+
const text = await extractPdf(file.path);
|
|
112
|
+
return text || undefined;
|
|
113
|
+
},
|
|
114
|
+
);
|
|
112
115
|
|
|
113
116
|
const dispose = registerTelegramVoiceTranscriptionProvider(
|
|
114
117
|
async (file) => {
|
package/docs/locks.md
CHANGED
|
@@ -58,13 +58,13 @@ During a user-initiated start/connect event, an extension should:
|
|
|
58
58
|
1. Read its lock entry
|
|
59
59
|
2. If `pid` is stale, replace the entry
|
|
60
60
|
3. If `pid` and `cwd` match the current pi instance, refresh or keep the entry
|
|
61
|
-
4. If a live
|
|
61
|
+
4. If a live polling owner exists, ask interactively whether to move singleton ownership here
|
|
62
62
|
|
|
63
63
|
## Acquisition timing
|
|
64
64
|
|
|
65
65
|
Lock writes must be caused by an explicit user-initiated runtime event, such as a start/connect command or a confirmed takeover prompt.
|
|
66
66
|
|
|
67
|
-
Extension initialization and session-start hooks may read `locks.json`, update local status, install ownership watchers, and resume local work when the existing lock already points at the current `pid`/`cwd`. After a full process restart, a session-start hook may replace a stale lock from the same `cwd` to restore explicitly requested ownership. They must not create ownership from an inactive lock, take over a live
|
|
67
|
+
Extension initialization and session-start hooks may read `locks.json`, update local status, install ownership watchers, and resume local work when the existing lock already points at the current `pid`/`cwd`. After a full process restart, a session-start hook may replace a stale lock from the same `cwd` to restore explicitly requested ownership. They must not create ownership from an inactive lock, take over a live polling owner, or replace a stale lock from another directory by themselves. Such locks should stay visible as state until the user runs the start/connect command. Session replacement should suspend local runtime work and ownership watchers without releasing the lock, so the next session in the same `pid`/`cwd` can resume from explicit ownership.
|
|
68
68
|
|
|
69
69
|
## Optional fields
|
|
70
70
|
|
|
@@ -105,7 +105,7 @@ Extensions may prefix those states with their own compact name, such as `wakeup
|
|
|
105
105
|
Start/connect commands should make singleton moves easy:
|
|
106
106
|
|
|
107
107
|
1. If no live owner exists, take ownership without an extra prompt
|
|
108
|
-
2. If a live
|
|
108
|
+
2. If a live polling owner exists, ask whether to move singleton ownership to this pi instance
|
|
109
109
|
3. On confirmation, write the current `{ "pid": ..., "cwd": ... }` to this extension's key in `locks.json`
|
|
110
110
|
4. The previous owner must notice that `locks.json` no longer points at its own `pid`/`cwd` and stop local runtime work without deleting the new lock
|
|
111
111
|
|
|
@@ -13,12 +13,12 @@ An outbound handler is selected by `type`. Text replies and assistant markup map
|
|
|
13
13
|
| Source | Handler | Action |
|
|
14
14
|
| ----------------- | ----------------------------- | ----------------------- |
|
|
15
15
|
| Final text | `outboundHandlers[type=text]` | Transform before render |
|
|
16
|
-
| `telegram_voice` | Voice pipeline | OGG/Opus `sendVoice`
|
|
16
|
+
| `telegram_voice` | Voice pipeline | OGG/Opus `sendVoice` |
|
|
17
17
|
| `telegram_button` | Built-in | Attach inline button |
|
|
18
18
|
|
|
19
19
|
The voice pipeline is detailed below: configured `type: "voice"` handlers first, then programmatic handlers, then registered synthesis providers.
|
|
20
20
|
|
|
21
|
-
Configured text handlers provide `template`. A string is one command; an array is ordered composition. Top-level `args` and `defaults` apply to all composed steps unless a step defines private values. The command-template default timeout applies automatically.
|
|
21
|
+
Configured text handlers provide `template`. A string is one command; an array is ordered composition. Top-level `args` and `defaults` apply to all composed steps unless a step defines private values. The command-template default timeout applies automatically. Use `template: [...]` for composition; the old local `pipe` alias is removed in 0.13.0.
|
|
22
22
|
|
|
23
23
|
## Text Handler Config
|
|
24
24
|
|
|
@@ -60,24 +60,27 @@ Voice replies use one fallback pipeline:
|
|
|
60
60
|
|
|
61
61
|
1. configured `outboundHandlers` with `type: "voice"` in `telegram.json` order
|
|
62
62
|
2. programmatic `registerTelegramOutboundHandler("voice", ...)` handlers
|
|
63
|
-
3. registered voice synthesis providers from `@llblab/pi-telegram/
|
|
63
|
+
3. registered voice synthesis providers from `@llblab/pi-telegram/voice`
|
|
64
64
|
|
|
65
65
|
This makes provider extensions a zero-config convenience without overriding explicit operator-owned `telegram.json` handlers. If several synthesis providers are registered, they are tried in registration order; the first provider that returns a valid `.ogg`/`.opus` artifact handles the reply. Returning `undefined` passes to the next provider, while thrown errors or invalid files are recorded before the next fallback is tried.
|
|
66
66
|
|
|
67
67
|
## Voice Synthesis Provider API
|
|
68
68
|
|
|
69
|
-
Voice replies can be delivered by synthesis providers registered through `@llblab/pi-telegram/
|
|
69
|
+
Voice replies can be delivered by synthesis providers registered through `@llblab/pi-telegram/voice`:
|
|
70
70
|
|
|
71
71
|
```ts
|
|
72
|
-
import { registerTelegramVoiceSynthesisProvider } from "@llblab/pi-telegram/
|
|
73
|
-
|
|
74
|
-
const dispose = registerTelegramVoiceSynthesisProvider(
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
}
|
|
72
|
+
import { registerTelegramVoiceSynthesisProvider } from "@llblab/pi-telegram/voice";
|
|
73
|
+
|
|
74
|
+
const dispose = registerTelegramVoiceSynthesisProvider(
|
|
75
|
+
async (text, options) => {
|
|
76
|
+
const audioPath = await synthesizeToOggOpus(text, options);
|
|
77
|
+
return { audioPath, transcriptText: text };
|
|
78
|
+
},
|
|
79
|
+
{ id: "my-extension/tts" },
|
|
80
|
+
);
|
|
78
81
|
```
|
|
79
82
|
|
|
80
|
-
Synthesis providers receive the extracted `telegram_voice` text plus optional `lang`/`rate` hints.
|
|
83
|
+
Synthesis providers receive the extracted `telegram_voice` text plus optional `lang`/`rate` hints. Stable registrations pass a durable `id`; omitted ids remain a compatibility path for older providers. Providers own translation, TTS, speech rewriting, transcript choice, and OGG/Opus conversion. The bridge validates that the returned file ends in `.ogg` or `.opus`, sends it through Telegram `sendVoice`, and falls back to planned text if delivery fails before any visible text was delivered. Providers run after configured and programmatic voice handlers in the priority chain above.
|
|
81
84
|
|
|
82
85
|
## Voice Markup
|
|
83
86
|
|
|
@@ -0,0 +1,420 @@
|
|
|
1
|
+
# Public API
|
|
2
|
+
|
|
3
|
+
`pi-telegram` is both a π extension and a small Telegram platform for companion extensions. This document defines the stable public surface. Everything outside this document is implementation detail unless another focused doc explicitly marks it stable.
|
|
4
|
+
|
|
5
|
+
## Stability Levels
|
|
6
|
+
|
|
7
|
+
- **Stable:** documented here and covered by compatibility expectations.
|
|
8
|
+
- **Advanced stable:** public for extension authors, but lower-level; prefer higher-level APIs when possible.
|
|
9
|
+
- **Compatibility:** older import/config paths that remain supported but should not be used for new code.
|
|
10
|
+
- **Internal:** exported from source for tests or domain reuse, but not a compatibility promise.
|
|
11
|
+
|
|
12
|
+
## Package Entrypoints
|
|
13
|
+
|
|
14
|
+
Preferred public imports:
|
|
15
|
+
|
|
16
|
+
```ts
|
|
17
|
+
import telegram from "@llblab/pi-telegram";
|
|
18
|
+
import { registerTelegramSection } from "@llblab/pi-telegram/sections";
|
|
19
|
+
import { registerTelegramUpdateHandler } from "@llblab/pi-telegram/updates";
|
|
20
|
+
import { registerTelegramInboundHandler } from "@llblab/pi-telegram/inbound";
|
|
21
|
+
import { registerTelegramOutboundHandler } from "@llblab/pi-telegram/outbound";
|
|
22
|
+
import {
|
|
23
|
+
registerTelegramVoiceSynthesisProvider,
|
|
24
|
+
registerTelegramVoiceTranscriptionProvider,
|
|
25
|
+
} from "@llblab/pi-telegram/voice";
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
`0.12.0` intentionally removes the published `@llblab/pi-telegram/lib/*.ts` compatibility wildcard. Integrations should use the public API domain subpaths above. Package exports point at `/api/*.ts` membranes that re-export only stable companion-extension symbols; implementation modules under `lib/` remain package-private. See [Public API Smoke Examples](#public-api-smoke-examples) below for minimal companion-extension patterns that avoid implementation imports.
|
|
29
|
+
|
|
30
|
+
## User-Facing API
|
|
31
|
+
|
|
32
|
+
### π commands
|
|
33
|
+
|
|
34
|
+
Stable commands inside π:
|
|
35
|
+
|
|
36
|
+
- `/telegram-setup` — configure/update the bot token.
|
|
37
|
+
- `/telegram-connect` — start polling in the current session and acquire ownership.
|
|
38
|
+
- `/telegram-disconnect` — stop polling and release ownership.
|
|
39
|
+
- `/telegram-status` — show connection, polling, execution, queue, and recent event diagnostics.
|
|
40
|
+
|
|
41
|
+
### Telegram commands
|
|
42
|
+
|
|
43
|
+
Stable commands inside the paired Telegram DM:
|
|
44
|
+
|
|
45
|
+
- `/start` — pair when needed and open the main application menu.
|
|
46
|
+
- `/compact` — open confirmation and compact when idle.
|
|
47
|
+
- `/next` — dispatch the next queued turn, aborting active work first when needed.
|
|
48
|
+
- `/continue` — enqueue a priority `continue` prompt.
|
|
49
|
+
- `/abort` — abort active Telegram-owned work and keep the queue.
|
|
50
|
+
- `/stop` — abort active Telegram-owned work and clear waiting Telegram queue items.
|
|
51
|
+
|
|
52
|
+
Hidden compatibility shortcuts may open sections directly: `/help`, `/status`, `/model`, `/thinking`, `/queue`, and `/settings`.
|
|
53
|
+
|
|
54
|
+
### Tools and assistant-authored actions
|
|
55
|
+
|
|
56
|
+
- `telegram_attach(paths)` is the stable artifact delivery tool for generated files.
|
|
57
|
+
- `telegram_voice` hidden comments request Telegram-native voice delivery.
|
|
58
|
+
- `telegram_button` hidden comments create inline buttons whose taps enqueue prompts.
|
|
59
|
+
|
|
60
|
+
See [Outbound Handlers](./outbound.md) for exact markup forms.
|
|
61
|
+
|
|
62
|
+
## Configuration API
|
|
63
|
+
|
|
64
|
+
Configuration lives in `~/.pi/agent/telegram.json` unless `PI_CODING_AGENT_DIR` changes the agent root.
|
|
65
|
+
|
|
66
|
+
Stable config keys:
|
|
67
|
+
|
|
68
|
+
```ts
|
|
69
|
+
interface TelegramConfig {
|
|
70
|
+
botToken?: string;
|
|
71
|
+
botUsername?: string; // runtime-managed
|
|
72
|
+
botId?: number; // runtime-managed
|
|
73
|
+
allowedUserId?: number;
|
|
74
|
+
lastUpdateId?: number; // runtime-managed
|
|
75
|
+
proactivePush?: boolean;
|
|
76
|
+
inboundHandlers?: TelegramInboundHandlerConfig[];
|
|
77
|
+
attachmentHandlers?: TelegramInboundHandlerConfig[]; // compatibility alias
|
|
78
|
+
outboundHandlers?: TelegramOutboundHandlerConfig[];
|
|
79
|
+
voice?: {
|
|
80
|
+
replyMode?: "manual" | "mirror" | "always";
|
|
81
|
+
sendTranscript?: boolean;
|
|
82
|
+
};
|
|
83
|
+
time?: {
|
|
84
|
+
injectionMode?: "hidden" | "always" | "interval";
|
|
85
|
+
interval?: number;
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
Hidden/default semantics are represented by absence:
|
|
91
|
+
|
|
92
|
+
- Voice Reply `hidden`: no `voice.replyMode` key is persisted.
|
|
93
|
+
- Time Injection `hidden`: no `time.injectionMode` key is persisted; if `time` becomes empty, the whole `time` object may be omitted.
|
|
94
|
+
|
|
95
|
+
Environment variables are stable only where documented in the README: bot-token bootstrap, proxy behavior, agent root, and inbound/outbound file size limits.
|
|
96
|
+
|
|
97
|
+
## Programmatic API Matrix
|
|
98
|
+
|
|
99
|
+
High-level stable APIs:
|
|
100
|
+
|
|
101
|
+
- `registerTelegramSection()`
|
|
102
|
+
- Identity: required `id`.
|
|
103
|
+
- Purpose: managed menu/settings UI surfaces.
|
|
104
|
+
- `registerTelegramVoiceTranscriptionProvider()`
|
|
105
|
+
- Identity: required stable `id` for new code.
|
|
106
|
+
- Purpose: STT fallback for voice/audio input.
|
|
107
|
+
- `registerTelegramVoiceSynthesisProvider()`
|
|
108
|
+
- Identity: required stable `id` for new code.
|
|
109
|
+
- Purpose: TTS fallback for Telegram voice replies.
|
|
110
|
+
|
|
111
|
+
Low-level stable buses:
|
|
112
|
+
|
|
113
|
+
- `registerTelegramUpdateHandler()`
|
|
114
|
+
- Identity: no id.
|
|
115
|
+
- Purpose: observe or consume raw Telegram updates before default routing.
|
|
116
|
+
- `registerTelegramInboundHandler()`
|
|
117
|
+
- Identity: no id.
|
|
118
|
+
- Purpose: generic Telegram-to-π transforms.
|
|
119
|
+
- `registerTelegramOutboundHandler()`
|
|
120
|
+
- Identity: no id.
|
|
121
|
+
- Purpose: generic final-reply transforms or voice command fallbacks.
|
|
122
|
+
|
|
123
|
+
Advanced stable diagnostics:
|
|
124
|
+
|
|
125
|
+
- `recordTelegramRuntimeEvent()`
|
|
126
|
+
- Identity: caller supplies category.
|
|
127
|
+
- Purpose: surface companion diagnostics in `/telegram-status`.
|
|
128
|
+
|
|
129
|
+
All registration APIs return a disposer. Companion extensions should call disposers on shutdown and re-register on session start when they recreate runtime state. Low-level bus APIs intentionally avoid ids and run in registration order. High-level provider/UI APIs require stable identity in their public contract so diagnostics, replacement, and cleanup are understandable. Generated voice-provider ids remain a temporary compatibility path where documented.
|
|
130
|
+
|
|
131
|
+
## Sections
|
|
132
|
+
|
|
133
|
+
Import from `@llblab/pi-telegram/sections`.
|
|
134
|
+
|
|
135
|
+
```ts
|
|
136
|
+
const unregister = registerTelegramSection({
|
|
137
|
+
id: "@scope/my-extension",
|
|
138
|
+
label: "🧩 My extension",
|
|
139
|
+
order: 10,
|
|
140
|
+
render: async (ctx) => ({
|
|
141
|
+
text: "<b>My extension</b>",
|
|
142
|
+
parseMode: "html",
|
|
143
|
+
replyMarkup: {
|
|
144
|
+
inline_keyboard: [
|
|
145
|
+
[{ text: "▶️ Run", callback_data: ctx.callbackData("run") }],
|
|
146
|
+
],
|
|
147
|
+
},
|
|
148
|
+
}),
|
|
149
|
+
handleCallback: async (ctx) => {
|
|
150
|
+
if (ctx.action !== "run") return "pass";
|
|
151
|
+
await ctx.enqueuePrompt("Run my extension workflow.");
|
|
152
|
+
await ctx.answerCallback("Queued");
|
|
153
|
+
return "handled";
|
|
154
|
+
},
|
|
155
|
+
});
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
Contract:
|
|
159
|
+
|
|
160
|
+
- `id` is unique per active registry. Duplicate ids are rejected.
|
|
161
|
+
- `ctx.callbackData(action, payload?)` builds compact `section:` callbacks and validates Telegram's 64-byte limit.
|
|
162
|
+
- `ctx.edit()` auto-prepends the correct Back/Main-menu row. `ctx.open()` sends a standalone chat message without auto-navigation.
|
|
163
|
+
- Section errors are isolated and surfaced as callback popups/diagnostics.
|
|
164
|
+
|
|
165
|
+
Full behavior: [Extension Sections](./sections.md).
|
|
166
|
+
|
|
167
|
+
## Updates
|
|
168
|
+
|
|
169
|
+
Import from `@llblab/pi-telegram/updates`.
|
|
170
|
+
|
|
171
|
+
```ts
|
|
172
|
+
const off = registerTelegramUpdateHandler(async (update) => {
|
|
173
|
+
const data = (update as { callback_query?: { data?: string } }).callback_query
|
|
174
|
+
?.data;
|
|
175
|
+
if (!data?.startsWith("myext:")) return "pass";
|
|
176
|
+
await handleMyCallback(data);
|
|
177
|
+
return "consume";
|
|
178
|
+
});
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
Use this as a low-level escape hatch. Prefer sections for menu-integrated UI.
|
|
182
|
+
|
|
183
|
+
Full behavior: [Updates](./updates.md).
|
|
184
|
+
|
|
185
|
+
## Inbound
|
|
186
|
+
|
|
187
|
+
Import from `@llblab/pi-telegram/inbound`.
|
|
188
|
+
|
|
189
|
+
```ts
|
|
190
|
+
const off = registerTelegramInboundHandler("document", async ({ file }) => {
|
|
191
|
+
if (!file?.mimeType?.includes("pdf")) return undefined;
|
|
192
|
+
return await extractPdfText(file.path);
|
|
193
|
+
});
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
Priority order:
|
|
197
|
+
|
|
198
|
+
1. configured `inboundHandlers`
|
|
199
|
+
2. compatibility `attachmentHandlers`
|
|
200
|
+
3. programmatic inbound handlers
|
|
201
|
+
4. voice transcription providers
|
|
202
|
+
5. built-in text-file fallback
|
|
203
|
+
|
|
204
|
+
Full behavior: [Inbound Handlers](./inbound.md).
|
|
205
|
+
|
|
206
|
+
## Outbound
|
|
207
|
+
|
|
208
|
+
Import from `@llblab/pi-telegram/outbound`.
|
|
209
|
+
|
|
210
|
+
```ts
|
|
211
|
+
const off = registerTelegramOutboundHandler("text", async (text) => {
|
|
212
|
+
return await rewriteFinalText(text);
|
|
213
|
+
});
|
|
214
|
+
```
|
|
215
|
+
|
|
216
|
+
Programmatic outbound handlers are fallbacks/transformers behind operator-owned `telegram.json` configuration. Voice delivery priority is configured voice handlers, then programmatic `voice` handlers, then synthesis providers.
|
|
217
|
+
|
|
218
|
+
Full behavior: [Outbound Handlers](./outbound.md).
|
|
219
|
+
|
|
220
|
+
## Voice Providers
|
|
221
|
+
|
|
222
|
+
Import from `@llblab/pi-telegram/voice`.
|
|
223
|
+
|
|
224
|
+
```ts
|
|
225
|
+
const offStt = registerTelegramVoiceTranscriptionProvider(
|
|
226
|
+
async (file) => {
|
|
227
|
+
if (file.kind !== "voice" && file.kind !== "audio") return undefined;
|
|
228
|
+
return { text: await transcribe(file.path) };
|
|
229
|
+
},
|
|
230
|
+
{ id: "@scope/my-extension/stt" },
|
|
231
|
+
);
|
|
232
|
+
|
|
233
|
+
const offTts = registerTelegramVoiceSynthesisProvider(
|
|
234
|
+
async (text, options) => {
|
|
235
|
+
const audioPath = await synthesizeOggOpus(text, options);
|
|
236
|
+
return getTelegramVoiceSendTranscript(getCurrentTelegramConfigView())
|
|
237
|
+
? { audioPath, transcriptText: text }
|
|
238
|
+
: { audioPath };
|
|
239
|
+
},
|
|
240
|
+
{ id: "@scope/my-extension/tts" },
|
|
241
|
+
);
|
|
242
|
+
```
|
|
243
|
+
|
|
244
|
+
Stable voice-provider registrations pass a durable `id`. Omitting `id` is a compatibility path for older providers and receives a generated session-local id. Providers return `undefined` to pass. TTS providers must return `.ogg` or `.opus` files for native Telegram voice notes. `voice.sendTranscript` is the bridge-owned transcript preference; providers that expose captions should gate `transcriptText` with `getTelegramVoiceSendTranscript(config)` instead of defining a second reply-policy toggle.
|
|
245
|
+
|
|
246
|
+
Full behavior: [Voice Integration](./voice.md).
|
|
247
|
+
|
|
248
|
+
## Public API Smoke Examples
|
|
249
|
+
|
|
250
|
+
Minimal companion-extension examples that import only stable `@llblab/pi-telegram/*` public membranes. Copy one into an extension `index.ts`, load it beside `pi-telegram`, and verify that it starts without importing any `@llblab/pi-telegram/lib/*` implementation path.
|
|
251
|
+
|
|
252
|
+
### Extension Sections
|
|
253
|
+
|
|
254
|
+
```ts
|
|
255
|
+
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
256
|
+
import { registerTelegramSection } from "@llblab/pi-telegram/sections";
|
|
257
|
+
|
|
258
|
+
export default function demoSection(pi: ExtensionAPI) {
|
|
259
|
+
let unregister: (() => void) | undefined;
|
|
260
|
+
pi.on("session_start", async () => {
|
|
261
|
+
unregister?.();
|
|
262
|
+
unregister = registerTelegramSection({
|
|
263
|
+
id: "demo-section/status",
|
|
264
|
+
label: "🧩 Demo section",
|
|
265
|
+
order: 50,
|
|
266
|
+
render: () => ({
|
|
267
|
+
text: "<b>Demo section</b>\n\nThis section was rendered by a companion extension.",
|
|
268
|
+
replyMarkup: { inline_keyboard: [] },
|
|
269
|
+
}),
|
|
270
|
+
});
|
|
271
|
+
});
|
|
272
|
+
pi.on("session_shutdown", async () => {
|
|
273
|
+
unregister?.();
|
|
274
|
+
unregister = undefined;
|
|
275
|
+
});
|
|
276
|
+
}
|
|
277
|
+
```
|
|
278
|
+
|
|
279
|
+
### Raw Update Handler
|
|
280
|
+
|
|
281
|
+
```ts
|
|
282
|
+
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
283
|
+
import { registerTelegramUpdateHandler } from "@llblab/pi-telegram/updates";
|
|
284
|
+
|
|
285
|
+
export default function demoUpdates(pi: ExtensionAPI) {
|
|
286
|
+
let unregister: (() => void) | undefined;
|
|
287
|
+
pi.on("session_start", async () => {
|
|
288
|
+
unregister?.();
|
|
289
|
+
unregister = registerTelegramUpdateHandler((update) => {
|
|
290
|
+
if (!update || typeof update !== "object") return "pass";
|
|
291
|
+
return "pass";
|
|
292
|
+
});
|
|
293
|
+
});
|
|
294
|
+
pi.on("session_shutdown", async () => {
|
|
295
|
+
unregister?.();
|
|
296
|
+
unregister = undefined;
|
|
297
|
+
});
|
|
298
|
+
}
|
|
299
|
+
```
|
|
300
|
+
|
|
301
|
+
### Inbound Handler
|
|
302
|
+
|
|
303
|
+
```ts
|
|
304
|
+
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
305
|
+
import { registerTelegramInboundHandler } from "@llblab/pi-telegram/inbound";
|
|
306
|
+
|
|
307
|
+
export default function demoInbound(pi: ExtensionAPI) {
|
|
308
|
+
let unregister: (() => void) | undefined;
|
|
309
|
+
pi.on("session_start", async () => {
|
|
310
|
+
unregister?.();
|
|
311
|
+
unregister = registerTelegramInboundHandler("text/*", async (file) => {
|
|
312
|
+
if (!file.path.endsWith(".demo.txt")) return undefined;
|
|
313
|
+
return `Demo inbound handler saw ${file.fileName ?? file.path}`;
|
|
314
|
+
});
|
|
315
|
+
});
|
|
316
|
+
pi.on("session_shutdown", async () => {
|
|
317
|
+
unregister?.();
|
|
318
|
+
unregister = undefined;
|
|
319
|
+
});
|
|
320
|
+
}
|
|
321
|
+
```
|
|
322
|
+
|
|
323
|
+
### Outbound Handler
|
|
324
|
+
|
|
325
|
+
```ts
|
|
326
|
+
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
327
|
+
import { registerTelegramOutboundHandler } from "@llblab/pi-telegram/outbound";
|
|
328
|
+
|
|
329
|
+
export default function demoOutbound(pi: ExtensionAPI) {
|
|
330
|
+
let unregister: (() => void) | undefined;
|
|
331
|
+
pi.on("session_start", async () => {
|
|
332
|
+
unregister?.();
|
|
333
|
+
unregister = registerTelegramOutboundHandler("text", async (text) => {
|
|
334
|
+
if (!text.includes("[demo-outbound]")) return undefined;
|
|
335
|
+
return text.replace("[demo-outbound]", "Demo outbound handler:");
|
|
336
|
+
});
|
|
337
|
+
});
|
|
338
|
+
pi.on("session_shutdown", async () => {
|
|
339
|
+
unregister?.();
|
|
340
|
+
unregister = undefined;
|
|
341
|
+
});
|
|
342
|
+
}
|
|
343
|
+
```
|
|
344
|
+
|
|
345
|
+
### Voice Providers
|
|
346
|
+
|
|
347
|
+
```ts
|
|
348
|
+
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
349
|
+
import {
|
|
350
|
+
getTelegramVoiceSendTranscript,
|
|
351
|
+
registerTelegramVoiceSynthesisProvider,
|
|
352
|
+
registerTelegramVoiceTranscriptionProvider,
|
|
353
|
+
} from "@llblab/pi-telegram/voice";
|
|
354
|
+
|
|
355
|
+
export default function demoVoice(pi: ExtensionAPI) {
|
|
356
|
+
let unregisterTts: (() => void) | undefined;
|
|
357
|
+
let unregisterStt: (() => void) | undefined;
|
|
358
|
+
let currentConfig: { voice?: { sendTranscript?: boolean } } = {};
|
|
359
|
+
pi.on("session_start", async () => {
|
|
360
|
+
unregisterTts?.();
|
|
361
|
+
unregisterStt?.();
|
|
362
|
+
unregisterTts = registerTelegramVoiceSynthesisProvider(
|
|
363
|
+
async (text) => {
|
|
364
|
+
const audioPath = await synthesizeDemoOgg(text);
|
|
365
|
+
return getTelegramVoiceSendTranscript(currentConfig)
|
|
366
|
+
? { audioPath, transcriptText: text }
|
|
367
|
+
: { audioPath };
|
|
368
|
+
},
|
|
369
|
+
{ id: "demo-voice/tts" },
|
|
370
|
+
);
|
|
371
|
+
unregisterStt = registerTelegramVoiceTranscriptionProvider(
|
|
372
|
+
async (file) => {
|
|
373
|
+
if (file.kind !== "voice" && file.kind !== "audio") return undefined;
|
|
374
|
+
return { text: `Demo transcript for ${file.fileName ?? file.path}` };
|
|
375
|
+
},
|
|
376
|
+
{ id: "demo-voice/stt" },
|
|
377
|
+
);
|
|
378
|
+
});
|
|
379
|
+
pi.on("session_shutdown", async () => {
|
|
380
|
+
unregisterTts?.();
|
|
381
|
+
unregisterStt?.();
|
|
382
|
+
unregisterTts = undefined;
|
|
383
|
+
unregisterStt = undefined;
|
|
384
|
+
});
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
async function synthesizeDemoOgg(_text: string): Promise<string> {
|
|
388
|
+
throw new Error("Replace synthesizeDemoOgg with a real OGG/Opus generator.");
|
|
389
|
+
}
|
|
390
|
+
```
|
|
391
|
+
|
|
392
|
+
### Smoke Checklist
|
|
393
|
+
|
|
394
|
+
- The extension imports only `@llblab/pi-telegram/sections`, `/updates`, `/inbound`, `/outbound`, `/voice`, or `/keyboard`.
|
|
395
|
+
- It does not import `@llblab/pi-telegram/lib/*`.
|
|
396
|
+
- It registers on `session_start` and disposes on `session_shutdown`.
|
|
397
|
+
- Stable high-level registrations use durable ids.
|
|
398
|
+
- Failures are visible during manual testing through `/telegram-status` or extension-owned logging.
|
|
399
|
+
|
|
400
|
+
## Callback Namespaces
|
|
401
|
+
|
|
402
|
+
Owned prefixes are reserved by `pi-telegram`: `compact:`, `tgbtn:`, `menu:`, `model:`, `thinking:`, `status:`, `queue:`, `settings:`, and `section:`.
|
|
403
|
+
|
|
404
|
+
Companion extensions should use their own short prefix for raw callbacks or use `ctx.callbackData()` inside sections. Unknown unowned callbacks may be forwarded to π as `[callback] <data>` after built-in handlers decline them.
|
|
405
|
+
|
|
406
|
+
Full behavior: [Callback Namespaces](./callback-namespaces.md).
|
|
407
|
+
|
|
408
|
+
## Internal Surface
|
|
409
|
+
|
|
410
|
+
The following are not stable public contracts unless explicitly documented elsewhere:
|
|
411
|
+
|
|
412
|
+
- queue/runtime/lifecycle stores and planners
|
|
413
|
+
- menu implementation helpers
|
|
414
|
+
- polling/lock internals
|
|
415
|
+
- Telegram API transport helpers
|
|
416
|
+
- rendering internals
|
|
417
|
+
- command implementation helpers
|
|
418
|
+
- test support functions
|
|
419
|
+
|
|
420
|
+
They are intentionally not exposed through a `./lib/*.ts` export wildcard in `0.12.0`.
|