@llblab/pi-telegram 0.14.0 → 0.15.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/CHANGELOG.md CHANGED
@@ -4,6 +4,11 @@
4
4
 
5
5
  No open changes.
6
6
 
7
+ ## 0.15.0: Companion Status Lines
8
+
9
+ - `[API]` Added `registerTelegramStatusLineProvider()` on the public `/status` subpath so companion extensions can append compact rows to the `/start` menu status text. Providers are synchronous, model-aware, isolated on failure, and rendered with Telegram-style capitalized labels. Impact: quota/status widgets can progressively enhance the Telegram operator menu without owning polling, transport, or core menu rendering.
10
+ - `[Docs]` Documented the status-line provider with an abstract companion-extension example and listed `pi-codex-usage` as a companion extension. Impact: the public API docs stay implementation-neutral while the README still points operators to the concrete Codex quota widget.
11
+
7
12
  ## 0.14.0: Direct Telegram Delivery, Queue Semantics, And Section Diagnostics
8
13
 
9
14
  - `[Prompt Guidance]` Tightened agent context for Telegram buttons: use normal Markdown plus top-level hidden `telegram_button` comments, never JSON button specs or standalone button actions, and keep comments out of code/quotes/lists/indented examples. Impact: agents immediately know how to author visible Telegram text, inline buttons, and direct `telegram_message` payloads without transport hacks.
package/README.md CHANGED
@@ -209,7 +209,7 @@ Unknown inline-button callbacks are forwarded to π as `[callback] <data>` when
209
209
 
210
210
  ### Extension Sections
211
211
 
212
- Ordinary pi extensions can register structured UI sections that appear in the main Telegram menu and Settings submenu without owning a second polling loop. Each section gets a narrow typed context with `edit`, `open`, `enqueuePrompt`, `answerCallback`, and `callbackData()` — enough to build interactive Telegram-native surfaces while `pi-telegram` owns transport, callback routing, navigation hierarchy, and diagnostics.
212
+ Ordinary pi extensions can register structured UI sections that appear in the main Telegram menu and Settings submenu without owning a second polling loop. Companion extensions can also register compact status lines for the `/start` menu status text, allowing widgets such as quota indicators to appear beside Status, Usage, Cost, and Context only when relevant to the active model. Each section gets a narrow typed context with `edit`, `open`, `enqueuePrompt`, `answerCallback`, and `callbackData()` — enough to build interactive Telegram-native surfaces while `pi-telegram` owns transport, callback routing, navigation hierarchy, and diagnostics.
213
213
 
