@llblab/pi-telegram 0.11.1 → 0.12.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 +17 -12
- package/BACKLOG.md +0 -10
- package/CHANGELOG.md +33 -2
- package/README.md +42 -25
- 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 +176 -135
- package/docs/callback-namespaces.md +3 -3
- package/docs/command-templates.md +81 -24
- package/docs/{inbound-handlers.md → inbound.md} +13 -10
- package/docs/locks.md +3 -3
- package/docs/{outbound-handlers.md → outbound.md} +13 -10
- package/docs/public-api.md +266 -0
- package/docs/{extension-sections.md → sections.md} +31 -27
- package/docs/ui-style.md +165 -0
- package/docs/{external-handlers.md → updates.md} +33 -31
- package/docs/voice.md +17 -14
- package/index.ts +86 -261
- package/lib/bindings.ts +301 -0
- package/lib/command-templates.ts +163 -32
- package/lib/commands.ts +114 -1
- package/lib/config.ts +45 -4
- package/lib/{inbound-handlers.ts → inbound.ts} +5 -4
- package/lib/lifecycle.ts +122 -1
- package/lib/menu-model.ts +3 -3
- package/lib/menu-queue.ts +1 -1
- package/lib/menu-settings.ts +63 -32
- package/lib/menu-status.ts +1 -1
- package/lib/menu.ts +1 -1
- package/lib/{outbound-handlers.ts → outbound.ts} +21 -11
- package/lib/pi.ts +4 -0
- package/lib/polling.ts +4 -3
- package/lib/preview.ts +1 -1
- package/lib/routing.ts +45 -13
- package/lib/{extension-sections.ts → sections.ts} +37 -8
- package/lib/time-injection.ts +1 -1
- package/lib/updates.ts +121 -1
- package/lib/voice.ts +33 -14
- package/package.json +11 -1
- package/lib/external-handlers.ts +0 -166
|
@@ -4,12 +4,14 @@ Command templates are the portable integration format for deterministic local au
|
|
|
4
4
|
|
|
5
5
|
**Meta-contract:** transportable (bit-for-bit identical across projects), high-density (zero fluff), constant (evolve by crystallizing, not speculating), optimal minimum (add only when it hurts).
|
|
6
6
|
|
|
7
|
-
**Scope:** portable synchronous command execution format — shell-free exec, composition/pipes, timeout
|
|
7
|
+
**Scope:** portable synchronous command execution format — shell-free exec, composition/pipes, optional timeout, delay-before-start, bounded retry, failure propagation, recover cleanup, output artifact selection, and handler-level fallback. Single JSON standard; no platform lock-in.
|
|
8
8
|
|
|
9
9
|
---
|
|
10
10
|
|
|
11
11
|
Extensions may choose their own config files, selectors, placeholder sources, and examples, but should preserve this core contract.
|
|
12
12
|
|
|
13
|
+
Layer boundary: command templates own only the synchronous execution graph. Recipe imports, import-reference expressions, recipe lookup, `async: true`, run ids, state dirs, FIFO controls, and outbox events are host/recipe/async-run configuration layers, not portable command-template syntax.
|
|
14
|
+
|
|
13
15
|
## Shape
|
|
14
16
|
|
|
15
17
|
A command template is either a command-line string or an ordered array of command-template leaves:
|
|
@@ -32,13 +34,15 @@ Common object fields:
|
|
|
32
34
|
|
|
33
35
|
- `label`: Optional human label for diagnostics and parallel branch reports.
|
|
34
36
|
- `mode`: Optional execution mode for array templates. Default is `"sequence"`; `"parallel"` runs children concurrently.
|
|
35
|
-
- `args`: Optional placeholder
|
|
37
|
+
- `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.
|
|
36
38
|
- `defaults`: Placeholder default values by name.
|
|
37
|
-
- `timeout`: Optional execution timeout in milliseconds.
|
|
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.
|
|
38
40
|
- `delay`: Optional wait in milliseconds before starting this node. Default is no delay.
|
|
39
41
|
- `output`: Optional result selector. Default is `"stdout"`; runtime values such as `"ogg"` are valid.
|
|
40
42
|
- `retry`: Optional max attempts including the first. Default is `1`.
|
|
41
|
-
- `critical`: Optional boolean.
|
|
43
|
+
- `critical`: Optional boolean. Backward-compatible alias for `failure: "root"`.
|
|
44
|
+
- `failure`: Optional failure propagation scope: `continue`, `branch`, or `root`. Default is `continue`.
|
|
45
|
+
- `recover`: Optional command template run between failed retry attempts. Recovery output is ignored; recovery failure stops retries.
|
|
42
46
|
- `template`: Required command string or ordered composition array.
|
|
43
47
|
|
|
44
48
|
For object form, write `template` last. Read the node flags first, then the executable content. Storage paths, labels, selectors, descriptions, and registry-specific metadata belong to each extension's local schema.
|
|
@@ -63,8 +67,9 @@ Supported forms:
|
|
|
63
67
|
| ---------------- | ------------------------------------------------ |
|
|
64
68
|
| `{name}` | Required value from runtime values or `defaults` |
|
|
65
69
|
| `{name=default}` | Inline default when no value is provided |
|
|
70
|
+
| `{items[index]}` | Array item selected by literal or repeat index |
|
|
66
71
|
|
|
67
|
-
Resolution order is runtime values → `defaults` → inline default → error.
|
|
72
|
+
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.
|
|
68
73
|
|
|
69
74
|
```json
|
|
70
75
|
{
|
|
@@ -80,6 +85,8 @@ With runtime values `{ "text": "hello" }`, argv is:
|
|
|
80
85
|
|
|
81
86
|
Use `defaults` for visible configuration data; use inline defaults for compact local literals. Prefer flag-style examples such as `/path/to/tool --file {file} --lang {lang=ru}` for readability, but positional forms such as `/path/to/tool {file} {lang=ru}` are valid when the invoked script defines that CLI contract.
|
|
82
87
|
|
|
88
|
+
Typed declarations annotate the public tool interface, not the shell command. They may live in `args` or inline placeholders such as `{timeout:int=60000}` and `{mode:enum(check,fix)=check}`. Use metadata-first authoring (`args` plus `defaults`) when long templates should stay visually short; use inline-first authoring when one self-contained `template` property is clearer. They do not sandbox or reinterpret the executable; they only let the host generate narrower input schemas and normalize runtime values before placeholder substitution. Untyped `args` and untyped placeholders continue to work unchanged.
|
|
89
|
+
|
|
83
90
|
## Quoting
|
|
84
91
|
|
|
85
92
|
Placeholder values are not shell-escaped because no shell is used. A value containing spaces remains one argv item when it replaces one split word:
|
|
@@ -122,12 +129,12 @@ Composition rules:
|
|
|
122
129
|
|
|
123
130
|
- Execute leaves in order when `mode` is omitted or set to `"sequence"`
|
|
124
131
|
- Execute child templates concurrently when `mode` is set to `"parallel"`
|
|
125
|
-
- Parallel composition uses soft-quorum semantics by default: failed
|
|
126
|
-
- Non-critical failures are recorded and execution continues, while `
|
|
132
|
+
- Parallel composition uses soft-quorum semantics by default: failed children are reported as degraded branches unless failure propagation escalates
|
|
133
|
+
- Non-critical failures are recorded and execution continues, while `failure: "branch"` stops the current branch and `failure: "root"` aborts the root composition
|
|
127
134
|
- Treat the whole composition as one handler for selector matching and fallback
|
|
128
135
|
- Top-level `args` and `defaults` apply to every leaf unless the leaf defines private values
|
|
129
136
|
- Leaf `args` replace inherited `args`; leaf `defaults` merge over inherited defaults; `timeout` and `output` are not inherited into leaves
|
|
130
|
-
-
|
|
137
|
+
- Timeout is disabled by default; configure a positive `timeout` for bounded commands that should fail closed
|
|
131
138
|
- Each sequence leaf receives the previous leaf's stdout on stdin by default, while the final leaf stdout remains the default composition result
|
|
132
139
|
- Each parallel child receives the same stdin, and child stdout values are joined in stable array order before flowing to the next sequence leaf
|
|
133
140
|
- Parallel branch joins include branch label and status, and tool details include branch metadata plus coverage summary
|
|
@@ -231,38 +238,76 @@ Legacy local schemas may accept `pipe` as an alias, but the portable standard is
|
|
|
231
238
|
|
|
232
239
|
By default, composition continues on failure: the failed step is logged and the next step executes. This is analogous to `make -k` — the user sees all failures at once and decides what to fix.
|
|
233
240
|
|
|
234
|
-
##
|
|
241
|
+
## Failure Propagation
|
|
242
|
+
|
|
243
|
+
By default, failed steps use `failure: "continue"`: record the failure, clear stdout for that step, and continue the current sequence. This preserves the fail-open profile.
|
|
244
|
+
|
|
245
|
+
Use `failure` when a node should stop more aggressively:
|
|
235
246
|
|
|
236
|
-
|
|
247
|
+
- `"continue"`: record the failure and continue the current sequence.
|
|
248
|
+
- `"branch"`: stop the current sequence/subtree and return a failed branch to the nearest parent. In a parallel node, sibling branches keep running and the join becomes degraded. At the root, branch failure is still a tool failure.
|
|
249
|
+
- `"root"`: abort the outermost composition.
|
|
237
250
|
|
|
238
251
|
```json
|
|
239
252
|
{
|
|
253
|
+
"mode": "parallel",
|
|
240
254
|
"template": [
|
|
241
|
-
{
|
|
242
|
-
|
|
243
|
-
|
|
255
|
+
{
|
|
256
|
+
"label": "agent-a",
|
|
257
|
+
"failure": "branch",
|
|
258
|
+
"template": [
|
|
259
|
+
"agent-a-work {scope}",
|
|
260
|
+
"agent-a-validate {scope}",
|
|
261
|
+
"agent-a-push {scope}"
|
|
262
|
+
]
|
|
263
|
+
},
|
|
264
|
+
{
|
|
265
|
+
"label": "agent-b",
|
|
266
|
+
"failure": "branch",
|
|
267
|
+
"template": [
|
|
268
|
+
"agent-b-work {scope}",
|
|
269
|
+
"agent-b-validate {scope}",
|
|
270
|
+
"agent-b-push {scope}"
|
|
271
|
+
]
|
|
272
|
+
}
|
|
244
273
|
]
|
|
245
274
|
}
|
|
246
275
|
```
|
|
247
276
|
|
|
248
|
-
`
|
|
277
|
+
If `agent-a-validate` fails, `agent-a-push` is skipped, `agent-b` can still finish, and the parallel join reports degraded branch coverage.
|
|
249
278
|
|
|
250
|
-
|
|
279
|
+
`critical: true` remains a backward-compatible alias for `failure: "root"`. Prefer `failure` for new templates because it names the propagation scope directly.
|
|
251
280
|
|
|
252
281
|
## Retry
|
|
253
282
|
|
|
254
|
-
Set `retry: N`
|
|
283
|
+
Set `retry: N` to attempt execution up to `N` times including the first. The first successful attempt stops the retry loop.
|
|
284
|
+
|
|
285
|
+
On leaf commands, retry repeats that command. On sequence or parallel nodes, retry repeats the whole node. A retried group only retries when the group returns a failure, so validator checkpoints normally pair group retry with `failure: "branch"` or `failure: "root"`.
|
|
255
286
|
|
|
256
287
|
```json
|
|
257
288
|
{
|
|
258
|
-
"
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
289
|
+
"failure": "branch",
|
|
290
|
+
"retry": 3,
|
|
291
|
+
"template": ["implement {scope}", "npm test", "git diff --check"]
|
|
292
|
+
}
|
|
293
|
+
```
|
|
294
|
+
|
|
295
|
+
Here the whole group runs again when a validator fails. Without `failure: "branch"`, the failed validator would be logged and the group would continue by default.
|
|
296
|
+
|
|
297
|
+
## Recover
|
|
298
|
+
|
|
299
|
+
Set `recover` on a retried node to run cleanup after a failed attempt and before the next attempt. `recover` is another command template: it can be a string command, sequence, or mode tree. Its output is ignored and the next retry receives the original stdin.
|
|
300
|
+
|
|
301
|
+
```json
|
|
302
|
+
{
|
|
303
|
+
"failure": "branch",
|
|
304
|
+
"retry": 3,
|
|
305
|
+
"recover": "git -C {work_dir} reset --hard HEAD",
|
|
306
|
+
"template": ["pi -p --tools read,edit,bash {scope_file}", "npm test"]
|
|
262
307
|
}
|
|
263
308
|
```
|
|
264
309
|
|
|
265
|
-
`
|
|
310
|
+
`recover` is not a fallback success path. It is cleanup between attempts. Practical uses include resetting a worktree, removing temp files, clearing generated output, releasing a local lock, or stopping a helper process before trying the node again. If recovery fails, retries stop and the recovery failure is returned. Recovery uses fail-closed semantics by default; set an explicit `failure` inside a recover template only when a softer cleanup failure is intentional.
|
|
266
311
|
|
|
267
312
|
## Delay
|
|
268
313
|
|
|
@@ -290,12 +335,24 @@ string → leaf command
|
|
|
290
335
|
string[] → sequential composition
|
|
291
336
|
{ template } → leaf command object
|
|
292
337
|
{ mode, template } → sequence or parallel subtree
|
|
293
|
-
{ mode, args, defaults, delay, retry,
|
|
338
|
+
{ mode, args, defaults, delay, retry, failure, recover, output, template } → full node
|
|
294
339
|
```
|
|
295
340
|
|
|
296
|
-
Start with a string. Add composition when needed. Add `mode: "parallel"` when independent work can run concurrently. Add delay when launch pacing matters. Add retry when flaky. Add
|
|
341
|
+
Start with a string. Add composition when needed. Add `mode: "parallel"` when independent work can run concurrently. 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
|
+
|
|
343
|
+
`mode: "parallel"` 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
|
+
|
|
345
|
+
## Trust Boundary
|
|
346
|
+
|
|
347
|
+
Command templates avoid shell interpolation by splitting the template into argv first and substituting placeholders per arg. A placeholder value containing spaces remains one argv value, not a shell fragment.
|
|
348
|
+
|
|
349
|
+
This is not a sandbox. The executable still runs with the same user permissions as the host agent. Shells, interpreter eval modes, destructive filesystem commands, and local scripts remain trusted code. Examples that deserve extra operator attention:
|
|
350
|
+
|
|
351
|
+
- `bash`, `sh`, `zsh`, or `fish`, especially with `-c`.
|
|
352
|
+
- `node -e`, `python -c`, `ruby -e`, `perl -e`, or similar eval modes.
|
|
353
|
+
- `rm`, `mv`, `cp`, or `rsync` over broad paths or placeholder-derived paths.
|
|
297
354
|
|
|
298
|
-
|
|
355
|
+
Hosts may surface lightweight warnings for these obvious high-risk shapes. Warnings should inform review without blocking existing tools, because many trusted local wrappers intentionally use shells or filesystem mutation.
|
|
299
356
|
|
|
300
357
|
## Tool Boundary
|
|
301
358
|
|
|
@@ -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: an extension such as `pi-xai-voice` 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,7 +13,7 @@ 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.
|
|
@@ -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,266 @@
|
|
|
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.
|
|
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()` and `ctx.open()` auto-prepend the correct Back/Main-menu row.
|
|
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 { audioPath, transcriptText: text };
|
|
237
|
+
},
|
|
238
|
+
{ id: "@scope/my-extension/tts" },
|
|
239
|
+
);
|
|
240
|
+
```
|
|
241
|
+
|
|
242
|
+
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.
|
|
243
|
+
|
|
244
|
+
Full behavior: [Voice Integration](./voice.md).
|
|
245
|
+
|
|
246
|
+
## Callback Namespaces
|
|
247
|
+
|
|
248
|
+
Owned prefixes are reserved by `pi-telegram`: `compact:`, `tgbtn:`, `menu:`, `model:`, `thinking:`, `status:`, `queue:`, `settings:`, and `section:`.
|
|
249
|
+
|
|
250
|
+
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.
|
|
251
|
+
|
|
252
|
+
Full behavior: [Callback Namespaces](./callback-namespaces.md).
|
|
253
|
+
|
|
254
|
+
## Internal Surface
|
|
255
|
+
|
|
256
|
+
The following are not stable public contracts unless explicitly documented elsewhere:
|
|
257
|
+
|
|
258
|
+
- queue/runtime/lifecycle stores and planners
|
|
259
|
+
- menu implementation helpers
|
|
260
|
+
- polling/lock internals
|
|
261
|
+
- Telegram API transport helpers
|
|
262
|
+
- rendering internals
|
|
263
|
+
- command implementation helpers
|
|
264
|
+
- test support functions
|
|
265
|
+
|
|
266
|
+
They are intentionally not exposed through a `./lib/*.ts` export wildcard in `0.12.0`.
|