@llblab/pi-telegram 0.9.2 → 0.9.3

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
@@ -1,5 +1,11 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.9.3: External Handlers Rename
4
+
5
+ - `[External Handlers]` Renamed the external update handlers domain to `external-handlers` across source, tests, and docs. Impact: the interop domain now has a cleaner name aligned with inbound/outbound handler naming.
6
+ - `[Breaking]` Removed the old `external-update-handlers` module/doc path and old exported update/interceptor aliases. Impact: layered extensions should import from `@llblab/pi-telegram/lib/external-handlers.ts` and use the `TelegramExternalHandler*` names.
7
+ - `[Package]` Bumped package metadata to `0.9.3` and kept the lockfile in sync.
8
+
3
9
  ## 0.9.2: External Update Interceptors
4
10
 
5
11
  - `[External Update Interceptors]` Added a versioned `globalThis` registry that lets layered pi extensions observe and optionally consume Telegram updates before pi-telegram's default routing. Impact: approval gates and other same-process extensions can react synchronously to Telegram callbacks without owning a second bot poller.
package/README.md CHANGED
@@ -224,7 +224,7 @@ List the main risks first.
224
224
  <!-- telegram_button: OK -->
225
225
  ```
226
226
 
227
- Button prompts are routed back into the normal Telegram queue as prompt turns. Keep the opening comment unclosed until the body-ending `-->` for body-form buttons. Closed heads must use `prompt="..."` or the colon shorthand to create a button. Unknown inline-button callbacks that do not belong to pi-telegram are forwarded to π as `[callback] <data>` so other extensions can namespace and handle their own Telegram buttons without polling the bot themselves; see the [Callback Namespace Standard](./docs/callback-namespaces.md). Layered extensions that need to react to Telegram updates synchronously inside their own runtime (for example, to resolve a blocking-tool approval Promise the moment a callback arrives) can register a runtime interceptor on the shared update registry; see [External Update Handlers](./docs/external-update-handlers.md). Outbound handler details are documented in [`docs/outbound-handlers.md`](./docs/outbound-handlers.md).
227
+ Button prompts are routed back into the normal Telegram queue as prompt turns. Keep the opening comment unclosed until the body-ending `-->` for body-form buttons. Closed heads must use `prompt="..."` or the colon shorthand to create a button. Unknown inline-button callbacks that do not belong to pi-telegram are forwarded to π as `[callback] <data>` so other extensions can namespace and handle their own Telegram buttons without polling the bot themselves; see the [Callback Namespace Standard](./docs/callback-namespaces.md). Layered extensions that need to react to Telegram updates synchronously inside their own runtime (for example, to resolve a blocking-tool approval Promise the moment a callback arrives) can register a runtime interceptor on the shared update registry; see [External Handlers](./docs/external-handlers.md). Outbound handler details are documented in [`docs/outbound-handlers.md`](./docs/outbound-handlers.md).
228
228
 
229
229
  ## Streaming
230
230
 
package/docs/README.md CHANGED
@@ -10,4 +10,4 @@ Living index of project documentation in `/docs`.
10
10
  - [outbound-handlers.md](./outbound-handlers.md) — Local `pi-telegram` outbound-handler config, text/voice/button behavior, artifact outputs, and callback routing
11
11
  - [locks.md](./locks.md) — Shared `locks.json` standard for singleton extension ownership
12
12
  - [callback-namespaces.md](./callback-namespaces.md) — Shared Telegram `callback_data` namespace standard for layered extensions
13
- - [external-update-handlers.md](./external-update-handlers.md) — Runtime interceptor registry that lets layered extensions observe and consume Telegram updates without owning their own polling connection
13
+ - [external-handlers.md](./external-handlers.md) — Runtime interceptor registry that lets layered extensions observe and consume Telegram updates without owning their own polling connection
@@ -1,4 +1,4 @@
1
- # External Update Handlers
1
+ # External Handlers
2
2
 
3
3
  `pi-telegram` owns a single `getUpdates` long-poll connection per bot. Other
4
4
  pi extensions cannot open a competing poller against the same bot — the
@@ -12,7 +12,7 @@ fires.
12
12
 
13
13
  It is the runtime counterpart to
14
14
  [Callback Namespaces](./callback-namespaces.md): callback namespaces define
15
- how to share `callback_data` cleanly; external update handlers define how to
15
+ how to share `callback_data` cleanly; external handlers define how to
16
16
  observe and optionally short-circuit the dispatch of those updates.
17
17
 
18
18
  ## When to use it
@@ -63,9 +63,9 @@ Two equivalent paths.
63
63
  ### Typed import (recommended when you can depend on `@llblab/pi-telegram`)
64
64
 
65
65
  ```ts
