@llblab/pi-kit 0.3.0 → 0.3.2
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/CHANGELOG.md +8 -0
- package/README.md +1 -1
- package/node_modules/@llblab/pi-telegram/AGENTS.md +1 -1
- package/node_modules/@llblab/pi-telegram/CHANGELOG.md +8 -0
- package/node_modules/@llblab/pi-telegram/README.md +1 -1
- package/node_modules/@llblab/pi-telegram/docs/architecture.md +1 -1
- package/node_modules/@llblab/pi-telegram/docs/compact-matrix-literal.md +13 -9
- package/node_modules/@llblab/pi-telegram/docs/outbound.md +6 -6
- package/node_modules/@llblab/pi-telegram/docs/public-api.md +3 -1
- package/node_modules/@llblab/pi-telegram/lib/outbound-markup.ts +43 -6
- package/node_modules/@llblab/pi-telegram/lib/outbound.ts +4 -1
- package/node_modules/@llblab/pi-telegram/package.json +1 -1
- package/node_modules/@llblab/pi-telegram/skills/telegram-bridge/SKILL.md +4 -3
- package/package.json +2 -2
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,14 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to `@llblab/pi-kit` are documented here.
|
|
4
4
|
|
|
5
|
+
## 0.3.2 - 2026-09-01
|
|
6
|
+
|
|
7
|
+
- `Telegram Comment Membrane`: Advances `@llblab/pi-telegram` to `0.42.2`, keeping assistant-authored HTML comments transport-private across Telegram previews and final replies while preserving top-level button and voice activation and skipping empty text sends after sanitization.
|
|
8
|
+
|
|
9
|
+
## 0.3.1 - 2026-09-01
|
|
10
|
+
|
|
11
|
+
- `Telegram Patch Release`: Advances `@llblab/pi-telegram` to `0.42.1`, accepting prompt-only CML button cells such as `{|e2}` and their optional selected-style form while preserving prompt-as-label fallback and fail-closed empty prompt, style, and voice atoms.
|
|
12
|
+
|
|
5
13
|
## 0.3.0 - 2026-09-01
|
|
6
14
|
|
|
7
15
|
- `Telegram Minor Release`: Advances `@llblab/pi-telegram` to `0.42.0`, bringing tolerant JSON and compact CML action parsing, positional voice action cells, Thread-aware generated-button prompts, and focused proactive control-surface guidance into the bundled kit.
|
package/README.md
CHANGED
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
| `@llblab/pi-actors` | `0.52.0` | Extension and Skills |
|
|
10
10
|
| `@llblab/pi-codex-usage` | `0.9.4` | Extension |
|
|
11
11
|
| `@llblab/pi-grow-loop` | `0.7.3` | Extension and Skills |
|
|
12
|
-
| `@llblab/pi-telegram` | `0.42.
|
|
12
|
+
| `@llblab/pi-telegram` | `0.42.2` | Extension and Skills |
|
|
13
13
|
|
|
14
14
|
Versions are exact by design. Updating an extension does not change an installed kit until this repository explicitly advances that dependency and publishes a new kit version.
|
|
15
15
|
|
|
@@ -90,7 +90,7 @@ Use the relevant local skill before non-trivial work in its domain. Keep skill o
|
|
|
90
90
|
- Queue lane/kind admission is explicit. Dispatch waits for active-turn, pending-dispatch, control, compaction, `ctx.isIdle()`, and Pi pending-message guards; a dispatched prompt stays queued until `agent_start` consumes it. Each prompt is one object with one active lane and no reserved return slot. Normal and Priority are separate FIFO lanes: crossing lanes removes it from the source and appends it at the destination tail, while Keep/Skip and same-category emoji changes preserve lane position. Complete reaction sets independently derive Priority from recognized positive emoji and Skip from recognized negative emoji; both may coexist, suppressed turns retain durable receipts while waiting, and Skip settles them only when the prompt reaches dispatch before dropping it without inference. Suppressed turns remain visible at a struck-through physical ordinal without contributing to executable queue counters, while graceful session shutdown discards all remaining queue authority before clearing memory.
|
|
91
91
|
- `/stop`, `/abort`, `/next`, and `/continue` respectively reset+abort, abort while preserving queue, force the next turn, and enqueue a control-lane continuation. Abort-history folding applies only to Telegram-owned active turns.
|
|
92
92
|
- Telegram extension side effects must not hold Pi's core lifecycle hostage after semantic completion. Preserve ordering in extension-owned background work, record failures, and fence target/profile/transport/session authority.
|
|
93
|
-
- Complete assistant/guest model answers use Telegram-native Rich Markdown. Harness-owned menus, status, diagnostics, thinking, and tool evidence remain explicit HTML/plain or their documented native surface. Preserve literal code and structurally safe chunking; never split invalid markup.
|
|
93
|
+
- Complete assistant/guest model answers use Telegram-native Rich Markdown. Harness-owned menus, status, diagnostics, thinking, and tool evidence remain explicit HTML/plain or their documented native surface. Before Telegram preview or final delivery, strip every assistant-authored HTML comment regardless of Markdown position while keeping action activation top-level-only; a comment-only result sends no text message. Preserve literal code outside comments and structurally safe chunking; never split invalid markup.
|
|
94
94
|
- `preview` owns streaming lifecycle only, not assistant rendering. Finalization waits for active preview flushes and must not issue pre/post-final draft-clear calls that create transient Telegram draft UI.
|
|
95
95
|
- Native `sendChatAction(typing)` is the automatic activity signal for unsettled agent and compaction work while Telegram transport is authorized. Extension-owned blocking UI prompts pause it and completion resumes it while either work owner remains active. Do not invent extra in-chat work indicators or emit activity for startup/connect/reload/recovery alone.
|
|
96
96
|
- Public activity handlers and connected companion delivery are asynchronous, target-bound, generation-fenced surfaces. Connected companion projection has no independent opt-out: disconnect or authority loss is its boundary. Token deltas, hidden reasoning, unknown sources, and stale authority never enter public projection.
|
|
@@ -2,6 +2,14 @@
|
|
|
2
2
|
|
|
3
3
|
> Each release keeps at most 8 outcome records of at most 512 characters.
|
|
4
4
|
|
|
5
|
+
## 0.42.2: Telegram Comment Membrane
|
|
6
|
+
|
|
7
|
+
- `Transport-Private HTML Comments`: Removes every assistant-authored `<!-- … -->` block from Telegram previews, active-turn finals, connected companion projections, direct sends, and Guest Mode regardless of Markdown position or owning extension; only recognized top-level comments activate actions, unclosed tails stay hidden, comment-only text plans send nothing, and the Pi terminal transcript remains unchanged.
|
|
8
|
+
|
|
9
|
+
## 0.42.1: Prompt-Only Button Cells
|
|
10
|
+
|
|
11
|
+
- `Prompt-Only CML Buttons`: Accepts `{|prompt}` and `{|prompt|selected_style}` as canonical button cells equivalent to prompt-only JSON, so established prompt fallback supplies both visible text and queued input without a separately authored label; empty one-atom cells, prompts, styles, and voice atoms still fail closed.
|
|
12
|
+
|
|
5
13
|
## 0.42.0: Compact Action Grammar
|
|
6
14
|
|
|
7
15
|
- `Adaptive Action Grammar`: Extracts the first semantically valid JSON or positional CML payload from noisy `telegram_button` and `telegram_voice` envelopes, tolerates bounded trailing commas and unmatched matrix noise, keeps malformed named JSON on the JSON path, and retains legacy attribute parsing as undocumented compatibility.
|
|
@@ -222,7 +222,7 @@ Voice notes, audio, images, PDFs, and other media can pass through configured in
|
|
|
222
222
|
|
|
223
223
|
### Buttons And Callbacks
|
|
224
224
|
|
|
225
|
-
Assistant replies can include top-level hidden `telegram_button` comments containing a JSON object, adaptive JSON/CML matrix, or positional Compact Matrix Literal (CML). One adaptive matrix may mix named JSON objects with positional CML cells; separators are optional and one trailing comma is tolerated, including inside JSON objects. Top-level cells become full-width rows while nested rows group one or more buttons horizontally without an artificial parser-level width cap; generated surfaces default to five columns and use six to eight only for short position-bearing labels. CML uses `{value}`, `{label|prompt}`,
|
|
225
|
+
Assistant replies can include top-level hidden `telegram_button` comments containing a JSON object, adaptive JSON/CML matrix, or positional Compact Matrix Literal (CML). One adaptive matrix may mix named JSON objects with positional CML cells; separators are optional and one trailing comma is tolerated, including inside JSON objects. Top-level cells become full-width rows while nested rows group one or more buttons horizontally without an artificial parser-level width cap; generated surfaces default to five columns and use six to eight only for short position-bearing labels. CML uses `{value}`, `{label|prompt}`, `{|prompt}`, or the corresponding three-atom form with `selected_style` set to `primary`, `success`, or `danger`; omitting the first atom leaves the existing prompt-as-label fallback in charge, while the optional style still requires a non-empty prompt. It trims atom boundaries, preserves non-structural text literally, and decodes only `\|`, `\}`, and `\\`. Prefer one matrix comment for multiple buttons. Buttons use `label` plus `prompt`, or the compact `value` key when both are identical. The bridge strips every assistant-authored HTML comment from Telegram previews and final replies regardless of Markdown position or owning extension, while only recognized top-level comments activate buttons or voice; comment-only output sends no text message and the Pi terminal transcript remains unchanged. It renders valid inline buttons and routes callbacks back into Pi as queued prompts or extension-owned callback actions. Button-only replies receive the standard `☑️ **Choose an option:**` heading as automatic visible fallback text. Once a generated prompt button is accepted, only that exact button switches to its optional `selected_style` (`primary` blue by default, `success` green, or `danger` red) without altering its agent-authored label or emoji; every style still queues the selected prompt.
|
|
226
226
|
|
|
227
227
|
### Threaded Mode And Multi-Instance Bus
|
|
228
228
|
|
|
@@ -357,7 +357,7 @@ Assistant-authored final-message actions use hidden top-level comments:
|
|
|
357
357
|
- `telegram_voice` accepts one positional compact action cell or JSON object and creates one voice artifact through configured outbound handlers, programmatic voice handlers, or registered synthesis providers.
|
|
358
358
|
- `telegram_button` accepts a JSON object, adaptive JSON/CML matrix, or positional Compact Matrix Literal. Named JSON objects and positional cells may coexist, with commas optional only between completed matrix or row elements. Each top-level cell creates one full-width row, while a nested row groups buttons horizontally without an artificial parser-width cap; every callback enqueues its configured prompt text as a normal Telegram prompt turn. The JSON-first grammar, positional trim/escape rules, atomic rejection, and renderer-owned width policy are specified in [Adaptive Button Literal](./compact-matrix-literal.md).
|
|
359
359
|
|
|
360
|
-
|
|
360
|
+
Action recognition remains restricted to top-level column-zero comments so nested examples cannot trigger voice, buttons, or callbacks. The Telegram surface independently strips every complete assistant-authored HTML comment from previews and final delivery regardless of Markdown position or comment owner; an unclosed comment is withheld through the remaining tail, and a comment-only result sends no text message. Pi's terminal transcript and model context remain unchanged.
|
|
361
361
|
|
|
362
362
|
Unknown callback data outside owned prefixes is forwarded as `[callback] <data>` only after built-in and extension handlers decline it.
|
|
363
363
|
|
|
@@ -58,13 +58,16 @@ A two-atom cell separates label and prompt:
|
|
|
58
58
|
{Pause|music::pause}
|
|
59
59
|
```
|
|
60
60
|
|
|
61
|
-
|
|
61
|
+
Only the first label position may be empty. Prompt-only `{|e2}` is equivalent to JSON `{"prompt":"e2"}`: the existing button fallback uses `e2` as both visible text and queued prompt without requiring a separately authored label.
|
|
62
|
+
|
|
63
|
+
A three-atom cell adds the selected style and retains the same optional-label form:
|
|
62
64
|
|
|
63
65
|
```text
|
|
64
66
|
{Stop|music::stop|danger}
|
|
67
|
+
{|e2|success}
|
|
65
68
|
```
|
|
66
69
|
|
|
67
|
-
The style atom is accepted only as `primary`, `success`, or `danger`.
|
|
70
|
+
The prompt and style atoms remain required. The style atom is accepted only as `primary`, `success`, or `danger`.
|
|
68
71
|
|
|
69
72
|
## Adaptive Grammar
|
|
70
73
|
|
|
@@ -78,8 +81,8 @@ row := "[" ws cell (boundary cell)* ws "]"
|
|
|
78
81
|
cell := json-object | positional-cell
|
|
79
82
|
boundary := ws [","] ws
|
|
80
83
|
positional-cell := "{" atom "}"
|
|
81
|
-
| "{" atom "|" atom "}"
|
|
82
|
-
| "{" atom "|" atom "|" atom "}"
|
|
84
|
+
| "{" [atom] "|" atom "}"
|
|
85
|
+
| "{" [atom] "|" atom "|" atom "}"
|
|
83
86
|
atom := atom-unit+
|
|
84
87
|
atom-unit := ordinary | "\|" | "\}" | "\\"
|
|
85
88
|
ws := *(SP | HTAB | CR | LF)
|
|
@@ -114,7 +117,7 @@ A conforming parser:
|
|
|
114
117
|
3. Tries one complete strict JSON object, then bounded trailing-comma recovery, at each cell boundary before positional interpretation.
|
|
115
118
|
4. Keeps JSON-shaped named objects on the JSON path when validation fails instead of exposing their source as positional text.
|
|
116
119
|
5. Accepts at most one comma between elements or immediately before a closing row or matrix delimiter, while rejecting leading, repeated, or property-level omitted commas.
|
|
117
|
-
6. Rejects empty
|
|
120
|
+
6. Rejects empty matrices, rows, one-atom cells, prompts, styles, and nesting deeper than one row; only the first label position may be empty in a two- or three-atom button cell.
|
|
118
121
|
7. Decodes only `\|`, `\}`, and `\\` in positional cells.
|
|
119
122
|
8. Extracts the first complete valid payload from a tolerant comment envelope and ignores unrelated text or isolated unmatched matrix brackets around it.
|
|
120
123
|
9. Returns no partial rows or cells from a balanced malformed candidate.
|
|
@@ -128,8 +131,9 @@ For `telegram_button` comments:
|
|
|
128
131
|
|
|
129
132
|
- JSON `value` keeps its existing label/prompt fallback semantics.
|
|
130
133
|
- Positional `{value}` is equivalent to JSON `{"value":"value"}`; a lone JSON `label` or `prompt` has the same both-fields shorthand semantics.
|
|
134
|
+
- Positional `{|prompt}` is equivalent to JSON `{"prompt":"prompt"}` and therefore uses the prompt as both visible text and queued prompt.
|
|
131
135
|
- Positional `{label|prompt}` is equivalent to JSON `{"label":"label","prompt":"prompt"}`.
|
|
132
|
-
- Positional `{label|prompt|selected_style}`
|
|
136
|
+
- Positional `{label|prompt|selected_style}` and `{|prompt|selected_style}` are equivalent to their corresponding JSON objects.
|
|
133
137
|
- Top-level cells become full-width rows.
|
|
134
138
|
- Nested rows become horizontal keyboard rows.
|
|
135
139
|
- Invalid payloads are stripped with their recognized action comment and register no callbacks.
|
|
@@ -153,7 +157,7 @@ The grammar imposes no visual row-width maximum. Renderer and interaction policy
|
|
|
153
157
|
Accepted classes include:
|
|
154
158
|
|
|
155
159
|
- Strict JSON objects and matrices.
|
|
156
|
-
- Positional singleton, two-atom, and styled cells.
|
|
160
|
+
- Positional singleton, two-atom, prompt-only `{|prompt}`, and styled cells.
|
|
157
161
|
- Matrices and rows with commas, without commas, or a mixture of boundaries.
|
|
158
162
|
- Named JSON and positional cells mixed in one matrix or row.
|
|
159
163
|
- Literal commas inside positional atoms and strict JSON strings.
|
|
@@ -162,7 +166,7 @@ Accepted classes include:
|
|
|
162
166
|
|
|
163
167
|
Rejected classes include:
|
|
164
168
|
|
|
165
|
-
- Empty payloads, matrices, rows,
|
|
169
|
+
- Empty payloads, matrices, rows, one-atom cells, prompts, or style atoms; an empty label is valid only as the first position of a two- or three-atom button cell.
|
|
166
170
|
- Leading or repeated element commas.
|
|
167
171
|
- Missing commas between properties inside a JSON object.
|
|
168
172
|
- Deeper row nesting.
|
|
@@ -175,4 +179,4 @@ Every rejected case proves zero callback registration.
|
|
|
175
179
|
|
|
176
180
|
## Versioning
|
|
177
181
|
|
|
178
|
-
This document defines CML v3. V3 extends the v2 positional grammar with strict JSON object cells, mixed representation,
|
|
182
|
+
This document defines CML v3. V3 extends the v2 positional grammar with strict JSON object cells, mixed representation, optional element-boundary commas, and prompt-only button cells that preserve the established JSON fallback semantics. It does not make JSON object internals permissive and does not add deeper structures. Future versions must preserve strict-JSON-first routing, bounded depth, atomic rejection, and an explicit discriminator for any new meaning at a security or ownership boundary.
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
`pi-telegram` maps hidden assistant-authored HTML comments to Telegram-native outbound actions.
|
|
4
4
|
|
|
5
|
-
Normal Telegram-turn replies are intentionally prompt-driven: the agent writes Markdown plus small hidden top-level blocks, and the bridge performs transport after `agent_end`. `telegram_voice` and `telegram_button` are not Pi tools. For local/TUI-initiated work where the user explicitly asks to send something to Telegram, the bridge also exposes direct tools: `telegram_message` for Markdown text and `telegram_attach` for file delivery when no Telegram turn is active. In classic mode, direct local/TUI delivery requires this Pi instance to own `/telegram-connect`; in Threaded Mode, a registered follower may route direct-tool sends through the leader-owned bus transport. During an active Telegram turn, `telegram_message` rejects an implicit or same-turn target so ordinary final delivery remains the sole current-target response. Its `thread` argument accepts a case-insensitive live thread name or numeric id: the bridge preflights one live owner, sends visibly, then admits the text as a source-attributed turn in that instance. Unknown, ambiguous, same-target, offline, and cross-chat destinations fail before sending. Existing `chat_id` plus `thread_id` targeting stays compatible; registered followers use authenticated, generation-fenced bus routing. Outbound behavior combines assistant prompt markup, text command-template handlers, registered voice synthesis providers, generated artifacts, direct Telegram tools, and reply delivery. Direct `telegram_message` text is planned through the same reply markup path, so embedded top-level `telegram_button` comments become buttons attached to that text message.
|
|
5
|
+
Normal Telegram-turn replies are intentionally prompt-driven: the agent writes Markdown plus small hidden top-level blocks, and the bridge performs transport after `agent_end`. `telegram_voice` and `telegram_button` are not Pi tools. Action activation remains restricted to recognized top-level column-zero comments, but Telegram preview and final delivery remove every assistant-authored `<!-- … -->` block regardless of Markdown position or owning extension. Unclosed comment tails are withheld, comment-only text plans send no message, and the Pi terminal transcript remains unchanged. For local/TUI-initiated work where the user explicitly asks to send something to Telegram, the bridge also exposes direct tools: `telegram_message` for Markdown text and `telegram_attach` for file delivery when no Telegram turn is active. In classic mode, direct local/TUI delivery requires this Pi instance to own `/telegram-connect`; in Threaded Mode, a registered follower may route direct-tool sends through the leader-owned bus transport. During an active Telegram turn, `telegram_message` rejects an implicit or same-turn target so ordinary final delivery remains the sole current-target response. Its `thread` argument accepts a case-insensitive live thread name or numeric id: the bridge preflights one live owner, sends visibly, then admits the text as a source-attributed turn in that instance. Unknown, ambiguous, same-target, offline, and cross-chat destinations fail before sending. Existing `chat_id` plus `thread_id` targeting stays compatible; registered followers use authenticated, generation-fenced bus routing. Outbound behavior combines assistant prompt markup, text command-template handlers, registered voice synthesis providers, generated artifacts, direct Telegram tools, and reply delivery. Direct `telegram_message` text is planned through the same reply markup path, so embedded top-level `telegram_button` comments become buttons attached to that text message.
|
|
6
6
|
|
|
7
7
|
Text handlers use the portable [Command Template Standard](./command-templates.md). Programmatic outbound handlers use `registerTelegramOutboundHandler(kind, handler)`. Voice replies can use configured command-template handlers or the provider API described in [Voice Integration](./voice.md).
|
|
8
8
|
|
|
@@ -139,7 +139,7 @@ Full text answer stays here.
|
|
|
139
139
|
<!-- telegram_voice {"text":"First line.\nSecond line.","lang":"ru"} -->
|
|
140
140
|
```
|
|
141
141
|
|
|
142
|
-
The bridge strips the comment from Telegram text. On `agent_end`, it maps each `telegram_voice` action to a provider call, generates one file per action, and sends each file as an independent Telegram-native voice message. Prefer `{text}`, `{text|lang}`, or `{text|lang|rate}`. Use JSON when multiline content, named fields, or escaping earns the extra syntax; equivalent JSON `text` or `value` supplies the spoken payload, with explicit `text` taking precedence. Each comment creates one voice artifact, so voice cells do not accept button-style matrix composition. The opening marker must start at column zero on a top-level line outside fenced code, quotes, lists, and indented examples; otherwise it
|
|
142
|
+
The bridge strips the comment from Telegram text. On `agent_end`, it maps each `telegram_voice` action to a provider call, generates one file per action, and sends each file as an independent Telegram-native voice message. Prefer `{text}`, `{text|lang}`, or `{text|lang|rate}`. Use JSON when multiline content, named fields, or escaping earns the extra syntax; equivalent JSON `text` or `value` supplies the spoken payload, with explicit `text` taking precedence. Each comment creates one voice artifact, so voice cells do not accept button-style matrix composition. The opening marker must start at column zero on a top-level line outside fenced code, quotes, lists, and indented examples; otherwise it does not activate a voice action and is still removed from the Telegram surface.
|
|
143
143
|
|
|
144
144
|
## Buttons Markup
|
|
145
145
|
|
|
@@ -155,10 +155,10 @@ I can continue.
|
|
|
155
155
|
|
|
156
156
|
Rules:
|
|
157
157
|
|
|
158
|
-
- The payload may be a JSON object, adaptive JSON/CML matrix, or positional [Compact Matrix Literal](./compact-matrix-literal.md). Named JSON objects and positional cells may coexist in one matrix or row. Commas are optional between completed elements, and one trailing comma before a closing delimiter is tolerated; JSON object validation likewise tolerates trailing commas but does not invent missing values, property names, or internal separators. CML uses `{value}`, `{label|prompt}`,
|
|
159
|
-
- Use `label` plus `prompt`, or the compact `value` key when both strings are identical. If only `label`, only `prompt`,
|
|
160
|
-
- The opening marker must start at column zero on a top-level line outside fenced code, quotes, lists, and indented examples; otherwise it
|
|
161
|
-
- Prefer one matrix comment for multiple buttons. Each top-level JSON object or CML cell becomes one full-width inline-keyboard row in source order; a nested row groups one or more buttons horizontally. The parser imposes no artificial per-row width cap; empty rows, malformed cells, unknown/trailing CML escapes, a third unescaped CML separator, empty atoms, unknown selected styles, and deeper nesting are rejected atomically. Generated surfaces default to five columns and expand to six through eight only for short position-bearing labels. Repeated singular comments remain valid.
|
|
158
|
+
- The payload may be a JSON object, adaptive JSON/CML matrix, or positional [Compact Matrix Literal](./compact-matrix-literal.md). Named JSON objects and positional cells may coexist in one matrix or row. Commas are optional between completed elements, and one trailing comma before a closing delimiter is tolerated; JSON object validation likewise tolerates trailing commas but does not invent missing values, property names, or internal separators. CML uses `{value}`, `{label|prompt}`, `{|prompt}`, or the corresponding three-atom form with `selected_style`; the optional third atom requires a non-empty prompt and accepts only `primary`, `success`, or `danger`. It trims every atom, preserves non-structural printable text, and decodes only `\|`, `\}`, and `\\`.
|
|
159
|
+
- Use `label` plus `prompt`, or the compact `value` key when both strings are identical. If only `label`, only `prompt`, one-field `{value}`, or prompt-only `{|prompt}` is present, that string supplies both visible label and queued prompt. An explicit counterpart takes precedence over `value`. Use JSON with `\n` escapes for multiline prompts.
|
|
160
|
+
- The opening marker must start at column zero on a top-level line outside fenced code, quotes, lists, and indented examples; otherwise it does not activate a button action and is still removed from the Telegram surface.
|
|
161
|
+
- Prefer one matrix comment for multiple buttons. Each top-level JSON object or CML cell becomes one full-width inline-keyboard row in source order; a nested row groups one or more buttons horizontally. The parser imposes no artificial per-row width cap; empty rows, malformed cells, unknown/trailing CML escapes, a third unescaped CML separator, empty prompt/style atoms, empty one-atom cells, unknown selected styles, and deeper nesting are rejected atomically. Only the first label position may be empty in a two- or three-atom button cell. Generated surfaces default to five columns and expand to six through eight only for short position-bearing labels. Repeated singular comments remain valid.
|
|
162
162
|
- Button actions are stored in memory with short `callback_data`; Telegram never sees the full prompt in the button payload.
|
|
163
163
|
- After Telegram accepts a generated button callback as a queued prompt, the bridge changes that exact button to its configured selection style without changing agent-authored text or emoji. Set `selected_style` to `primary` (blue), `success` (green), or `danger` (red); omitted or invalid values fall back to `primary`. The style never suppresses queue admission. Other choices stay visually unchanged and remain available; the callback acknowledgement remains the fallback on clients that do not render button styles.
|
|
164
164
|
- When generated button markup is the entire assistant reply, the bridge supplies the standard `☑️ **Choose an option:**` heading as visible message text so Telegram has a message to which it can attach the inline keyboard.
|
|
@@ -61,12 +61,14 @@ This command surface is a mobile companion subset, not a raw terminal-command br
|
|
|
61
61
|
|
|
62
62
|
### Tools and assistant-authored actions
|
|
63
63
|
|
|
64
|
+
Every assistant-authored HTML comment is transport-private on Telegram: previews and final replies remove `<!-- … -->` blocks regardless of Markdown nesting or which extension owns the comment, while only recognized top-level column-zero comments can activate voice or buttons. Unclosed comment tails are withheld and a comment-only result sends no text message; Pi's terminal transcript remains unchanged.
|
|
65
|
+
|
|
64
66
|
- `telegram_bind({ app, script, argument? } | { app, method, argument? })` installs and initializes one canonical managed Generative App module under `<agent-dir>/genapps/<app>/<app>.mjs`, or invokes one named method on an installed app. Installation rejects silent replacement and noncanonical/symlink sources. Methods receive immutable JSON state, one optional JSON argument, cancellation, revision, and a bounded non-shell process port; successful state changes commit to `state.json` plus `states.jsonl`, while output-only methods leave history unchanged. After one-shot `tgbtn` resolution, a complete `app::method` or `app::method(<strict JSON>)` prompt invokes the installed app before Pi queue admission and sends its planned Markdown/buttons directly; malformed or failed bound actions never fall back to a model prompt. Direct app-output buttons retain hidden source revisions and stale actions fail before method execution; sibling processes serialize transitions and recover dead lock owners. Bound actions send a fresh message by default and retain the clicked button's selected state on its prior surface. A result may opt into `viewMode: "edit"` to replace the callback message and keyboard in place, with one fresh-send fallback only for that explicit action. Agent-mediated initial-surface revisions, process-birth lock proof, automatic refresh, and voice output remain open.
|
|
65
67
|
- `telegram_attach(paths, chat_id?, thread_id?, caption?)` is the stable artifact delivery tool for generated files. During Telegram turns it queues files for the active reply; with `assistant.rendering: "rich"`, exactly one PNG/JPEG, MP4, or MP3 artifact plus non-empty final Markdown can become one reply-anchored Rich Message. HTML mode, multiple/unsupported files, Guest Mode, and voice outputs retain their established paths. Outside Telegram turns the tool sends files directly to the paired/default chat, the registered follower's assigned thread, or an explicit `chat_id` plus optional `thread_id` when this Pi instance owns `/telegram-connect` or is registered with the multi-instance bus.
|
|
66
68
|
- `telegram_message(text, chat_id?, thread_id?)` sends a direct Telegram Markdown message when this Pi instance owns `/telegram-connect` or is registered with the multi-instance bus. During an active Telegram turn, omitted targeting and an explicit target equal to that turn are rejected so the ordinary final-reply path remains the sole current-target response; an explicit different chat/thread target remains allowed for requested cross-target delivery. Outside active turns, paired/default local/TUI delivery remains unchanged. Top-level `telegram_button` comments inside `text` are parsed with the same planner used for normal replies and attached to that message; buttons are never standalone Telegram messages.
|
|
67
69
|
- The bundled `telegram-bridge` Skill owns action syntax, target routing, Threaded Mode, formatting, Generative App operation, and profile-specific debugging guidance. The regular prompt routes applicable turns to that Skill. `telegram_attach`, `telegram_bind`, and `telegram_message` remain registered but are model-active only while this instance owns direct transport or holds a live follower registration; disconnect/loss suppresses their schemas and prompt metadata, and recovery restores only the operator's previously active pi-telegram subset.
|
|
68
70
|
- `telegram_voice` hidden comments request Telegram-native voice delivery through `{text}`, `{text|lang}`, `{text|lang|rate}`, or a JSON object. JSON is the fallback for multiline content, named fields, or escaping; equivalent `text` or `value` supplies the spoken payload, with explicit `text` taking precedence.
|
|
69
|
-
- `telegram_button` hidden comments create inline buttons whose taps enqueue prompts. One marker accepts a JSON object, adaptive JSON/CML matrix, or positional [Compact Matrix Literal](./compact-matrix-literal.md). Named JSON objects and positional cells may coexist in one matrix or row; separators are optional and one trailing comma is tolerated at matrix, row, and JSON-object boundaries. Top-level cells become full-width rows, while nested rows group one or more buttons horizontally without an artificial parser-width cap. CML uses `{value}`, `{label|prompt}`,
|
|
71
|
+
- `telegram_button` hidden comments create inline buttons whose taps enqueue prompts. One marker accepts a JSON object, adaptive JSON/CML matrix, or positional [Compact Matrix Literal](./compact-matrix-literal.md). Named JSON objects and positional cells may coexist in one matrix or row; separators are optional and one trailing comma is tolerated at matrix, row, and JSON-object boundaries. Top-level cells become full-width rows, while nested rows group one or more buttons horizontally without an artificial parser-width cap. CML uses `{value}`, `{label|prompt}`, prompt-only `{|prompt}`, or the corresponding three-atom form with `selected_style`; an omitted label uses the existing prompt-as-label fallback, and the optional third atom requires a non-empty prompt and accepts only `primary`, `success`, or `danger`. It trims atom boundaries and supports only the minimal escapes `\|`, `\}`, and `\\`. Prefer one matrix comment for multiple buttons. Use JSON `label` plus `prompt`, or `value` when both strings are identical. Action markers are colon-free; colon-prefixed payloads are rejected. Use top-level column-zero comments outside code, quotes, lists, and indented examples; do not emit standalone button actions.
|
|
70
72
|
|
|
71
73
|
Prompt guidance is context-aware: local/TUI prompts see only explicit direct-delivery guidance, while Telegram-originated turns receive the full action-comment syntax and phone-width output contract.
|
|
72
74
|
|
|
@@ -276,22 +276,27 @@ function parseTelegramButtonCompactActionPayload(
|
|
|
276
276
|
atoms: readonly string[],
|
|
277
277
|
): Record<string, unknown> | undefined {
|
|
278
278
|
const [label, prompt, selectedStyle] = atoms;
|
|
279
|
-
if (atoms.length === 1) return { value: label };
|
|
280
|
-
if (
|
|
279
|
+
if (atoms.length === 1) return label ? { value: label } : undefined;
|
|
280
|
+
if (!prompt) return undefined;
|
|
281
|
+
const action = label ? { label, prompt } : { prompt };
|
|
282
|
+
if (atoms.length === 2) return action;
|
|
281
283
|
if (
|
|
282
284
|
selectedStyle !== "primary" &&
|
|
283
285
|
selectedStyle !== "success" &&
|
|
284
286
|
selectedStyle !== "danger"
|
|
285
287
|
) return undefined;
|
|
286
|
-
return {
|
|
288
|
+
return { ...action, selected_style: selectedStyle };
|
|
287
289
|
}
|
|
288
290
|
|
|
289
291
|
function parseTelegramVoiceCompactActionPayload(
|
|
290
292
|
atoms: readonly string[],
|
|
291
293
|
): Record<string, unknown> | undefined {
|
|
292
294
|
const [text, lang, rate] = atoms;
|
|
295
|
+
if (!text) return undefined;
|
|
293
296
|
if (atoms.length === 1) return { text };
|
|
297
|
+
if (!lang) return undefined;
|
|
294
298
|
if (atoms.length === 2) return { text, lang };
|
|
299
|
+
if (!rate) return undefined;
|
|
295
300
|
return { text, lang, rate };
|
|
296
301
|
}
|
|
297
302
|
|
|
@@ -318,7 +323,7 @@ function parseTelegramAdaptiveActionPayloadRows(
|
|
|
318
323
|
};
|
|
319
324
|
const normalizeAtom = (value: string): string | undefined => {
|
|
320
325
|
const normalized = value.trim();
|
|
321
|
-
return
|
|
326
|
+
return !TELEGRAM_COMPACT_ACTION_CONTROL_PATTERN.test(normalized)
|
|
322
327
|
? normalized
|
|
323
328
|
: undefined;
|
|
324
329
|
};
|
|
@@ -551,8 +556,40 @@ export function normalizeMarkdownAfterVoiceExtraction(
|
|
|
551
556
|
return markdown.replace(/\n{3,}/g, "\n\n").trim();
|
|
552
557
|
}
|
|
553
558
|
|
|
559
|
+
function isTelegramCommentOnlyLinePrefix(value: string): boolean {
|
|
560
|
+
return /^[ \t]*(?:(?:>[ \t]*)+)?(?:(?:[-+*]|\d+[.)])[ \t]+)?$/u.test(
|
|
561
|
+
value,
|
|
562
|
+
);
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
function stripTelegramHtmlCommentBlocks(markdown: string): string {
|
|
566
|
+
let result = "";
|
|
567
|
+
let offset = 0;
|
|
568
|
+
while (offset < markdown.length) {
|
|
569
|
+
const start = markdown.indexOf("<!--", offset);
|
|
570
|
+
if (start === -1) return result + markdown.slice(offset);
|
|
571
|
+
const close = markdown.indexOf("-->", start + 4);
|
|
572
|
+
const lineStart = markdown.lastIndexOf("\n", start - 1) + 1;
|
|
573
|
+
const afterComment = close === -1 ? markdown.length : close + 3;
|
|
574
|
+
const newlineAfterComment = markdown.indexOf("\n", afterComment);
|
|
575
|
+
const lineEnd =
|
|
576
|
+
newlineAfterComment === -1 ? markdown.length : newlineAfterComment;
|
|
577
|
+
const commentOwnsLine =
|
|
578
|
+
isTelegramCommentOnlyLinePrefix(markdown.slice(lineStart, start)) &&
|
|
579
|
+
markdown.slice(afterComment, lineEnd).trim().length === 0;
|
|
580
|
+
result += markdown.slice(offset, commentOwnsLine ? lineStart : start);
|
|
581
|
+
if (close === -1) return result;
|
|
582
|
+
offset = commentOwnsLine
|
|
583
|
+
? newlineAfterComment === -1
|
|
584
|
+
? markdown.length
|
|
585
|
+
: newlineAfterComment + 1
|
|
586
|
+
: afterComment;
|
|
587
|
+
}
|
|
588
|
+
return result;
|
|
589
|
+
}
|
|
590
|
+
|
|
554
591
|
export function stripTelegramCommentMarkupForPreview(markdown: string): string {
|
|
555
|
-
const withoutClosedBlocks =
|
|
592
|
+
const withoutClosedBlocks = stripTelegramHtmlCommentBlocks(markdown);
|
|
556
593
|
const openBlockIndex =
|
|
557
594
|
findTopLevelOpenOrPartialHtmlCommentIndex(withoutClosedBlocks);
|
|
558
595
|
const previewMarkdown =
|
|
@@ -565,7 +602,7 @@ export function stripTelegramCommentMarkupForPreview(markdown: string): string {
|
|
|
565
602
|
export function stripTelegramCommentMarkupForDelivery(
|
|
566
603
|
markdown: string,
|
|
567
604
|
): string {
|
|
568
|
-
const withoutClosedBlocks =
|
|
605
|
+
const withoutClosedBlocks = stripTelegramHtmlCommentBlocks(markdown);
|
|
569
606
|
const openBlockIndex =
|
|
570
607
|
findTopLevelOpenOrPartialHtmlCommentIndex(withoutClosedBlocks);
|
|
571
608
|
const deliveryMarkdown =
|
|
@@ -26,6 +26,7 @@ import {
|
|
|
26
26
|
} from "./outbound-buttons.ts";
|
|
27
27
|
import {
|
|
28
28
|
planTelegramVoiceReply,
|
|
29
|
+
stripTelegramCommentMarkupForDelivery,
|
|
29
30
|
type TelegramVoiceReplyItem,
|
|
30
31
|
} from "./outbound-markup.ts";
|
|
31
32
|
import { createTelegramVoiceReplySender as createTelegramVoiceReplySenderWithPorts } from "./outbound-voice.ts";
|
|
@@ -698,7 +699,9 @@ export function createTelegramOutboundTextReplyRuntime<TReplyMarkup = unknown>(
|
|
|
698
699
|
return deps.sendTextReply(chatId, replyToMessageId, transformed, options);
|
|
699
700
|
},
|
|
700
701
|
sendMarkdownReply: async (chatId, replyToMessageId, markdown, options) => {
|
|
701
|
-
const
|
|
702
|
+
const deliveryMarkdown = stripTelegramCommentMarkupForDelivery(markdown);
|
|
703
|
+
if (!deliveryMarkdown) return undefined;
|
|
704
|
+
const transformed = await transformTelegramOutboundTextReply(deliveryMarkdown, {
|
|
702
705
|
handlers: deps.getHandlers?.(),
|
|
703
706
|
cwd: deps.cwd,
|
|
704
707
|
execCommand: deps.execCommand,
|
|
@@ -39,7 +39,7 @@ Reply in concise, phone-width Telegram Rich Markdown. Use `$...$` and `$$...$$`
|
|
|
39
39
|
|
|
40
40
|
## Assistant Actions
|
|
41
41
|
|
|
42
|
-
`telegram_button` and `telegram_voice` are hidden HTML comments, not tools. Emit each complete comment at column zero, outside lists, quotes, code blocks, and indentation.
|
|
42
|
+
`telegram_button` and `telegram_voice` are hidden HTML comments, not tools. Emit each complete action comment at column zero, outside lists, quotes, code blocks, and indentation. Telegram removes every assistant-authored HTML comment from previews and final replies regardless of owner or Markdown position, but only recognized top-level comments activate actions; comment-only output sends no text message.
|
|
43
43
|
|
|
44
44
|
### Shared Encoding Rule
|
|
45
45
|
|
|
@@ -52,11 +52,12 @@ CML trims atom boundaries and decodes `\|`, `\}`, and `\\`. Keep one complete ac
|
|
|
52
52
|
|
|
53
53
|
### Prompt Buttons
|
|
54
54
|
|
|
55
|
-
Every button has a short distinct `emoji + space + text` label
|
|
55
|
+
Every button has a self-contained prompt and an optional selection style. Use a short distinct `emoji + space + text` label when separate human-readable labeling adds meaning; established coordinates or symbolic tokens may use the prompt itself as visible text. A click creates an ordinary user request; it never grants authority or bypasses confirmation.
|
|
56
56
|
|
|
57
57
|
- `{prompt}` uses the same text for label and prompt.
|
|
58
|
+
- `{|prompt}` omits a separately authored label and uses the prompt as both visible text and queued prompt.
|
|
58
59
|
- `{label|prompt}` separates visible label from queued prompt.
|
|
59
|
-
- `{label|prompt|selected_style}`
|
|
60
|
+
- `{label|prompt|selected_style}` and `{|prompt|selected_style}` accept `primary`, `success`, or `danger`.
|
|
60
61
|
- Top-level cells form vertical rows; one nested row groups horizontal peers.
|
|
61
62
|
- Prefer one matrix comment for the complete surface.
|
|
62
63
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@llblab/pi-kit",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.2",
|
|
4
4
|
"private": false,
|
|
5
5
|
"publishConfig": {
|
|
6
6
|
"access": "public"
|
|
@@ -43,7 +43,7 @@
|
|
|
43
43
|
"@llblab/pi-actors": "0.52.0",
|
|
44
44
|
"@llblab/pi-codex-usage": "0.9.4",
|
|
45
45
|
"@llblab/pi-grow-loop": "0.7.3",
|
|
46
|
-
"@llblab/pi-telegram": "0.42.
|
|
46
|
+
"@llblab/pi-telegram": "0.42.2"
|
|
47
47
|
},
|
|
48
48
|
"bundledDependencies": [
|
|
49
49
|
"@llblab/pi-actors",
|