214
214
  Import `registerTelegramSection()` from `@llblab/pi-telegram/sections` and return a disposer on shutdown. Sections can send interactive messages directly into the chat via `ctx.open()` — confirmation dialogs, approve/deny gates, and multi-step forms live outside the menu hierarchy while callbacks route through the same typed handler. See [`@llblab/pi-telegram-extension-demo`](https://github.com/llblab/pi-telegram-extension-demo) for a working reference and the [Extension Sections Standard](./docs/sections.md) for the full contract.
215
215
 
@@ -260,6 +260,12 @@ Modes are `hidden`, `always`, and `interval`. `hidden` means no time line is add
260
260
 
261
261
  Third-party extensions that integrate with `pi-telegram`:
262
262
 
263
+ - [`pi-codex-usage`](https://github.com/llblab/pi-codex-usage) — Compact Codex subscription quota/status widget for the Pi statusline and the inline menu status text opened by `/start`.
264
+
265
+ ```bash
266
+ pi install npm:@llblab/pi-codex-usage
267
+ ```
268
+
263
269
  - [`pi-telegram-tool-status`](https://github.com/Timur00Kh/pi-telegram-tool-status) — Live-updating service messages that list tools used by the agent. It keeps one message per Telegram prompt and edits it in place as tools execute.
264
270
 
265
271
  ```bash
package/api/status.ts ADDED
@@ -0,0 +1,12 @@
1
+ /**
2
+ * Public Telegram status API
3
+ * Zones: package boundary, companion extension interop
4
+ * Exposes compact status-menu line registration for companion extensions while keeping status rendering internals package-private
5
+ */
6
+
7
+ export {
8
+ registerTelegramStatusLineProvider,
9
+ type TelegramStatusLineProvider,
10
+ type TelegramStatusLineProviderContext,
11
+ type TelegramStatusLineProviderResult,
12
+ } from "../lib/status.ts";
@@ -16,6 +16,7 @@ Preferred public imports:
16
16
  ```ts
17
17
  import telegram from "@llblab/pi-telegram";
18
18
  import { registerTelegramSection } from "@llblab/pi-telegram/sections";
19
+ import { registerTelegramStatusLineProvider } from "@llblab/pi-telegram/status";
19
20
  import { registerTelegramUpdateHandler } from "@llblab/pi-telegram/updates";
20
21
  import { registerTelegramInboundHandler } from "@llblab/pi-telegram/inbound";
21
22
  import { registerTelegramOutboundHandler } from "@llblab/pi-telegram/outbound";
@@ -102,6 +103,9 @@ High-level stable APIs:
102
103
  - `registerTelegramSection()`
103
104
  - Identity: required `id`.
104
105
  - Purpose: managed menu/settings UI surfaces.
106
+ - `registerTelegramStatusLineProvider()`
107
+ - Identity: required `id`.
108
+ - Purpose: compact companion status rows in the `/start` menu status text.
105
109
  - `registerTelegramVoiceTranscriptionProvider()`
106
110
  - Identity: required stable `id` for new code.
107
111
  - Purpose: STT fallback for voice/audio input.
@@ -165,6 +169,27 @@ Contract:
165
169
 
166
170
  Full behavior: [Extension Sections](./sections.md).
167
171
 
172
+ ## Status Lines
173
+
174
+ Import from `@llblab/pi-telegram/status`.
175
+
176
+ ```ts
177
+ const off = registerTelegramStatusLineProvider(
178
+ ({ activeModel }) => {
179
+ if (activeModel?.provider !== "example-provider") return undefined;
180
+ return { label: "service", value: "ready" };
181
+ },
182
+ { id: "@scope/example-status" },
183
+ );
184
+ ```
185
+
186
+ Contract:
187
+
188
+ - Providers are synchronous because `/start` status text is rendered inline with the menu.
189
+ - Return `undefined` when the line is not relevant for the active model.
190
+ - Provider failures are isolated and skipped so optional companion status cannot break the core Telegram menu.
191
+ - The bridge renders rows as `<Label>: <value>` in the same HTML status block as Status, Usage, Cost, and Context, capitalizing the first label character for Telegram UI consistency.
192
+
168
193
  ## Updates
169
194
 
170
195
  Import from `@llblab/pi-telegram/updates`.
package/lib/status.ts CHANGED
@@ -36,9 +36,24 @@ interface TelegramContextUsage {
36
36
  }
37
37
 
38
38
  export interface TelegramStatusActiveModel {
39
+ provider?: string;
40
+ id?: string;
39
41
  contextWindow?: number;
40
42
  }
41
43
 
44
+ export interface TelegramStatusLineProviderContext {
45
+ activeModel: TelegramStatusActiveModel | undefined;
46
+ }
47
+
48
+ export interface TelegramStatusLineProviderResult {
49
+ label: string;
50
+ value: string;
51
+ }
52
+
53
+ export type TelegramStatusLineProvider = (
54
+ ctx: TelegramStatusLineProviderContext,
55
+ ) => TelegramStatusLineProviderResult | undefined;
56
+
42
57
  export interface TelegramStatusContext {
43
58
  sessionManager: { getEntries(): TelegramStatusSessionEntry[] };
44
59
  getContextUsage(): TelegramContextUsage | undefined;
@@ -52,6 +67,8 @@ export interface TelegramStatusContext {
52
67
 
53
68
  export type TelegramRuntimeEventDetailValue = string | number | boolean | null;
54
69
 
70
+ const TELEGRAM_STATUS_LINE_PROVIDER_REGISTRY_KEY =
71
+ "__piTelegramStatusLineProviders__";
55
72
  const MAX_RECENT_TELEGRAM_RUNTIME_EVENTS = 10;
56
73
  const MAX_TELEGRAM_RUNTIME_EVENT_MESSAGE_LENGTH = 1000;
57
74
  const MAX_TELEGRAM_RUNTIME_EVENT_DETAIL_LENGTH = 1000;
@@ -166,7 +183,10 @@ export interface TelegramStatusRuntime<
166
183
  getStatusLines: () => string[];
167
184
  }
168
185
 
169
- function truncateTelegramRuntimeEventText(text: string, maxLength: number): string {
186
+ function truncateTelegramRuntimeEventText(
187
+ text: string,
188
+ maxLength: number,
189
+ ): string {
170
190
  if (text.length <= maxLength) return text;
171
191
  return `${text.slice(0, maxLength).trimEnd()}… [truncated ${text.length - maxLength} chars]`;
172
192
  }
@@ -262,6 +282,61 @@ export function recordTelegramRuntimeEvent(
262
282
  recordStructuredTelegramRuntimeEvent(events, { category, error }, options);
263
283
  }
264
284
 
285
+ function getOrCreateTelegramStatusLineProviderRegistry(): Map<
286
+ string,
287
+ TelegramStatusLineProvider
288
+ > {
289
+ const existing = (globalThis as Record<string, unknown>)[
290
+ TELEGRAM_STATUS_LINE_PROVIDER_REGISTRY_KEY
291
+ ];
292
+ if (existing instanceof Map)
293
+ return existing as Map<string, TelegramStatusLineProvider>;
294
+ const registry = new Map<string, TelegramStatusLineProvider>();
295
+ (globalThis as Record<string, unknown>)[
296
+ TELEGRAM_STATUS_LINE_PROVIDER_REGISTRY_KEY
297
+ ] = registry;
298
+ return registry;
299
+ }
300
+
301
+ /**
302
+ * Register a compact companion-extension line for the Telegram status menu.
303
+ *
304
+ * Providers are synchronous and should return undefined when their line is not
305
+ * relevant for the active model. Errors are isolated so optional companion
306
+ * status cannot break the core Telegram menu.
307
+ */
308
+ export function registerTelegramStatusLineProvider(
309
+ provider: TelegramStatusLineProvider,
310
+ options: { id: string },
311
+ ): () => void {
312
+ const registry = getOrCreateTelegramStatusLineProviderRegistry();
313
+ registry.set(options.id, provider);
314
+ return () => {
315
+ if (registry.get(options.id) === provider) registry.delete(options.id);
316
+ };
317
+ }
318
+
319
+ export function getTelegramStatusLineProviderResults(
320
+ ctx: TelegramStatusLineProviderContext,
321
+ ): TelegramStatusLineProviderResult[] {
322
+ const results: TelegramStatusLineProviderResult[] = [];
323
+ const registry = getOrCreateTelegramStatusLineProviderRegistry();
324
+ for (const provider of registry.values()) {
325
+ try {
326
+ const result = provider(ctx);
327
+ if (!result?.label || !result.value) continue;
328
+ results.push(result);
329
+ } catch {
330
+ continue;
331
+ }
332
+ }
333
+ return results;
334
+ }
335
+
336
+ export function clearTelegramStatusLineProviders(): void {
337
+ getOrCreateTelegramStatusLineProviderRegistry().clear();
338
+ }
339
+
265
340
  export function createTelegramRuntimeEventRecorder(
266
341
  options: TelegramRuntimeEventRecorderOptions,
267
342
  ): TelegramRuntimeEventRecorder {
@@ -314,7 +389,9 @@ function formatTelegramRuntimeEvent(event: TelegramRuntimeEvent): string {
314
389
  return `${new Date(event.at).toISOString()} ${formatTelegramRuntimeEventSummary(event)}`;
315
390
  }
316
391
 
317
- function buildTelegramRuntimeEventSummary(events: TelegramRuntimeEvent[]): string {
392
+ function buildTelegramRuntimeEventSummary(
393
+ events: TelegramRuntimeEvent[],
394
+ ): string {
318
395
  const counts = new Map<string, number>();
319
396
  for (const event of events) {
320
397
  const category = formatTelegramRuntimeEventCategory(event);
@@ -553,8 +630,13 @@ function collectUsageStats(ctx: TelegramStatusContext): TelegramUsageStats {
553
630
  return stats;
554
631
  }
555
632
 
633
+ function formatStatusRowLabel(label: string): string {
634
+ if (!label) return label;
635
+ return `${label[0]?.toUpperCase() ?? ""}${label.slice(1)}`;
636
+ }
637
+
556
638
  function buildStatusRow(label: string, value: string): string {
557
- return `<b>${escapeHtml(label)}:</b> <code>${escapeHtml(value)}</code>`;
639
+ return `<b>${escapeHtml(formatStatusRowLabel(label))}:</b> <code>${escapeHtml(value)}</code>`;
558
640
  }
559
641
 
560
642
  function buildUsageSummary(stats: TelegramUsageStats): string | undefined {
@@ -613,5 +695,8 @@ export function buildStatusHtml(
613
695
  lines.push(buildStatusRow("Cost", costSummary));
614
696
  }
615
697
  lines.push(buildStatusRow("Context", buildContextSummary(ctx, activeModel)));
698
+ for (const row of getTelegramStatusLineProviderResults({ activeModel })) {
699
+ lines.push(buildStatusRow(row.label, row.value));
700
+ }
616
701
  return lines.join("\n");
617
702
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@llblab/pi-telegram",
3
- "version": "0.14.0",
3
+ "version": "0.15.0",
4
4
  "private": false,
5
5
  "publishConfig": {
6
6
  "access": "public"
@@ -50,6 +50,7 @@
50
50
  "./outbound": "./api/outbound.ts",
51
51
  "./updates": "./api/updates.ts",
52
52
  "./sections": "./api/sections.ts",
53
+ "./status": "./api/status.ts",
53
54
  "./voice": "./api/voice.ts",
54
55
  "./keyboard": "./api/keyboard.ts"
55
56
  },