66
- import { onTelegramUpdate } from "@llblab/pi-telegram/lib/external-update-handlers.ts";
66
+ import { onTelegramExternalUpdate } from "@llblab/pi-telegram/lib/external-handlers.ts";
67
67
 
68
- const off = onTelegramUpdate(async (update) => {
68
+ const off = onTelegramExternalUpdate(async (update) => {
69
69
  const cb = (update as { callback_query?: { id?: string; data?: string } })
70
70
  .callback_query;
71
71
  if (!cb?.data?.startsWith("myext:")) return "pass";
@@ -83,7 +83,7 @@ When the layered extension prefers no `import` from `@llblab/pi-telegram` (so
83
83
  load order between the two extensions does not matter, and either can be
84
84
  installed first), it must implement the **full v1 registry contract**, not
85
85
  just `version` and `add`. pi-telegram's polling runtime calls `dispatch` on
86
- whatever object it finds at `globalThis.__piTelegramExternalUpdateRegistry__`,
86
+ whatever object it finds at `globalThis.__piTelegramExternalHandlerRegistry__`,
87
87
  so a partial object would silently break the first update.
88
88
 
89
89
  pi-telegram defensively re-creates the registry if the object on `globalThis`
@@ -100,19 +100,19 @@ type PiTelegramVerdict =
100
100
  | Promise<"consume" | "pass" | void>;
101
101
  type PiTelegramInterceptor = (update: unknown) => PiTelegramVerdict;
102
102
 
103
- interface PiTelegramExternalUpdateRegistry {
103
+ interface PiTelegramExternalHandlerRegistry {
104
104
  readonly version: 1;
105
105
  add: (handler: PiTelegramInterceptor) => () => void;
106
106
  // Required: pi-telegram's polling loop calls this on every update.
107
107
  dispatch: (update: unknown) => Promise<"consume" | "pass">;
108
108
  }
109
109
 
110
- const REGISTRY_KEY = "__piTelegramExternalUpdateRegistry__";
110
+ const REGISTRY_KEY = "__piTelegramExternalHandlerRegistry__";
111
111
 
112
- function getOrCreateRegistry(): PiTelegramExternalUpdateRegistry {
112
+ function getOrCreateRegistry(): PiTelegramExternalHandlerRegistry {
113
113
  const g = globalThis as Record<string, unknown>;
114
114
  const existing = g[REGISTRY_KEY] as
115
- | PiTelegramExternalUpdateRegistry
115
+ | PiTelegramExternalHandlerRegistry
116
116
  | undefined;
117
117
  if (
118
118
  existing &&
@@ -123,7 +123,7 @@ function getOrCreateRegistry(): PiTelegramExternalUpdateRegistry {
123
123
  return existing;
124
124
  }
125
125
  const handlers = new Set<PiTelegramInterceptor>();
126
- const registry: PiTelegramExternalUpdateRegistry = {
126
+ const registry: PiTelegramExternalHandlerRegistry = {
127
127
  version: 1,
128
128
  add(handler) {
129
129
  handlers.add(handler);
@@ -151,7 +151,7 @@ const off = getOrCreateRegistry().add((update) => {
151
151
  });
152
152
  ```
153
153
 
154
- The registry object on `globalThis.__piTelegramExternalUpdateRegistry__` is
154
+ The registry object on `globalThis.__piTelegramExternalHandlerRegistry__` is
155
155
  versioned (`version: 1`) and stable across pi-telegram releases; future
156
156
  breaking changes will use a new schema version and a new key.
157
157
 
package/index.ts CHANGED
@@ -8,7 +8,7 @@ import * as Api from "./lib/api.ts";
8
8
  import * as CommandTemplates from "./lib/command-templates.ts";
9
9
  import * as Commands from "./lib/commands.ts";
10
10
  import * as Config from "./lib/config.ts";
11
- import { createTelegramInterceptedHandleUpdate } from "./lib/external-update-handlers.ts";
11
+ import { createTelegramExternalHandleUpdate } from "./lib/external-handlers.ts";
12
12
  import * as InboundHandlers from "./lib/inbound-handlers.ts";
13
13
  import * as Keyboard from "./lib/keyboard.ts";
14
14
  import * as Lifecycle from "./lib/lifecycle.ts";
@@ -359,7 +359,7 @@ export default function (pi: Pi.ExtensionAPI) {
359
359
  deleteWebhook,
360
360
  getUpdates,
361
361
  persistConfig: configStore.persist,
362
- handleUpdate: createTelegramInterceptedHandleUpdate({
362
+ handleUpdate: createTelegramExternalHandleUpdate({
363
363
  defaultHandle: inboundRouteRuntime.handleUpdate,
364
364
  }),
365
365
  stopTypingLoop: typing.stop,
@@ -1,5 +1,5 @@
1
1
  /**
2
- * External Telegram update interceptor registry
2
+ * External Telegram handler registry
3
3
  * Zones: telegram transport, layered extension interop
4
4
  * Lets other pi extensions hook into the polling loop without owning their own getUpdates connection
5
5
  */
@@ -10,16 +10,16 @@
10
10
  * - `"consume"` — the interceptor handled this update; pi-telegram skips default routing.
11
11
  * - `"pass"` (or `void`/`undefined`) — pi-telegram routes the update normally.
12
12
  */
13
- export type TelegramExternalUpdateVerdict = "consume" | "pass";
13
+ export type TelegramExternalHandlerVerdict = "consume" | "pass";
14
14
 
15
- export type TelegramExternalUpdateInterceptor = (
15
+ export type TelegramExternalHandler = (
16
16
  update: unknown,
17
17
  ) =>
18
- | TelegramExternalUpdateVerdict
18
+ | TelegramExternalHandlerVerdict
19
19
  | void
20
- | Promise<TelegramExternalUpdateVerdict | void>;
20
+ | Promise<TelegramExternalHandlerVerdict | void>;
21
21
 
22
- export interface TelegramExternalUpdateRegistry {
22
+ export interface TelegramExternalHandlerRegistry {
23
23
  /** Schema version of this registry shape. */
24
24
  readonly version: 1;
25
25
  /**
@@ -29,17 +29,17 @@ export interface TelegramExternalUpdateRegistry {
29
29
  * before pi-telegram's own routing. The first interceptor that returns
30
30
  * `"consume"` wins and stops the chain for that update.
31
31
  */
32
- add: (handler: TelegramExternalUpdateInterceptor) => () => void;
32
+ add: (handler: TelegramExternalHandler) => () => void;
33
33
  /**
34
34
  * Run all registered interceptors against an update.
35
35
  *
36
36
  * Used by pi-telegram's polling runtime; layered extensions should call
37
- * {@link onTelegramUpdate} or `add` instead of dispatching directly.
37
+ * {@link onTelegramExternalUpdate} or `add` instead of dispatching directly.
38
38
  */
39
- dispatch: (update: unknown) => Promise<TelegramExternalUpdateVerdict>;
39
+ dispatch: (update: unknown) => Promise<TelegramExternalHandlerVerdict>;
40
40
  }
41
41
 
42
- const REGISTRY_KEY = "__piTelegramExternalUpdateRegistry__";
42
+ const REGISTRY_KEY = "__piTelegramExternalHandlerRegistry__";
43
43
 
44
44
  /**
45
45
  * Validate that a value on `globalThis` matches the full v1 registry contract.
@@ -55,9 +55,9 @@ const REGISTRY_KEY = "__piTelegramExternalUpdateRegistry__";
55
55
  */
56
56
  function isValidV1Registry(
57
57
  candidate: unknown,
58
- ): candidate is TelegramExternalUpdateRegistry {
58
+ ): candidate is TelegramExternalHandlerRegistry {
59
59
  if (!candidate || typeof candidate !== "object") return false;
60
- const r = candidate as Partial<TelegramExternalUpdateRegistry>;
60
+ const r = candidate as Partial<TelegramExternalHandlerRegistry>;
61
61
  return (
62
62
  r.version === 1 &&
63
63
  typeof r.add === "function" &&
@@ -65,12 +65,12 @@ function isValidV1Registry(
65
65
  );
66
66
  }
67
67
 
68
- function getOrCreateRegistry(): TelegramExternalUpdateRegistry {
68
+ function getOrCreateRegistry(): TelegramExternalHandlerRegistry {
69
69
  const g = globalThis as Record<string, unknown>;
70
70
  const existing = g[REGISTRY_KEY];
71
71
  if (isValidV1Registry(existing)) return existing;
72
- const handlers = new Set<TelegramExternalUpdateInterceptor>();
73
- const registry: TelegramExternalUpdateRegistry = {
72
+ const handlers = new Set<TelegramExternalHandler>();
73
+ const registry: TelegramExternalHandlerRegistry = {
74
74
  version: 1,
75
75
  add(handler) {
76
76
  handlers.add(handler);
@@ -95,16 +95,18 @@ function getOrCreateRegistry(): TelegramExternalUpdateRegistry {
95
95
  /**
96
96
  * Called by pi-telegram's own runtime to obtain the registry it dispatches
97
97
  * through. Layered extensions should not call this; use
98
- * {@link onTelegramUpdate} instead.
98
+ * {@link onTelegramExternalUpdate} instead.
99
99
  */
100
- export function getTelegramExternalUpdateRegistry(): TelegramExternalUpdateRegistry {
100
+ export function getTelegramExternalHandlerRegistry(): TelegramExternalHandlerRegistry {
101
101
  return getOrCreateRegistry();
102
102
  }
103
103
 
104
- export interface TelegramExternalInterceptorWrapDeps<TUpdate, TContext> {
104
+ export interface TelegramExternalHandlerWrapDeps<TUpdate, TContext> {
105
105
  defaultHandle: (update: TUpdate, ctx: TContext) => Promise<void>;
106
- registry?: TelegramExternalUpdateRegistry;
106
+ registry?: TelegramExternalHandlerRegistry;
107
107
  }
108
+ export type TelegramExternalInterceptorWrapDeps<TUpdate, TContext> =
109
+ TelegramExternalHandlerWrapDeps<TUpdate, TContext>;
108
110
 
109
111
  /**
110
112
  * Wrap a default polling `handleUpdate` with the external interceptor registry.
@@ -115,8 +117,8 @@ export interface TelegramExternalInterceptorWrapDeps<TUpdate, TContext> {
115
117
  * Composition-root callers (pi-telegram's `index.ts`) should use this builder
116
118
  * instead of writing the lifting logic inline.
117
119
  */
118
- export function createTelegramInterceptedHandleUpdate<TUpdate, TContext>(
119
- deps: TelegramExternalInterceptorWrapDeps<TUpdate, TContext>,
120
+ export function createTelegramExternalHandleUpdate<TUpdate, TContext>(
121
+ deps: TelegramExternalHandlerWrapDeps<TUpdate, TContext>,
120
122
  ): (update: TUpdate, ctx: TContext) => Promise<void> {
121
123
  const registry = deps.registry ?? getOrCreateRegistry();
122
124
  const { defaultHandle } = deps;
@@ -140,9 +142,9 @@ export function createTelegramInterceptedHandleUpdate<TUpdate, TContext>(
140
142
  *
141
143
  * @example
142
144
  * ```ts
143
- * import { onTelegramUpdate } from "@llblab/pi-telegram/lib/external-update-handlers.ts";
145
+ * import { onTelegramExternalUpdate } from "@llblab/pi-telegram/lib/external-handlers.ts";
144
146
  *
145
- * const off = onTelegramUpdate(async (update) => {
147
+ * const off = onTelegramExternalUpdate(async (update) => {
146
148
  * const cb = (update as { callback_query?: { data?: string } }).callback_query;
147
149
  * if (!cb?.data?.startsWith("myext:")) return "pass";
148
150
  * await handleMyCallback(cb);
@@ -154,12 +156,12 @@ export function createTelegramInterceptedHandleUpdate<TUpdate, TContext>(
154
156
  * ```
155
157
  *
156
158
  * Extensions that prefer zero coupling can also reach the registry directly
157
- * via `globalThis.__piTelegramExternalUpdateRegistry__` (versioned object,
158
- * see {@link TelegramExternalUpdateRegistry}). This avoids importing
159
+ * via `globalThis.__piTelegramExternalHandlerRegistry__` (versioned object,
160
+ * see {@link TelegramExternalHandlerRegistry}). This avoids importing
159
161
  * `@llblab/pi-telegram` and tolerates either install order.
160
162
  */
161
- export function onTelegramUpdate(
162
- handler: TelegramExternalUpdateInterceptor,
163
+ export function onTelegramExternalUpdate(
164
+ handler: TelegramExternalHandler,
163
165
  ): () => void {
164
166
  return getOrCreateRegistry().add(handler);
165
167
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@llblab/pi-telegram",
3
- "version": "0.9.2",
3
+ "version": "0.9.3",
4
4
  "private": false,
5
5
  "description": "Better Telegram DM bridge extension for π",
6
6
  "type": "module",