@llblab/pi-telegram 0.2.9 → 0.3.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/README.md +40 -26
- package/docs/architecture.md +62 -35
- package/index.ts +388 -1936
- package/lib/api.ts +647 -76
- package/lib/attachments.ts +128 -16
- package/lib/commands.ts +721 -0
- package/lib/config.ts +157 -0
- package/lib/media.ts +211 -36
- package/lib/menu.ts +920 -338
- package/lib/model.ts +647 -0
- package/lib/pi.ts +80 -0
- package/lib/polling.ts +264 -18
- package/lib/preview.ts +451 -29
- package/lib/queue.ts +1134 -110
- package/lib/registration.ts +127 -28
- package/lib/rendering.ts +575 -281
- package/lib/replies.ts +198 -8
- package/lib/runtime.ts +475 -0
- package/lib/setup.ts +129 -1
- package/lib/status.ts +428 -13
- package/lib/turns.ts +207 -17
- package/lib/updates.ts +392 -99
- package/package.json +18 -3
- package/AGENTS.md +0 -91
- package/BACKLOG.md +0 -5
- package/CHANGELOG.md +0 -23
- package/lib/model-switch.ts +0 -62
- package/tests/api.test.ts +0 -89
- package/tests/attachments.test.ts +0 -132
- package/tests/config.test.ts +0 -80
- package/tests/media.test.ts +0 -77
- package/tests/menu.test.ts +0 -676
- package/tests/polling.test.ts +0 -129
- package/tests/preview.test.ts +0 -441
- package/tests/queue.test.ts +0 -3245
- package/tests/registration.test.ts +0 -268
- package/tests/rendering.test.ts +0 -475
- package/tests/replies.test.ts +0 -142
- package/tests/turns.test.ts +0 -132
- package/tests/updates.test.ts +0 -357
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@llblab/pi-telegram",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"private": false,
|
|
5
5
|
"description": "Better Telegram DM bridge extension for pi",
|
|
6
6
|
"type": "module",
|
|
@@ -21,8 +21,19 @@
|
|
|
21
21
|
"url": "https://github.com/llblab/pi-telegram/issues"
|
|
22
22
|
},
|
|
23
23
|
"scripts": {
|
|
24
|
-
"test": "node --experimental-strip-types --test tests/*.test.ts"
|
|
24
|
+
"test": "node --experimental-strip-types --test tests/*.test.ts",
|
|
25
|
+
"typecheck": "tsc --noEmit",
|
|
26
|
+
"audit": "npm audit",
|
|
27
|
+
"pack:check": "npm pack --dry-run",
|
|
28
|
+
"validate": "npm run typecheck && npm test && npm run audit && npm run pack:check"
|
|
25
29
|
},
|
|
30
|
+
"files": [
|
|
31
|
+
"index.ts",
|
|
32
|
+
"lib/",
|
|
33
|
+
"README.md",
|
|
34
|
+
"docs/",
|
|
35
|
+
"screenshot.png"
|
|
36
|
+
],
|
|
26
37
|
"publishConfig": {
|
|
27
38
|
"access": "public"
|
|
28
39
|
},
|
|
@@ -33,9 +44,13 @@
|
|
|
33
44
|
"image": "https://github.com/llblab/pi-telegram/raw/main/screenshot.png"
|
|
34
45
|
},
|
|
35
46
|
"peerDependencies": {
|
|
36
|
-
"@mariozechner/pi-ai": "*",
|
|
37
47
|
"@mariozechner/pi-agent-core": "*",
|
|
48
|
+
"@mariozechner/pi-ai": "*",
|
|
38
49
|
"@mariozechner/pi-coding-agent": "*",
|
|
39
50
|
"@sinclair/typebox": "*"
|
|
51
|
+
},
|
|
52
|
+
"devDependencies": {
|
|
53
|
+
"@types/node": "latest",
|
|
54
|
+
"typescript": "latest"
|
|
40
55
|
}
|
|
41
56
|
}
|
package/AGENTS.md
DELETED
|
@@ -1,91 +0,0 @@
|
|
|
1
|
-
# Project Context
|
|
2
|
-
|
|
3
|
-
## 0. Meta-Protocol Principles
|
|
4
|
-
|
|
5
|
-
- `Constraint-Driven Evolution`: Add structure when the bridge gains real operator or runtime constraints
|
|
6
|
-
- `Single Source of Truth`: Keep durable rules in `AGENTS.md`, open work in `BACKLOG.md`, completed delivery in `CHANGELOG.md`, and deeper technical detail in `/docs`
|
|
7
|
-
- `Boundary Clarity`: Separate Telegram transport concerns, pi integration concerns, rendering behavior, and release/documentation state
|
|
8
|
-
- `Progressive Enhancement + Graceful Degradation`: Prefer behavior that upgrades automatically when richer runtime context exists, but always preserves a useful fallback path when it does not
|
|
9
|
-
- `Runtime Safety`: Prefer queue and rendering behavior that fails predictably over clever behavior that can desynchronize the Telegram bridge from pi session state
|
|
10
|
-
|
|
11
|
-
## 1. Concept
|
|
12
|
-
|
|
13
|
-
`pi-telegram` is a pi extension that turns a Telegram DM into a session-local frontend for pi, including text/file forwarding, streaming previews, queued follow-ups, model controls, and outbound attachment delivery.
|
|
14
|
-
|
|
15
|
-
## 2. Identity & Naming Contract
|
|
16
|
-
|
|
17
|
-
- `Telegram turn`: One unit of Telegram input processed by pi; this may represent one message or a coalesced media group
|
|
18
|
-
- `Queued Telegram turn`: A Telegram turn accepted by the bridge but not yet active in pi
|
|
19
|
-
- `Active Telegram turn`: The Telegram turn currently bound to the running pi agent loop
|
|
20
|
-
- `Preview`: The transient streamed response shown through Telegram drafts or editable messages before the final reply lands
|
|
21
|
-
- `Scoped models`: The subset of models exposed to Telegram model selection when pi settings or CLI flags limit the available list
|
|
22
|
-
|
|
23
|
-
## 3. Project Topology
|
|
24
|
-
|
|
25
|
-
- `/index.ts`: Main extension entrypoint and runtime composition layer for the bridge
|
|
26
|
-
- `/lib/*.ts`: Flat domain modules for reusable runtime logic. Favor domain files such as queueing/runtime, replies, polling, updates, attachments, registration/hooks, Telegram API/config, turns, media, setup, rendering, menu/status/model-resolution support, and other cohesive bridge subsystems; use `shared` only when a type or constant truly spans multiple domains
|
|
27
|
-
- `/tests/*.test.ts`: Domain-mirrored regression suites that follow the same flat naming as `/lib`
|
|
28
|
-
- `/docs/README.md`: Documentation index for technical project docs
|
|
29
|
-
- `/docs/architecture.md`: Runtime and subsystem overview for the bridge
|
|
30
|
-
- `/README.md`: User-facing project entry point, install guide, and fork summary
|
|
31
|
-
- `/AGENTS.md`: Durable engineering and runtime conventions
|
|
32
|
-
- `/BACKLOG.md`: Canonical open work
|
|
33
|
-
- `/CHANGELOG.md`: Completed delivery history
|
|
34
|
-
|
|
35
|
-
## 4. Core Entities
|
|
36
|
-
|
|
37
|
-
- `TelegramConfig`: Persisted bot/session pairing state
|
|
38
|
-
- `PendingTelegramTurn` / `ActiveTelegramTurn`: Queue and active-turn state for Telegram-originated work
|
|
39
|
-
- `TelegramPreviewState`: Streaming preview state for drafts or editable Telegram messages
|
|
40
|
-
- `TelegramModelMenuState`: Inline menu state for status/model/thinking controls
|
|
41
|
-
- `QueuedAttachment`: Outbound files staged for delivery through `telegram_attach`
|
|
42
|
-
|
|
43
|
-
## 5. Architectural Decisions
|
|
44
|
-
|
|
45
|
-
- `index.ts` stays the single extension entrypoint, while reusable runtime logic should be split into flat domain files under `/lib`; prefer domain-oriented grouping over atomizing every helper into its own file, and use `shared` sparingly for genuinely cross-domain types or constants
|
|
46
|
-
- The bridge is session-local and intentionally pairs with a single allowed Telegram user per config
|
|
47
|
-
- Telegram queue state is tracked locally and must stay aligned with pi agent lifecycle hooks; queued items now have explicit kinds and lanes so prompt turns and synthetic control actions can share one ordering model, while dispatch still respects active turns, pending dispatch, compaction, and pi pending-message state
|
|
48
|
-
- Prompt items should remain in the queue until `agent_start` consumes the dispatched turn; removing them earlier breaks active-turn binding, preview delivery, and end-of-turn follow-up behavior
|
|
49
|
-
- In-flight `/model` switching is supported only for Telegram-owned active turns and is implemented as set-model plus synthetic continuation turn plus abort; if a tool call is active, the abort is delayed until that tool finishes instead of interrupting the tool mid-flight
|
|
50
|
-
- Telegram replies render through Telegram HTML, not raw Markdown; real code blocks must stay literal and escaped
|
|
51
|
-
- `telegram_attach` is the canonical outbound file-delivery path for Telegram-originated requests
|
|
52
|
-
|
|
53
|
-
## 6. Engineering Conventions
|
|
54
|
-
|
|
55
|
-
- Treat queue handling, compaction interaction, and lifecycle-hook state transitions as regression-prone areas; validate them after changing dispatch logic
|
|
56
|
-
- Treat Markdown rendering as Telegram-specific output work, not generic Markdown rendering; preserve literal code content, avoid HTML chunk splits that break tags, prefer width-efficient monospace table and list formatting for narrow clients, and flatten nested Markdown quotes into indented single-blockquote output because Telegram does not render nested blockquotes reliably
|
|
57
|
-
- Keep comments and user-facing docs in English unless the surrounding file already follows another convention
|
|
58
|
-
- Each project `.ts` file should start with a short multi-line responsibility header comment that explains the file boundary to future maintainers
|
|
59
|
-
- Name extracted `/lib` modules and mirrored `/tests` suites by bare domain when the repository already supplies the Telegram scope; prefer `api.ts`, `queue.ts`, `updates.ts`, and `queue.test.ts` over redundant `telegram-*` filename prefixes
|
|
60
|
-
- Prefer targeted edits, keeping `index.ts` as the orchestration layer and moving reusable logic into flat `/lib` domain modules when a subsystem becomes large enough to earn extraction; current extracted domains include queueing/runtime decisions, preview streaming, replies, polling, updates, attachments, registration and lifecycle-hook binding, Telegram API/config support, turn-building, media extraction, setup, rendering, status rendering, menu/model-resolution/UI support, and model-switch support
|
|
61
|
-
- Keep preview appearance logic in the rendering domain and preview transport/lifecycle logic in the preview domain so richer streaming strategies can evolve without entangling Telegram delivery state with Markdown formatting rules
|
|
62
|
-
|
|
63
|
-
## 7. Operational Conventions
|
|
64
|
-
|
|
65
|
-
- When Telegram-visible behavior changes, sync `README.md` and the relevant `/docs` entry in the same pass
|
|
66
|
-
- When durable runtime constraints or repeat bug patterns emerge, record them here instead of burying them in changelog prose
|
|
67
|
-
- When fork identity changes, keep `README.md`, package metadata, and docs aligned so the published package does not point back at stale upstream coordinates
|
|
68
|
-
- Work only inside this repository during development tasks; updating the installed Pi extension checkout is a separate manual operator step, not part of normal in-repo implementation work
|
|
69
|
-
|
|
70
|
-
## 8. Integration Protocols
|
|
71
|
-
|
|
72
|
-
- Telegram API methods currently used include polling, message editing, draft streaming, callback queries, reactions, file download, and media upload endpoints
|
|
73
|
-
- pi integration depends on lifecycle hooks such as `before_agent_start`, `agent_start`, `message_start`, `message_update`, and `agent_end`
|
|
74
|
-
- `ctx.ui.input()` provides placeholder text rather than an editable prefilled value; when a real default must appear already filled in, prefer `ctx.ui.editor()`
|
|
75
|
-
- For `/telegram-setup`, prefer the locally saved bot token over environment variables on repeat setup runs; env vars are the bootstrap path when no local token exists
|
|
76
|
-
- Status/model/thinking controls are driven through Telegram inline keyboards and callback queries
|
|
77
|
-
- Inbound files may become pi image inputs; outbound files must flow through `telegram_attach`
|
|
78
|
-
|
|
79
|
-
## 9. Pre-Task Preparation Protocol
|
|
80
|
-
|
|
81
|
-
- Read `README.md` for current user-facing behavior and fork positioning
|
|
82
|
-
- Read `BACKLOG.md` before changing runtime behavior or documentation so open work stays truthful
|
|
83
|
-
- Read `/docs/architecture.md` before restructuring queue, preview, rendering, or command-handling logic
|
|
84
|
-
- Inspect the relevant `index.ts` section before editing because most bridge behavior is stateful and cross-linked
|
|
85
|
-
|
|
86
|
-
## 10. Task Completion Protocol
|
|
87
|
-
|
|
88
|
-
- Run the smallest meaningful validation for the touched area; `npm test` is the default regression suite once rendering or queue logic changes
|
|
89
|
-
- For rendering changes, ensure regressions still cover nested lists, code blocks, underscore-heavy text, and long-message chunking
|
|
90
|
-
- For queue/dispatch changes, validate abort, compaction, pending-dispatch, and pi pending-message guard behavior
|
|
91
|
-
- Sync `README.md`, `CHANGELOG.md`, `BACKLOG.md`, and `/docs` whenever user-visible behavior or real open-work state changes
|
package/BACKLOG.md
DELETED
package/CHANGELOG.md
DELETED
|
@@ -1,23 +0,0 @@
|
|
|
1
|
-
# Changelog
|
|
2
|
-
|
|
3
|
-
## Current
|
|
4
|
-
|
|
5
|
-
- `[Rendering]` Hardened link rendering so absolute links stay clickable, markdown-heavy link labels reduce to plain clickable labels, tooltip titles are ignored safely, balanced-parenthesis URLs stay intact, and unsupported link forms degrade without broken anchors. Impact: Telegram replies now keep more links usable while avoiding malformed output for relative, reference-style, or footnote-like link syntax.
|
|
6
|
-
- `[Preview]` Evolved rich streaming from first-chunk snapshots to stable-block previews with a conservative plain tail fallback, while preserving original blank-line spacing between rendered blocks and keeping headings visually separated from following blocks. Impact: closed top-level Markdown blocks now stream as rich Telegram HTML before finalization, incomplete fences, quotes, lists, and other trailing work remain readable without producing broken rich formatting, preview/final block spacing no longer collapses extra empty lines, and headings no longer visually merge into following code blocks when source Markdown omits a blank line.
|
|
7
|
-
- `[Refactor]` Split preview concerns so runtime transport and finalization live in the preview domain while preview snapshot derivation lives in the rendering domain. Impact: rich streaming can evolve independently from final reply delivery while keeping preview appearance decisions closer to the Telegram renderer.
|
|
8
|
-
- `[Streaming]` Switched Telegram previews from plain draft-first text to rich first-chunk message editing, so formatting appears during generation instead of only after finalization. Impact: users now see richer streamed output earlier, while final replies still replace the preview with fully rendered Telegram HTML.
|
|
9
|
-
- `[Rendering]` Preserved leading indentation on the first Markdown line, kept numeric markers for ordered task lists in both preview and final Telegram rendering, and stopped reinterpreting standalone `[x]` or `[ ]` prose as inline checkboxes. Impact: nested content no longer flattens when a message starts with indentation, numbered checklists keep their ordered semantics, and literal checklist-like prose stays literal.
|
|
10
|
-
- `[Queue UI]` Marked liked high-priority queued Telegram turns with `⬆` in the pi status-bar queue preview. Impact: operators can now distinguish reaction-promoted turns from normal queued prompts at a glance.
|
|
11
|
-
- `[Docs]` Added short responsibility header comments to every project `.ts` file. Impact: file boundaries are easier to understand while navigating the growing `/lib` split.
|
|
12
|
-
- `[Naming]` Renamed extracted domain modules and mirrored regression suites to use repo-scoped bare domain filenames such as `api.ts`, `queue.ts`, and `queue.test.ts` instead of repeating `telegram-*` in every path. Impact: the internal topology is easier to scan and stays aligned with the repository-level Telegram scope.
|
|
13
|
-
- `[Controls]` Expanded Telegram session controls with a richer `/status` view, inline model selection, and thinking-level controls, and fixed the callback-selection path so idle model and thinking picks apply immediately instead of only becoming visible after a later Telegram interaction. Impact: more bridge configuration can be managed directly from Telegram with more predictable immediate feedback.
|
|
14
|
-
- `[Queue]` Upgraded Telegram turn queueing with previews, reaction-driven prioritization/removal, media-group handling, aborted-turn history preservation, and safer dispatch gating. Impact: follow-up handling is more transparent and less prone to lifecycle races.
|
|
15
|
-
- `[Rendering]` Added Telegram-oriented Markdown rendering and hardened reply streaming/chunking behavior, including narrower monospace Markdown table output without outer side borders, monospace list markers for unordered and ordered lists, and flattened nested quote indentation inside a single Telegram blockquote. Impact: formatted replies render more reliably while preserving literal code blocks and using width more efficiently on narrow Telegram clients.
|
|
16
|
-
- `[Runtime]` Hardened attachment delivery, polling/runtime behavior, Telegram session integration, preview-finalization and reply-transport routing into the replies domain, lazy Telegram API client routing into the Telegram API domain, turn-building extraction into its own domain, menu/model-resolution plus menu-state, pure menu-page derivation, pure menu render-payload builders, menu-message runtime, callback parsing, callback entry handling, callback mutation helpers, full model-callback planning and execution, and interface-polished callback effect ports into the menu domain, direct execute-from-update routing into the updates domain, model-switch restart glue extraction into the model-switch domain, and tool/command/lifecycle-hook registration extraction into a dedicated registration domain. Impact: the bridge is more robust as a daily Telegram frontend for pi.
|
|
17
|
-
- `[Metadata]` Updated package repository metadata to point at the `llblab/pi-telegram` fork and renamed the npm package to `@llblab/pi-telegram` with public scoped publish settings. Impact: published package links no longer send users to stale upstream coordinates and the package can be published under the fork-owned npm scope.
|
|
18
|
-
- `[Validation]` Added lightweight regression tests for Telegram Markdown rendering, queue/runtime/agent-loop/session/control/dispatch, replies, polling, updates, attachments, registration, turns, menu, and Telegram API/media/config helpers, including quote/list, table, link/code, mixed-link/code chunking, mixed-block chunk transitions, long multi-block, long-quote, long inline-formatting chunk boundaries, list-code-quote-prose chunk transitions, narrower monospace table rendering without outer side borders, monospace unordered and ordered list markers, flattened nested quote indentation inside one Telegram blockquote, inbound poll/pair/dispatch runtime cases, preview finalization, aborted-turn history carry-over, queued-status/model-after-agent-end sequencing, compaction gating, media-group debounce dispatch, direct menu callback planning and execution, pure menu-page derivation, pure menu render-payload builders, reaction-driven reprioritization/removal, immediate in-flight model-switch continuation, delayed abort-after-tool-completion, lazy Telegram API client routing, turn-building, and scoped-model resolution. Impact: key renderer and queue invariants now have repeatable automated coverage across the known high-risk bridge paths.
|
|
19
|
-
- `[Model Switching]` Enabled `/model` during an active Telegram-owned run by applying the new model and continuing on the new model automatically, delaying the abort until the current tool finishes when needed. Impact: Telegram can now approximate pi's manual stop-switch-continue workflow with fewer mid-tool aborts.
|
|
20
|
-
- `[Queue Core]` Introduced queued item kinds and explicit queue-lane ordering semantics so prompt turns and synthetic control actions share one ordering model, then regrouped the extracted helpers into flatter domain-oriented `/lib` modules such as queue, replies, polling, updates, attachments, turns, menu, Telegram API, and registration while keeping `index.ts` as the entrypoint. Prompt items now stay queued until `agent_start` consumes the dispatched turn, which restores correct active-turn binding for previews and final delivery. Impact: the bridge now has a clearer foundation for scheduling async extension operations alongside Telegram prompts without losing a single obvious runtime entry file.
|
|
21
|
-
- `[Registration]` Moved extension tool, command, and lifecycle-hook binding into the registration domain and added registration-focused regression coverage. Impact: extension wiring is easier to reason about and test without dragging full runtime state into every registration change.
|
|
22
|
-
- `[Control Queue]` Moved `/status` and `/model` command handling onto high-priority control queue items. Impact: control actions can wait safely behind the current run while still jumping ahead of normal queued prompts.
|
|
23
|
-
- `[Setup]` `/telegram-setup` now shows the stored bot token first, otherwise prefills from common Telegram bot environment variables before falling back to the placeholder, using an actual prefilled editor when a real default exists. Impact: repeat setup respects local saved state while first-run and secret-managed setup stay fast.
|
package/lib/model-switch.ts
DELETED
|
@@ -1,62 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* In-flight Telegram model-switch helpers
|
|
3
|
-
* Encodes the safe restart and continuation rules for switching models during active Telegram-owned runs
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
import type { Model } from "@mariozechner/pi-ai";
|
|
7
|
-
|
|
8
|
-
import type { TelegramInFlightModelSwitchState } from "./queue.ts";
|
|
9
|
-
|
|
10
|
-
export type TelegramThinkingLevel =
|
|
11
|
-
| "off"
|
|
12
|
-
| "minimal"
|
|
13
|
-
| "low"
|
|
14
|
-
| "medium"
|
|
15
|
-
| "high"
|
|
16
|
-
| "xhigh";
|
|
17
|
-
|
|
18
|
-
export function canRestartTelegramTurnForModelSwitch(
|
|
19
|
-
state: TelegramInFlightModelSwitchState,
|
|
20
|
-
): boolean {
|
|
21
|
-
return !state.isIdle && state.hasActiveTelegramTurn && state.hasAbortHandler;
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
export function shouldTriggerPendingTelegramModelSwitchAbort(state: {
|
|
25
|
-
hasPendingModelSwitch: boolean;
|
|
26
|
-
hasActiveTelegramTurn: boolean;
|
|
27
|
-
hasAbortHandler: boolean;
|
|
28
|
-
activeToolExecutions: number;
|
|
29
|
-
}): boolean {
|
|
30
|
-
return (
|
|
31
|
-
state.hasPendingModelSwitch &&
|
|
32
|
-
state.hasActiveTelegramTurn &&
|
|
33
|
-
state.hasAbortHandler &&
|
|
34
|
-
state.activeToolExecutions === 0
|
|
35
|
-
);
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
export function restartTelegramModelSwitchContinuation<TTurn, TSelection>(state: {
|
|
39
|
-
activeTurn: TTurn | undefined;
|
|
40
|
-
abort: (() => void) | undefined;
|
|
41
|
-
selection: TSelection;
|
|
42
|
-
queueContinuation: (turn: TTurn, selection: TSelection) => void;
|
|
43
|
-
}): boolean {
|
|
44
|
-
if (!state.activeTurn || !state.abort) return false;
|
|
45
|
-
state.queueContinuation(state.activeTurn, state.selection);
|
|
46
|
-
state.abort();
|
|
47
|
-
return true;
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
export function buildTelegramModelSwitchContinuationText<
|
|
51
|
-
TModel extends Pick<Model<any>, "provider" | "id">,
|
|
52
|
-
>(
|
|
53
|
-
telegramPrefix: string,
|
|
54
|
-
model: TModel,
|
|
55
|
-
thinkingLevel?: TelegramThinkingLevel,
|
|
56
|
-
): string {
|
|
57
|
-
const modelLabel = `${model.provider}/${model.id}`;
|
|
58
|
-
const thinkingSuffix = thinkingLevel
|
|
59
|
-
? ` Keep the selected thinking level (${thinkingLevel}) if it still applies.`
|
|
60
|
-
: "";
|
|
61
|
-
return `${telegramPrefix} Continue the interrupted previous Telegram request using the newly selected model (${modelLabel}). Resume from the last unfinished step instead of restarting from scratch unless necessary.${thinkingSuffix}`;
|
|
62
|
-
}
|
package/tests/api.test.ts
DELETED
|
@@ -1,89 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Regression tests for Telegram API and config helpers
|
|
3
|
-
* Verifies config persistence and direct helper behavior around missing tokens and callback-query failures
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
import assert from "node:assert/strict";
|
|
7
|
-
import { mkdtemp, readFile } from "node:fs/promises";
|
|
8
|
-
import { tmpdir } from "node:os";
|
|
9
|
-
import { join } from "node:path";
|
|
10
|
-
import test from "node:test";
|
|
11
|
-
|
|
12
|
-
import {
|
|
13
|
-
answerTelegramCallbackQuery,
|
|
14
|
-
callTelegram,
|
|
15
|
-
createTelegramApiClient,
|
|
16
|
-
downloadTelegramFile,
|
|
17
|
-
readTelegramConfig,
|
|
18
|
-
writeTelegramConfig,
|
|
19
|
-
} from "../lib/api.ts";
|
|
20
|
-
|
|
21
|
-
test("Telegram config helpers persist and reload config", async () => {
|
|
22
|
-
const agentDir = await mkdtemp(join(tmpdir(), "pi-telegram-config-"));
|
|
23
|
-
const configPath = join(agentDir, "telegram.json");
|
|
24
|
-
const config = {
|
|
25
|
-
botToken: "123:abc",
|
|
26
|
-
botUsername: "demo_bot",
|
|
27
|
-
allowedUserId: 42,
|
|
28
|
-
};
|
|
29
|
-
await writeTelegramConfig(agentDir, configPath, config);
|
|
30
|
-
const reloaded = await readTelegramConfig(configPath);
|
|
31
|
-
assert.deepEqual(reloaded, config);
|
|
32
|
-
const raw = await readFile(configPath, "utf8");
|
|
33
|
-
assert.match(raw, /demo_bot/);
|
|
34
|
-
});
|
|
35
|
-
|
|
36
|
-
test("Telegram API helpers reject missing bot token for direct calls", async () => {
|
|
37
|
-
await assert.rejects(() => callTelegram(undefined, "getMe", {}), {
|
|
38
|
-
message: "Telegram bot token is not configured",
|
|
39
|
-
});
|
|
40
|
-
await assert.rejects(
|
|
41
|
-
() =>
|
|
42
|
-
downloadTelegramFile(
|
|
43
|
-
undefined,
|
|
44
|
-
"file-id",
|
|
45
|
-
"demo.txt",
|
|
46
|
-
join(tmpdir(), "pi-telegram-missing-token"),
|
|
47
|
-
),
|
|
48
|
-
{
|
|
49
|
-
message: "Telegram bot token is not configured",
|
|
50
|
-
},
|
|
51
|
-
);
|
|
52
|
-
});
|
|
53
|
-
|
|
54
|
-
test("answerTelegramCallbackQuery ignores Telegram API failures", async () => {
|
|
55
|
-
const originalFetch = globalThis.fetch;
|
|
56
|
-
globalThis.fetch = (async () => {
|
|
57
|
-
throw new Error("network down");
|
|
58
|
-
}) as typeof fetch;
|
|
59
|
-
try {
|
|
60
|
-
await assert.doesNotReject(() =>
|
|
61
|
-
answerTelegramCallbackQuery("123:abc", "callback-id", "ok"),
|
|
62
|
-
);
|
|
63
|
-
} finally {
|
|
64
|
-
globalThis.fetch = originalFetch;
|
|
65
|
-
}
|
|
66
|
-
});
|
|
67
|
-
|
|
68
|
-
test("Telegram API client resolves bot tokens lazily for wrapped calls", async () => {
|
|
69
|
-
const originalFetch = globalThis.fetch;
|
|
70
|
-
const calls: string[] = [];
|
|
71
|
-
let botToken = "123:abc";
|
|
72
|
-
globalThis.fetch = (async (input) => {
|
|
73
|
-
calls.push(typeof input === "string" ? input : input.toString());
|
|
74
|
-
return {
|
|
75
|
-
ok: true,
|
|
76
|
-
json: async () => ({ ok: true, result: true }),
|
|
77
|
-
} as Response;
|
|
78
|
-
}) as typeof fetch;
|
|
79
|
-
try {
|
|
80
|
-
const client = createTelegramApiClient(() => botToken);
|
|
81
|
-
await client.call("sendChatAction", { chat_id: 1, action: "typing" });
|
|
82
|
-
botToken = "456:def";
|
|
83
|
-
await client.answerCallbackQuery("cb-1", "ok");
|
|
84
|
-
assert.match(calls[0] ?? "", /bot123:abc\/sendChatAction$/);
|
|
85
|
-
assert.match(calls[1] ?? "", /bot456:def\/answerCallbackQuery$/);
|
|
86
|
-
} finally {
|
|
87
|
-
globalThis.fetch = originalFetch;
|
|
88
|
-
}
|
|
89
|
-
});
|
|
@@ -1,132 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Regression tests for the Telegram attachments domain
|
|
3
|
-
* Covers attachment queueing and attachment delivery behavior in one domain-level suite
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
import assert from "node:assert/strict";
|
|
7
|
-
import test from "node:test";
|
|
8
|
-
|
|
9
|
-
import {
|
|
10
|
-
queueTelegramAttachments,
|
|
11
|
-
sendQueuedTelegramAttachments,
|
|
12
|
-
} from "../lib/attachments.ts";
|
|
13
|
-
|
|
14
|
-
test("Attachment queueing adds files to the active Telegram turn", async () => {
|
|
15
|
-
const activeTurn = {
|
|
16
|
-
queuedAttachments: [],
|
|
17
|
-
} as unknown as {
|
|
18
|
-
queuedAttachments: Array<{ path: string; fileName: string }>;
|
|
19
|
-
} & Parameters<typeof queueTelegramAttachments>[0]["activeTurn"];
|
|
20
|
-
const result = await queueTelegramAttachments({
|
|
21
|
-
activeTurn,
|
|
22
|
-
paths: ["/tmp/demo.txt"],
|
|
23
|
-
maxAttachmentsPerTurn: 2,
|
|
24
|
-
statPath: async () => ({ isFile: () => true }),
|
|
25
|
-
});
|
|
26
|
-
assert.deepEqual(activeTurn.queuedAttachments, [
|
|
27
|
-
{ path: "/tmp/demo.txt", fileName: "demo.txt" },
|
|
28
|
-
]);
|
|
29
|
-
assert.deepEqual(result.details.paths, ["/tmp/demo.txt"]);
|
|
30
|
-
});
|
|
31
|
-
|
|
32
|
-
test("Attachment queueing rejects missing turns, non-files, and full queues", async () => {
|
|
33
|
-
await assert.rejects(
|
|
34
|
-
() =>
|
|
35
|
-
queueTelegramAttachments({
|
|
36
|
-
activeTurn: undefined,
|
|
37
|
-
paths: ["/tmp/demo.txt"],
|
|
38
|
-
maxAttachmentsPerTurn: 1,
|
|
39
|
-
statPath: async () => ({ isFile: () => true }),
|
|
40
|
-
}),
|
|
41
|
-
{ message: /active Telegram turn/ },
|
|
42
|
-
);
|
|
43
|
-
await assert.rejects(
|
|
44
|
-
() =>
|
|
45
|
-
queueTelegramAttachments({
|
|
46
|
-
activeTurn: { queuedAttachments: [] } as never,
|
|
47
|
-
paths: ["/tmp/demo.txt"],
|
|
48
|
-
maxAttachmentsPerTurn: 1,
|
|
49
|
-
statPath: async () => ({ isFile: () => false }),
|
|
50
|
-
}),
|
|
51
|
-
{ message: "Not a file: /tmp/demo.txt" },
|
|
52
|
-
);
|
|
53
|
-
await assert.rejects(
|
|
54
|
-
() =>
|
|
55
|
-
queueTelegramAttachments({
|
|
56
|
-
activeTurn: {
|
|
57
|
-
queuedAttachments: [{ path: "/tmp/a.txt", fileName: "a.txt" }],
|
|
58
|
-
} as never,
|
|
59
|
-
paths: ["/tmp/demo.txt"],
|
|
60
|
-
maxAttachmentsPerTurn: 1,
|
|
61
|
-
statPath: async () => ({ isFile: () => true }),
|
|
62
|
-
}),
|
|
63
|
-
{ message: "Attachment limit reached (1)" },
|
|
64
|
-
);
|
|
65
|
-
});
|
|
66
|
-
|
|
67
|
-
test("Attachment delivery chooses photo vs document methods from file paths", async () => {
|
|
68
|
-
const sent: Array<string> = [];
|
|
69
|
-
await sendQueuedTelegramAttachments(
|
|
70
|
-
{
|
|
71
|
-
kind: "prompt",
|
|
72
|
-
chatId: 1,
|
|
73
|
-
replyToMessageId: 2,
|
|
74
|
-
sourceMessageIds: [],
|
|
75
|
-
queueOrder: 1,
|
|
76
|
-
queueLane: "default",
|
|
77
|
-
laneOrder: 1,
|
|
78
|
-
queuedAttachments: [
|
|
79
|
-
{ path: "/tmp/a.png", fileName: "a.png" },
|
|
80
|
-
{ path: "/tmp/b.txt", fileName: "b.txt" },
|
|
81
|
-
],
|
|
82
|
-
content: [{ type: "text", text: "prompt" }],
|
|
83
|
-
historyText: "history",
|
|
84
|
-
statusSummary: "summary",
|
|
85
|
-
},
|
|
86
|
-
{
|
|
87
|
-
sendMultipart: async (
|
|
88
|
-
method,
|
|
89
|
-
_fields,
|
|
90
|
-
fileField,
|
|
91
|
-
_filePath,
|
|
92
|
-
fileName,
|
|
93
|
-
) => {
|
|
94
|
-
sent.push(`${method}:${fileField}:${fileName}`);
|
|
95
|
-
},
|
|
96
|
-
sendTextReply: async () => undefined,
|
|
97
|
-
},
|
|
98
|
-
);
|
|
99
|
-
assert.deepEqual(sent, [
|
|
100
|
-
"sendPhoto:photo:a.png",
|
|
101
|
-
"sendDocument:document:b.txt",
|
|
102
|
-
]);
|
|
103
|
-
});
|
|
104
|
-
|
|
105
|
-
test("Attachment delivery reports per-file failures via text replies", async () => {
|
|
106
|
-
const replies: string[] = [];
|
|
107
|
-
await sendQueuedTelegramAttachments(
|
|
108
|
-
{
|
|
109
|
-
kind: "prompt",
|
|
110
|
-
chatId: 1,
|
|
111
|
-
replyToMessageId: 2,
|
|
112
|
-
sourceMessageIds: [],
|
|
113
|
-
queueOrder: 1,
|
|
114
|
-
queueLane: "default",
|
|
115
|
-
laneOrder: 1,
|
|
116
|
-
queuedAttachments: [{ path: "/tmp/a.png", fileName: "a.png" }],
|
|
117
|
-
content: [{ type: "text", text: "prompt" }],
|
|
118
|
-
historyText: "history",
|
|
119
|
-
statusSummary: "summary",
|
|
120
|
-
},
|
|
121
|
-
{
|
|
122
|
-
sendMultipart: async () => {
|
|
123
|
-
throw new Error("upload failed");
|
|
124
|
-
},
|
|
125
|
-
sendTextReply: async (_chatId, _replyToMessageId, text) => {
|
|
126
|
-
replies.push(text);
|
|
127
|
-
return undefined;
|
|
128
|
-
},
|
|
129
|
-
},
|
|
130
|
-
);
|
|
131
|
-
assert.deepEqual(replies, ["Failed to send attachment a.png: upload failed"]);
|
|
132
|
-
});
|
package/tests/config.test.ts
DELETED
|
@@ -1,80 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Regression tests for Telegram setup prompt defaults
|
|
3
|
-
* Covers token-prefill priority across stored config, environment variables, and placeholder fallback
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
import assert from "node:assert/strict";
|
|
7
|
-
import test from "node:test";
|
|
8
|
-
|
|
9
|
-
import { __telegramTestUtils } from "../index.ts";
|
|
10
|
-
|
|
11
|
-
test("Bot token input prefers stored config over env vars", () => {
|
|
12
|
-
const value = __telegramTestUtils.getTelegramBotTokenInputDefault(
|
|
13
|
-
{
|
|
14
|
-
TELEGRAM_KEY: "key-last",
|
|
15
|
-
TELEGRAM_TOKEN: "token-third",
|
|
16
|
-
TELEGRAM_BOT_KEY: "key-second",
|
|
17
|
-
TELEGRAM_BOT_TOKEN: "token-first",
|
|
18
|
-
},
|
|
19
|
-
"stored-token",
|
|
20
|
-
);
|
|
21
|
-
assert.equal(value, "stored-token");
|
|
22
|
-
});
|
|
23
|
-
|
|
24
|
-
test("Bot token input prefers the first configured Telegram env var when no config exists", () => {
|
|
25
|
-
const value = __telegramTestUtils.getTelegramBotTokenInputDefault({
|
|
26
|
-
TELEGRAM_KEY: "key-last",
|
|
27
|
-
TELEGRAM_TOKEN: "token-third",
|
|
28
|
-
TELEGRAM_BOT_KEY: "key-second",
|
|
29
|
-
TELEGRAM_BOT_TOKEN: "token-first",
|
|
30
|
-
});
|
|
31
|
-
assert.equal(value, "token-first");
|
|
32
|
-
});
|
|
33
|
-
|
|
34
|
-
test("Bot token prompt uses the editor when a real prefill exists", () => {
|
|
35
|
-
const prompt = __telegramTestUtils.getTelegramBotTokenPromptSpec({
|
|
36
|
-
TELEGRAM_BOT_TOKEN: "token-first",
|
|
37
|
-
});
|
|
38
|
-
assert.deepEqual(prompt, {
|
|
39
|
-
method: "editor",
|
|
40
|
-
value: "token-first",
|
|
41
|
-
});
|
|
42
|
-
});
|
|
43
|
-
|
|
44
|
-
test("Bot token prompt shows stored config before env values", () => {
|
|
45
|
-
const prompt = __telegramTestUtils.getTelegramBotTokenPromptSpec(
|
|
46
|
-
{
|
|
47
|
-
TELEGRAM_BOT_TOKEN: "token-first",
|
|
48
|
-
},
|
|
49
|
-
"stored-token",
|
|
50
|
-
);
|
|
51
|
-
assert.deepEqual(prompt, {
|
|
52
|
-
method: "editor",
|
|
53
|
-
value: "stored-token",
|
|
54
|
-
});
|
|
55
|
-
});
|
|
56
|
-
|
|
57
|
-
test("Bot token input skips blank env vars and falls back to config", () => {
|
|
58
|
-
const value = __telegramTestUtils.getTelegramBotTokenInputDefault(
|
|
59
|
-
{
|
|
60
|
-
TELEGRAM_BOT_TOKEN: " ",
|
|
61
|
-
TELEGRAM_BOT_KEY: "",
|
|
62
|
-
TELEGRAM_TOKEN: " ",
|
|
63
|
-
},
|
|
64
|
-
"stored-token",
|
|
65
|
-
);
|
|
66
|
-
assert.equal(value, "stored-token");
|
|
67
|
-
});
|
|
68
|
-
|
|
69
|
-
test("Bot token input falls back to placeholder when no value exists", () => {
|
|
70
|
-
const value = __telegramTestUtils.getTelegramBotTokenInputDefault({});
|
|
71
|
-
assert.equal(value, "123456:ABCDEF...");
|
|
72
|
-
});
|
|
73
|
-
|
|
74
|
-
test("Bot token prompt uses placeholder input when no prefill exists", () => {
|
|
75
|
-
const prompt = __telegramTestUtils.getTelegramBotTokenPromptSpec({});
|
|
76
|
-
assert.deepEqual(prompt, {
|
|
77
|
-
method: "input",
|
|
78
|
-
value: "123456:ABCDEF...",
|
|
79
|
-
});
|
|
80
|
-
});
|
package/tests/media.test.ts
DELETED
|
@@ -1,77 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Regression tests for Telegram media and text extraction helpers
|
|
3
|
-
* Covers inbound file-info collection, text extraction, id collection, and history formatting
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
import assert from "node:assert/strict";
|
|
7
|
-
import test from "node:test";
|
|
8
|
-
|
|
9
|
-
import {
|
|
10
|
-
collectTelegramFileInfos,
|
|
11
|
-
collectTelegramMessageIds,
|
|
12
|
-
extractFirstTelegramMessageText,
|
|
13
|
-
extractTelegramMessagesText,
|
|
14
|
-
formatTelegramHistoryText,
|
|
15
|
-
guessMediaType,
|
|
16
|
-
} from "../lib/media.ts";
|
|
17
|
-
|
|
18
|
-
test("Media helpers collect file infos across Telegram message variants", () => {
|
|
19
|
-
const files = collectTelegramFileInfos([
|
|
20
|
-
{
|
|
21
|
-
message_id: 1,
|
|
22
|
-
text: "hello",
|
|
23
|
-
photo: [
|
|
24
|
-
{ file_id: "small", file_size: 1 },
|
|
25
|
-
{ file_id: "large", file_size: 10 },
|
|
26
|
-
],
|
|
27
|
-
document: {
|
|
28
|
-
file_id: "doc",
|
|
29
|
-
file_name: "report.png",
|
|
30
|
-
mime_type: "image/png",
|
|
31
|
-
},
|
|
32
|
-
voice: {
|
|
33
|
-
file_id: "voice",
|
|
34
|
-
mime_type: "audio/ogg",
|
|
35
|
-
},
|
|
36
|
-
sticker: {
|
|
37
|
-
file_id: "sticker",
|
|
38
|
-
},
|
|
39
|
-
},
|
|
40
|
-
]);
|
|
41
|
-
assert.deepEqual(
|
|
42
|
-
files.map((file) => ({
|
|
43
|
-
id: file.file_id,
|
|
44
|
-
name: file.fileName,
|
|
45
|
-
image: file.isImage,
|
|
46
|
-
})),
|
|
47
|
-
[
|
|
48
|
-
{ id: "large", name: "photo-1.jpg", image: true },
|
|
49
|
-
{ id: "doc", name: "report.png", image: true },
|
|
50
|
-
{ id: "voice", name: "voice-1.ogg", image: false },
|
|
51
|
-
{ id: "sticker", name: "sticker-1.webp", image: true },
|
|
52
|
-
],
|
|
53
|
-
);
|
|
54
|
-
});
|
|
55
|
-
|
|
56
|
-
test("Media helpers extract text, ids, and history summaries", () => {
|
|
57
|
-
const messages = [
|
|
58
|
-
{ message_id: 1, text: "first" },
|
|
59
|
-
{ message_id: 2, caption: "second" },
|
|
60
|
-
{ message_id: 2, text: "duplicate id" },
|
|
61
|
-
];
|
|
62
|
-
assert.equal(
|
|
63
|
-
extractTelegramMessagesText(messages),
|
|
64
|
-
"first\n\nsecond\n\nduplicate id",
|
|
65
|
-
);
|
|
66
|
-
assert.equal(extractFirstTelegramMessageText(messages), "first");
|
|
67
|
-
assert.deepEqual(collectTelegramMessageIds(messages), [1, 2]);
|
|
68
|
-
assert.equal(
|
|
69
|
-
formatTelegramHistoryText("hello", [{ path: "/tmp/demo.txt" }]),
|
|
70
|
-
"hello\nAttachments:\n- /tmp/demo.txt",
|
|
71
|
-
);
|
|
72
|
-
});
|
|
73
|
-
|
|
74
|
-
test("Media helpers infer outgoing image media types from file paths", () => {
|
|
75
|
-
assert.equal(guessMediaType("/tmp/demo.png"), "image/png");
|
|
76
|
-
assert.equal(guessMediaType("/tmp/demo.txt"), undefined);
|
|
77
|
-
});
|