bot token from env

This commit is contained in:
LLB
2026-04-11 02:09:55 +04:00
parent a2bb588c3b
commit 233b20b089
5 changed files with 139 additions and 5 deletions
+2
View File
@@ -5,6 +5,7 @@
- `Constraint-Driven Evolution`: Add structure when the bridge gains real operator or runtime constraints
- `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`
- `Boundary Clarity`: Separate Telegram transport concerns, pi integration concerns, rendering behavior, and release/documentation state
- `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
- `Runtime Safety`: Prefer queue and rendering behavior that fails predictably over clever behavior that can desynchronize the Telegram bridge from pi session state
## 1. Concept
@@ -63,6 +64,7 @@
- Telegram API methods currently used include polling, message editing, draft streaming, callback queries, reactions, file download, and media upload endpoints
- pi integration depends on lifecycle hooks such as `before_agent_start`, `agent_start`, `message_start`, `message_update`, and `agent_end`
- `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()`
- Status/model/thinking controls are driven through Telegram inline keyboards and callback queries
- Inbound files may become pi image inputs; outbound files must flow through `telegram_attach`
+1
View File
@@ -9,3 +9,4 @@
- `[Metadata]` Updated package repository metadata to point at the `llblab/pi-telegram` fork. Impact: published package links no longer send users to stale upstream coordinates.
- `[Validation]` Added lightweight regression tests for Telegram Markdown rendering and queue/compaction dispatch guards. Impact: key renderer and queue invariants now have repeatable automated coverage.
- `[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.
- `[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.
+1
View File
@@ -61,6 +61,7 @@ Start pi, then run:
```
Paste the bot token when prompted.
If a bot token is already saved in `~/.pi/agent/telegram.json`, `/telegram-setup` shows that stored value by default. Otherwise it pre-fills from the first configured environment variable in `TELEGRAM_BOT_TOKEN`, `TELEGRAM_BOT_KEY`, `TELEGRAM_TOKEN`, or `TELEGRAM_KEY`.
The extension stores config in:
+59 -5
View File
@@ -247,6 +247,11 @@ interface TelegramInFlightModelSwitchState {
hasAbortHandler: boolean;
}
interface TelegramBotTokenPromptSpec {
method: "input" | "editor";
value: string;
}
type TelegramReplyMarkup = {
inline_keyboard: Array<Array<{ text: string; callback_data: string }>>;
};
@@ -269,6 +274,15 @@ const THINKING_LEVELS: readonly ThinkingLevel[] = [
"high",
"xhigh",
];
const TELEGRAM_BOT_TOKEN_INPUT_PLACEHOLDER = "123456:ABCDEF...";
const TELEGRAM_BOT_TOKEN_ENV_VARS = [
"TELEGRAM_BOT_TOKEN",
"TELEGRAM_BOT_KEY",
"TELEGRAM_TOKEN",
"TELEGRAM_KEY",
"BOT_TOKEN",
"BOT_KEY",
] as const;
const SYSTEM_PROMPT_SUFFIX = `
Telegram bridge extension is active.
@@ -1319,6 +1333,31 @@ function canDispatchTelegramTurnState(
);
}
function getTelegramBotTokenInputDefault(
env: NodeJS.ProcessEnv = process.env,
configToken?: string,
): string {
const trimmedConfigToken = configToken?.trim();
if (trimmedConfigToken) return trimmedConfigToken;
for (const key of TELEGRAM_BOT_TOKEN_ENV_VARS) {
const value = env[key]?.trim();
if (value) return value;
}
return TELEGRAM_BOT_TOKEN_INPUT_PLACEHOLDER;
}
function getTelegramBotTokenPromptSpec(
env: NodeJS.ProcessEnv = process.env,
configToken?: string,
): TelegramBotTokenPromptSpec {
const value = getTelegramBotTokenInputDefault(env, configToken);
return {
method:
value === TELEGRAM_BOT_TOKEN_INPUT_PLACEHOLDER ? "input" : "editor",
value,
};
}
function canRestartTelegramTurnForModelSwitch(
state: TelegramInFlightModelSwitchState,
): boolean {
@@ -1362,6 +1401,8 @@ export const __telegramTestUtils = {
MAX_MESSAGE_LENGTH,
renderTelegramMessage,
canDispatchTelegramTurnState,
getTelegramBotTokenInputDefault,
getTelegramBotTokenPromptSpec,
canRestartTelegramTurnForModelSwitch,
buildTelegramModelSwitchContinuationText,
};
@@ -1915,10 +1956,16 @@ export default function (pi: ExtensionAPI) {
if (!ctx.hasUI || setupInProgress) return;
setupInProgress = true;
try {
const token = await ctx.ui.input(
"Telegram bot token",
"123456:ABCDEF...",
const tokenPrompt = getTelegramBotTokenPromptSpec(
process.env,
config.botToken,
);
// Use the editor when a real default exists because ctx.ui.input only
// exposes placeholder text, not an editable prefilled value.
const token =
tokenPrompt.method === "editor"
? await ctx.ui.editor("Telegram bot token", tokenPrompt.value)
: await ctx.ui.input("Telegram bot token", tokenPrompt.value);
if (!token) return;
const nextConfig: TelegramConfig = { ...config, botToken: token.trim() };
@@ -2440,7 +2487,11 @@ export default function (pi: ExtensionAPI) {
);
return true;
}
queueTelegramModelSwitchContinuation(activeTelegramTurn, selection, ctx);
queueTelegramModelSwitchContinuation(
activeTelegramTurn,
selection,
ctx,
);
currentAbort();
await answerCallbackQuery(
query.id,
@@ -3495,7 +3546,10 @@ export default function (pi: ExtensionAPI) {
pi.on("tool_execution_end", async (_event, ctx) => {
if (!activeTelegramTurn) return;
activeTelegramToolExecutions = Math.max(0, activeTelegramToolExecutions - 1);
activeTelegramToolExecutions = Math.max(
0,
activeTelegramToolExecutions - 1,
);
triggerPendingTelegramModelSwitchAbort(ctx);
});
+76
View File
@@ -0,0 +1,76 @@
import test from "node:test";
import assert from "node:assert/strict";
import { __telegramTestUtils } from "../index.ts";
test("Bot token input prefers stored config over env vars", () => {
const value = __telegramTestUtils.getTelegramBotTokenInputDefault(
{
TELEGRAM_KEY: "key-last",
TELEGRAM_TOKEN: "token-third",
TELEGRAM_BOT_KEY: "key-second",
TELEGRAM_BOT_TOKEN: "token-first",
},
"stored-token",
);
assert.equal(value, "stored-token");
});
test("Bot token input prefers the first configured Telegram env var when no config exists", () => {
const value = __telegramTestUtils.getTelegramBotTokenInputDefault({
TELEGRAM_KEY: "key-last",
TELEGRAM_TOKEN: "token-third",
TELEGRAM_BOT_KEY: "key-second",
TELEGRAM_BOT_TOKEN: "token-first",
});
assert.equal(value, "token-first");
});
test("Bot token prompt uses the editor when a real prefill exists", () => {
const prompt = __telegramTestUtils.getTelegramBotTokenPromptSpec({
TELEGRAM_BOT_TOKEN: "token-first",
});
assert.deepEqual(prompt, {
method: "editor",
value: "token-first",
});
});
test("Bot token prompt shows stored config before env values", () => {
const prompt = __telegramTestUtils.getTelegramBotTokenPromptSpec(
{
TELEGRAM_BOT_TOKEN: "token-first",
},
"stored-token",
);
assert.deepEqual(prompt, {
method: "editor",
value: "stored-token",
});
});
test("Bot token input skips blank env vars and falls back to config", () => {
const value = __telegramTestUtils.getTelegramBotTokenInputDefault(
{
TELEGRAM_BOT_TOKEN: " ",
TELEGRAM_BOT_KEY: "",
TELEGRAM_TOKEN: " ",
},
"stored-token",
);
assert.equal(value, "stored-token");
});
test("Bot token input falls back to placeholder when no value exists", () => {
const value = __telegramTestUtils.getTelegramBotTokenInputDefault({});
assert.equal(value, "123456:ABCDEF...");
});
test("Bot token prompt uses placeholder input when no prefill exists", () => {
const prompt = __telegramTestUtils.getTelegramBotTokenPromptSpec({});
assert.deepEqual(prompt, {
method: "input",
value: "123456:ABCDEF...",
});
});