@dreb/telegram 1.16.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 +91 -0
- package/dist/agent-bridge.d.ts +146 -0
- package/dist/agent-bridge.d.ts.map +1 -0
- package/dist/agent-bridge.js +466 -0
- package/dist/agent-bridge.js.map +1 -0
- package/dist/bot.d.ts +11 -0
- package/dist/bot.d.ts.map +1 -0
- package/dist/bot.js +112 -0
- package/dist/bot.js.map +1 -0
- package/dist/bridge-lifecycle.d.ts +17 -0
- package/dist/bridge-lifecycle.d.ts.map +1 -0
- package/dist/bridge-lifecycle.js +71 -0
- package/dist/bridge-lifecycle.js.map +1 -0
- package/dist/commands/agent.d.ts +11 -0
- package/dist/commands/agent.d.ts.map +1 -0
- package/dist/commands/agent.js +171 -0
- package/dist/commands/agent.js.map +1 -0
- package/dist/commands/buddy.d.ts +20 -0
- package/dist/commands/buddy.d.ts.map +1 -0
- package/dist/commands/buddy.js +84 -0
- package/dist/commands/buddy.js.map +1 -0
- package/dist/commands/core.d.ts +13 -0
- package/dist/commands/core.d.ts.map +1 -0
- package/dist/commands/core.js +107 -0
- package/dist/commands/core.js.map +1 -0
- package/dist/commands/index.d.ts +16 -0
- package/dist/commands/index.d.ts.map +1 -0
- package/dist/commands/index.js +132 -0
- package/dist/commands/index.js.map +1 -0
- package/dist/commands/refresh.d.ts +18 -0
- package/dist/commands/refresh.d.ts.map +1 -0
- package/dist/commands/refresh.js +55 -0
- package/dist/commands/refresh.js.map +1 -0
- package/dist/commands/sessions.d.ts +10 -0
- package/dist/commands/sessions.d.ts.map +1 -0
- package/dist/commands/sessions.js +125 -0
- package/dist/commands/sessions.js.map +1 -0
- package/dist/commands/skills.d.ts +10 -0
- package/dist/commands/skills.d.ts.map +1 -0
- package/dist/commands/skills.js +48 -0
- package/dist/commands/skills.js.map +1 -0
- package/dist/config.d.ts +30 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +77 -0
- package/dist/config.js.map +1 -0
- package/dist/handlers/buddy.d.ts +31 -0
- package/dist/handlers/buddy.d.ts.map +1 -0
- package/dist/handlers/buddy.js +126 -0
- package/dist/handlers/buddy.js.map +1 -0
- package/dist/handlers/events.d.ts +65 -0
- package/dist/handlers/events.d.ts.map +1 -0
- package/dist/handlers/events.js +381 -0
- package/dist/handlers/events.js.map +1 -0
- package/dist/handlers/file.d.ts +11 -0
- package/dist/handlers/file.d.ts.map +1 -0
- package/dist/handlers/file.js +138 -0
- package/dist/handlers/file.js.map +1 -0
- package/dist/handlers/message.d.ts +34 -0
- package/dist/handlers/message.d.ts.map +1 -0
- package/dist/handlers/message.js +262 -0
- package/dist/handlers/message.js.map +1 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +82 -0
- package/dist/index.js.map +1 -0
- package/dist/state.d.ts +11 -0
- package/dist/state.d.ts.map +1 -0
- package/dist/state.js +47 -0
- package/dist/state.js.map +1 -0
- package/dist/types.d.ts +50 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +5 -0
- package/dist/types.js.map +1 -0
- package/dist/util/files.d.ts +27 -0
- package/dist/util/files.d.ts.map +1 -0
- package/dist/util/files.js +75 -0
- package/dist/util/files.js.map +1 -0
- package/dist/util/telegram.d.ts +60 -0
- package/dist/util/telegram.d.ts.map +1 -0
- package/dist/util/telegram.js +192 -0
- package/dist/util/telegram.js.map +1 -0
- package/package.json +49 -0
package/dist/config.js
ADDED
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Configuration loaded from environment variables.
|
|
3
|
+
* All secrets must come from env vars â never hardcode tokens.
|
|
4
|
+
*
|
|
5
|
+
* If TELEGRAM_BOT_TOKEN is not already set, we auto-load from
|
|
6
|
+
* ~/.dreb/secrets/telegram.env (the same file the systemd service uses).
|
|
7
|
+
* This way `node dist/index.js` works without manual env setup.
|
|
8
|
+
*/
|
|
9
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
10
|
+
import { homedir } from "node:os";
|
|
11
|
+
import { join } from "node:path";
|
|
12
|
+
/** Default path to the secrets env file */
|
|
13
|
+
export const DEFAULT_SECRETS_FILE = join(homedir(), ".dreb", "secrets", "telegram.env");
|
|
14
|
+
/** Default path to shared provider API keys */
|
|
15
|
+
export const DEFAULT_PROVIDERS_FILE = join(homedir(), ".dreb", "secrets", "providers.env");
|
|
16
|
+
/**
|
|
17
|
+
* Load KEY=VALUE pairs from a file into process.env (without overwriting).
|
|
18
|
+
* Handles quoting, comments, and empty lines.
|
|
19
|
+
*/
|
|
20
|
+
function loadEnvFile(path) {
|
|
21
|
+
if (!existsSync(path))
|
|
22
|
+
return;
|
|
23
|
+
const content = readFileSync(path, "utf-8");
|
|
24
|
+
for (const line of content.split("\n")) {
|
|
25
|
+
const trimmed = line.trim();
|
|
26
|
+
if (!trimmed || trimmed.startsWith("#"))
|
|
27
|
+
continue;
|
|
28
|
+
const eqIndex = trimmed.indexOf("=");
|
|
29
|
+
if (eqIndex === -1)
|
|
30
|
+
continue;
|
|
31
|
+
const key = trimmed.slice(0, eqIndex).trim();
|
|
32
|
+
let value = trimmed.slice(eqIndex + 1).trim();
|
|
33
|
+
// Strip surrounding quotes
|
|
34
|
+
if ((value.startsWith('"') && value.endsWith('"')) || (value.startsWith("'") && value.endsWith("'"))) {
|
|
35
|
+
value = value.slice(1, -1);
|
|
36
|
+
}
|
|
37
|
+
// Don't overwrite existing env vars (explicit env takes priority)
|
|
38
|
+
if (!(key in process.env)) {
|
|
39
|
+
process.env[key] = value;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
export function loadConfig(secretsFile = DEFAULT_SECRETS_FILE) {
|
|
44
|
+
// Auto-load provider API keys (shared with dreb CLI)
|
|
45
|
+
loadEnvFile(DEFAULT_PROVIDERS_FILE);
|
|
46
|
+
// Auto-load telegram-specific secrets
|
|
47
|
+
loadEnvFile(secretsFile);
|
|
48
|
+
const botToken = process.env.TELEGRAM_BOT_TOKEN;
|
|
49
|
+
if (!botToken) {
|
|
50
|
+
throw new Error(`TELEGRAM_BOT_TOKEN not set.\n\n` +
|
|
51
|
+
`Either:\n` +
|
|
52
|
+
` 1. Set the environment variable directly, or\n` +
|
|
53
|
+
` 2. Create ${secretsFile} with:\n\n` +
|
|
54
|
+
` TELEGRAM_BOT_TOKEN=your-token-here\n` +
|
|
55
|
+
` ALLOWED_USER_IDS=your-user-id-here\n\n` +
|
|
56
|
+
` Then: chmod 600 ${secretsFile}`);
|
|
57
|
+
}
|
|
58
|
+
const allowedUserIds = (process.env.ALLOWED_USER_IDS || "")
|
|
59
|
+
.split(",")
|
|
60
|
+
.filter((id) => id.trim())
|
|
61
|
+
.map((id) => {
|
|
62
|
+
const n = Number.parseInt(id.trim(), 10);
|
|
63
|
+
if (Number.isNaN(n))
|
|
64
|
+
throw new Error(`Invalid user ID: ${id}`);
|
|
65
|
+
return n;
|
|
66
|
+
});
|
|
67
|
+
return {
|
|
68
|
+
botToken,
|
|
69
|
+
allowedUserIds,
|
|
70
|
+
workingDir: process.env.DREB_WORKING_DIR || process.env.HOME || "/",
|
|
71
|
+
drebPath: process.env.DREB_PATH || "dreb",
|
|
72
|
+
serviceName: process.env.DREB_TELEGRAM_SERVICE || "dreb-telegram",
|
|
73
|
+
provider: process.env.DREB_PROVIDER,
|
|
74
|
+
model: process.env.DREB_MODEL,
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
//# sourceMappingURL=config.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"config.js","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Configuration loaded from environment variables.\n * All secrets must come from env vars â never hardcode tokens.\n *\n * If TELEGRAM_BOT_TOKEN is not already set, we auto-load from\n * ~/.dreb/secrets/telegram.env (the same file the systemd service uses).\n * This way `node dist/index.js` works without manual env setup.\n */\n\nimport { existsSync, readFileSync } from \"node:fs\";\nimport { homedir } from \"node:os\";\nimport { join } from \"node:path\";\n\nexport interface Config {\n\t/** Telegram Bot API token */\n\tbotToken: string;\n\t/** Authorized Telegram user IDs */\n\tallowedUserIds: number[];\n\t/** Working directory for dreb sessions */\n\tworkingDir: string;\n\t/** Path to the dreb CLI binary */\n\tdrebPath: string;\n\t/** Systemd service name for /restart */\n\tserviceName: string;\n\t/** Provider to use for dreb */\n\tprovider?: string;\n\t/** Model to use for dreb */\n\tmodel?: string;\n}\n\n/** Default path to the secrets env file */\nexport const DEFAULT_SECRETS_FILE = join(homedir(), \".dreb\", \"secrets\", \"telegram.env\");\n\n/** Default path to shared provider API keys */\nexport const DEFAULT_PROVIDERS_FILE = join(homedir(), \".dreb\", \"secrets\", \"providers.env\");\n\n/**\n * Load KEY=VALUE pairs from a file into process.env (without overwriting).\n * Handles quoting, comments, and empty lines.\n */\nfunction loadEnvFile(path: string): void {\n\tif (!existsSync(path)) return;\n\n\tconst content = readFileSync(path, \"utf-8\");\n\tfor (const line of content.split(\"\\n\")) {\n\t\tconst trimmed = line.trim();\n\t\tif (!trimmed || trimmed.startsWith(\"#\")) continue;\n\n\t\tconst eqIndex = trimmed.indexOf(\"=\");\n\t\tif (eqIndex === -1) continue;\n\n\t\tconst key = trimmed.slice(0, eqIndex).trim();\n\t\tlet value = trimmed.slice(eqIndex + 1).trim();\n\n\t\t// Strip surrounding quotes\n\t\tif ((value.startsWith('\"') && value.endsWith('\"')) || (value.startsWith(\"'\") && value.endsWith(\"'\"))) {\n\t\t\tvalue = value.slice(1, -1);\n\t\t}\n\n\t\t// Don't overwrite existing env vars (explicit env takes priority)\n\t\tif (!(key in process.env)) {\n\t\t\tprocess.env[key] = value;\n\t\t}\n\t}\n}\n\nexport function loadConfig(secretsFile: string = DEFAULT_SECRETS_FILE): Config {\n\t// Auto-load provider API keys (shared with dreb CLI)\n\tloadEnvFile(DEFAULT_PROVIDERS_FILE);\n\n\t// Auto-load telegram-specific secrets\n\tloadEnvFile(secretsFile);\n\n\tconst botToken = process.env.TELEGRAM_BOT_TOKEN;\n\tif (!botToken) {\n\t\tthrow new Error(\n\t\t\t`TELEGRAM_BOT_TOKEN not set.\\n\\n` +\n\t\t\t\t`Either:\\n` +\n\t\t\t\t` 1. Set the environment variable directly, or\\n` +\n\t\t\t\t` 2. Create ${secretsFile} with:\\n\\n` +\n\t\t\t\t` TELEGRAM_BOT_TOKEN=your-token-here\\n` +\n\t\t\t\t` ALLOWED_USER_IDS=your-user-id-here\\n\\n` +\n\t\t\t\t` Then: chmod 600 ${secretsFile}`,\n\t\t);\n\t}\n\n\tconst allowedUserIds = (process.env.ALLOWED_USER_IDS || \"\")\n\t\t.split(\",\")\n\t\t.filter((id) => id.trim())\n\t\t.map((id) => {\n\t\t\tconst n = Number.parseInt(id.trim(), 10);\n\t\t\tif (Number.isNaN(n)) throw new Error(`Invalid user ID: ${id}`);\n\t\t\treturn n;\n\t\t});\n\n\treturn {\n\t\tbotToken,\n\t\tallowedUserIds,\n\t\tworkingDir: process.env.DREB_WORKING_DIR || process.env.HOME || \"/\",\n\t\tdrebPath: process.env.DREB_PATH || \"dreb\",\n\t\tserviceName: process.env.DREB_TELEGRAM_SERVICE || \"dreb-telegram\",\n\t\tprovider: process.env.DREB_PROVIDER,\n\t\tmodel: process.env.DREB_MODEL,\n\t};\n}\n"]}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Telegram buddy handler â creates and configures a BuddyController
|
|
3
|
+
* with Telegram-specific rendering callbacks and formatting.
|
|
4
|
+
*/
|
|
5
|
+
import type { BuddyState } from "@dreb/coding-agent/buddy";
|
|
6
|
+
import { BuddyController } from "@dreb/coding-agent/buddy";
|
|
7
|
+
import type { Api } from "grammy";
|
|
8
|
+
import type { Config } from "../config.js";
|
|
9
|
+
import type { UserState } from "../types.js";
|
|
10
|
+
import type { SendFn } from "./events.js";
|
|
11
|
+
export declare const SPECIES_EMOJI: Record<string, string>;
|
|
12
|
+
/**
|
|
13
|
+
* Format a stats panel as a Telegram code block.
|
|
14
|
+
* Shows stat bars using â and â characters.
|
|
15
|
+
*/
|
|
16
|
+
export declare function formatBuddyStats(state: BuddyState): string;
|
|
17
|
+
/**
|
|
18
|
+
* Format a buddy speech/reaction message.
|
|
19
|
+
*/
|
|
20
|
+
export declare function formatBuddySpeech(name: string, species: string, text: string): string;
|
|
21
|
+
/**
|
|
22
|
+
* Create a BuddyController wired up with Telegram-specific callbacks.
|
|
23
|
+
*
|
|
24
|
+
* @param send â outbox send function (enqueueSend) for reliable delivery
|
|
25
|
+
* @param api â grammy Api instance for chat actions and reactions
|
|
26
|
+
* @param chatId â Telegram chat ID for this user
|
|
27
|
+
* @param config â bot config (for bridge resolution in hatch/reroll)
|
|
28
|
+
* @param userState â per-user state (for bridge resolution in hatch/reroll)
|
|
29
|
+
*/
|
|
30
|
+
export declare function createTelegramBuddyController(send: SendFn, api: Api, chatId: number, config: Config, userState: UserState): BuddyController;
|
|
31
|
+
//# sourceMappingURL=buddy.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"buddy.d.ts","sourceRoot":"","sources":["../../src/handlers/buddy.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,KAAK,EAAkB,UAAU,EAAE,MAAM,0BAA0B,CAAC;AAC3E,OAAO,EAAE,eAAe,EAAuC,MAAM,0BAA0B,CAAC;AAChG,OAAO,KAAK,EAAE,GAAG,EAAE,MAAM,QAAQ,CAAC;AAElC,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,cAAc,CAAC;AAC3C,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,aAAa,CAAC;AAC7C,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,aAAa,CAAC;AAM1C,eAAO,MAAM,aAAa,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAmBhD,CAAC;AAUF;;;GAGG;AACH,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,UAAU,GAAG,MAAM,CAgC1D;AAED;;GAEG;AACH,wBAAgB,iBAAiB,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,MAAM,CAErF;AAMD;;;;;;;;GAQG;AACH,wBAAgB,6BAA6B,CAC5C,IAAI,EAAE,MAAM,EACZ,GAAG,EAAE,GAAG,EACR,MAAM,EAAE,MAAM,EACd,MAAM,EAAE,MAAM,EACd,SAAS,EAAE,SAAS,GAClB,eAAe,CAwCjB","sourcesContent":["/**\n * Telegram buddy handler â creates and configures a BuddyController\n * with Telegram-specific rendering callbacks and formatting.\n */\n\nimport type { BuddyCallbacks, BuddyState } from \"@dreb/coding-agent/buddy\";\nimport { BuddyController, BuddyManager, STAT_NAMES, type Stat } from \"@dreb/coding-agent/buddy\";\nimport type { Api } from \"grammy\";\nimport { ensureBridgeWithSession } from \"../bridge-lifecycle.js\";\nimport type { Config } from \"../config.js\";\nimport type { UserState } from \"../types.js\";\nimport type { SendFn } from \"./events.js\";\n\n// ---------------------------------------------------------------------------\n// Species emoji mapping\n// ---------------------------------------------------------------------------\n\nexport const SPECIES_EMOJI: Record<string, string> = {\n\tDuck: \"đĻ\",\n\tGoose: \"đĒŋ\",\n\tBlob: \"đĸ\",\n\tCat: \"đą\",\n\tDragon: \"đ\",\n\tOctopus: \"đ\",\n\tOwl: \"đĻ\",\n\tPenguin: \"đ§\",\n\tTurtle: \"đĸ\",\n\tSnail: \"đ\",\n\tGhost: \"đģ\",\n\tAxolotl: \"đĢ\",\n\tCapybara: \"đĻĢ\",\n\tCactus: \"đĩ\",\n\tRobot: \"đ¤\",\n\tRabbit: \"đ°\",\n\tMushroom: \"đ\",\n\tChonk: \"đš\",\n};\n\nfunction speciesEmoji(species: string): string {\n\treturn SPECIES_EMOJI[species] ?? \"đŖ\";\n}\n\n// ---------------------------------------------------------------------------\n// Formatting helpers\n// ---------------------------------------------------------------------------\n\n/**\n * Format a stats panel as a Telegram code block.\n * Shows stat bars using â and â characters.\n */\nexport function formatBuddyStats(state: BuddyState): string {\n\tconst emoji = speciesEmoji(state.species);\n\tconst shiny = state.shiny ? \" ⨠SHINY!\" : \"\";\n\tconst maxBar = 10;\n\tconst personality = state.personality.replace(/\\n/g, \" \");\n\tconst backstory = state.backstory.replace(/\\n/g, \" \");\n\tconst hatched = state.hatchedAt ? new Date(state.hatchedAt).toLocaleDateString() : \"unknown\";\n\n\tconst statLines = STAT_NAMES.map((s: string) => {\n\t\tconst value = state.stats[s as Stat];\n\t\tconst filled = Math.round((value / 100) * maxBar);\n\t\tconst bar = \"â\".repeat(filled) + \"â\".repeat(maxBar - filled);\n\t\treturn `â ${s.padEnd(12)} ${bar} ${value}`;\n\t});\n\n\treturn [\n\t\t`\\`\\`\\``,\n\t\t`ââ ${emoji} ${state.name} ${shiny} âââââââââââââââââââŽ`,\n\t\t`â Species: ${state.species}`,\n\t\t`â Rarity: ${state.rarity}`,\n\t\t`â Eyes: ${state.eyeStyle} Hat: ${state.hat || \"none\"}`,\n\t\t`â Hatched: ${hatched}`,\n\t\t`â Re-rolls: ${state.rerollCount}`,\n\t\t`â`,\n\t\t`â Stats:`,\n\t\t...statLines,\n\t\t`â`,\n\t\t`â Personality: ${personality}`,\n\t\t`â Backstory: ${backstory}`,\n\t\t`â°âââââââââââââââââââââââââââââââââââ¯`,\n\t\t`\\`\\`\\``,\n\t].join(\"\\n\");\n}\n\n/**\n * Format a buddy speech/reaction message.\n */\nexport function formatBuddySpeech(name: string, species: string, text: string): string {\n\treturn `${speciesEmoji(species)} ${name}: \"${text}\"`;\n}\n\n// ---------------------------------------------------------------------------\n// Factory\n// ---------------------------------------------------------------------------\n\n/**\n * Create a BuddyController wired up with Telegram-specific callbacks.\n *\n * @param send â outbox send function (enqueueSend) for reliable delivery\n * @param api â grammy Api instance for chat actions and reactions\n * @param chatId â Telegram chat ID for this user\n * @param config â bot config (for bridge resolution in hatch/reroll)\n * @param userState â per-user state (for bridge resolution in hatch/reroll)\n */\nexport function createTelegramBuddyController(\n\tsend: SendFn,\n\tapi: Api,\n\tchatId: number,\n\tconfig: Config,\n\tuserState: UserState,\n): BuddyController {\n\tconst manager = new BuddyManager();\n\n\tconst callbacks: BuddyCallbacks = {\n\t\tonSpeech(text: string): void {\n\t\t\tconst name = manager.getName() ?? \"Buddy\";\n\t\t\tconst state = manager.getState();\n\t\t\tconst species = state?.species ?? \"\";\n\t\t\tsend(formatBuddySpeech(name, species, text));\n\t\t},\n\t\tonThinkingStart(): void {\n\t\t\t// Fire-and-forget typing indicator\n\t\t\tapi.sendChatAction(chatId, \"typing\").catch(() => {});\n\t\t},\n\t\tonThinkingEnd(): void {\n\t\t\t// No-op â can't cancel a chat action in Telegram\n\t\t},\n\t\tasync onHatch(_mgr): Promise<BuddyState> {\n\t\t\tconst bridge = await ensureBridgeWithSession(config, userState);\n\t\t\treturn bridge.buddyHatch();\n\t\t},\n\t\tasync onReroll(_mgr): Promise<BuddyState> {\n\t\t\tconst bridge = await ensureBridgeWithSession(config, userState);\n\t\t\treturn bridge.buddyReroll();\n\t\t},\n\t};\n\n\tconst controller = new BuddyController(manager, callbacks, {\n\t\tidleTimeoutMs: 30_000,\n\t\treactionCooldownMs: 60_000,\n\t\tcontextMaxEntries: 20,\n\t\tactivityGateMs: 7_200_000, // 2 hours\n\t\treactionsPerHour: 3,\n\t});\n\n\t// Auto-load buddy from shared buddy.json (same file TUI uses).\n\t// If a buddy exists and is visible, it'll be active immediately.\n\tcontroller.start();\n\n\treturn controller;\n}\n"]}
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Telegram buddy handler â creates and configures a BuddyController
|
|
3
|
+
* with Telegram-specific rendering callbacks and formatting.
|
|
4
|
+
*/
|
|
5
|
+
import { BuddyController, BuddyManager, STAT_NAMES } from "@dreb/coding-agent/buddy";
|
|
6
|
+
import { ensureBridgeWithSession } from "../bridge-lifecycle.js";
|
|
7
|
+
// ---------------------------------------------------------------------------
|
|
8
|
+
// Species emoji mapping
|
|
9
|
+
// ---------------------------------------------------------------------------
|
|
10
|
+
export const SPECIES_EMOJI = {
|
|
11
|
+
Duck: "đĻ",
|
|
12
|
+
Goose: "đĒŋ",
|
|
13
|
+
Blob: "đĸ",
|
|
14
|
+
Cat: "đą",
|
|
15
|
+
Dragon: "đ",
|
|
16
|
+
Octopus: "đ",
|
|
17
|
+
Owl: "đĻ",
|
|
18
|
+
Penguin: "đ§",
|
|
19
|
+
Turtle: "đĸ",
|
|
20
|
+
Snail: "đ",
|
|
21
|
+
Ghost: "đģ",
|
|
22
|
+
Axolotl: "đĢ",
|
|
23
|
+
Capybara: "đĻĢ",
|
|
24
|
+
Cactus: "đĩ",
|
|
25
|
+
Robot: "đ¤",
|
|
26
|
+
Rabbit: "đ°",
|
|
27
|
+
Mushroom: "đ",
|
|
28
|
+
Chonk: "đš",
|
|
29
|
+
};
|
|
30
|
+
function speciesEmoji(species) {
|
|
31
|
+
return SPECIES_EMOJI[species] ?? "đŖ";
|
|
32
|
+
}
|
|
33
|
+
// ---------------------------------------------------------------------------
|
|
34
|
+
// Formatting helpers
|
|
35
|
+
// ---------------------------------------------------------------------------
|
|
36
|
+
/**
|
|
37
|
+
* Format a stats panel as a Telegram code block.
|
|
38
|
+
* Shows stat bars using â and â characters.
|
|
39
|
+
*/
|
|
40
|
+
export function formatBuddyStats(state) {
|
|
41
|
+
const emoji = speciesEmoji(state.species);
|
|
42
|
+
const shiny = state.shiny ? " ⨠SHINY!" : "";
|
|
43
|
+
const maxBar = 10;
|
|
44
|
+
const personality = state.personality.replace(/\n/g, " ");
|
|
45
|
+
const backstory = state.backstory.replace(/\n/g, " ");
|
|
46
|
+
const hatched = state.hatchedAt ? new Date(state.hatchedAt).toLocaleDateString() : "unknown";
|
|
47
|
+
const statLines = STAT_NAMES.map((s) => {
|
|
48
|
+
const value = state.stats[s];
|
|
49
|
+
const filled = Math.round((value / 100) * maxBar);
|
|
50
|
+
const bar = "â".repeat(filled) + "â".repeat(maxBar - filled);
|
|
51
|
+
return `â ${s.padEnd(12)} ${bar} ${value}`;
|
|
52
|
+
});
|
|
53
|
+
return [
|
|
54
|
+
`\`\`\``,
|
|
55
|
+
`ââ ${emoji} ${state.name} ${shiny} âââââââââââââââââââŽ`,
|
|
56
|
+
`â Species: ${state.species}`,
|
|
57
|
+
`â Rarity: ${state.rarity}`,
|
|
58
|
+
`â Eyes: ${state.eyeStyle} Hat: ${state.hat || "none"}`,
|
|
59
|
+
`â Hatched: ${hatched}`,
|
|
60
|
+
`â Re-rolls: ${state.rerollCount}`,
|
|
61
|
+
`â`,
|
|
62
|
+
`â Stats:`,
|
|
63
|
+
...statLines,
|
|
64
|
+
`â`,
|
|
65
|
+
`â Personality: ${personality}`,
|
|
66
|
+
`â Backstory: ${backstory}`,
|
|
67
|
+
`â°âââââââââââââââââââââââââââââââââââ¯`,
|
|
68
|
+
`\`\`\``,
|
|
69
|
+
].join("\n");
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Format a buddy speech/reaction message.
|
|
73
|
+
*/
|
|
74
|
+
export function formatBuddySpeech(name, species, text) {
|
|
75
|
+
return `${speciesEmoji(species)} ${name}: "${text}"`;
|
|
76
|
+
}
|
|
77
|
+
// ---------------------------------------------------------------------------
|
|
78
|
+
// Factory
|
|
79
|
+
// ---------------------------------------------------------------------------
|
|
80
|
+
/**
|
|
81
|
+
* Create a BuddyController wired up with Telegram-specific callbacks.
|
|
82
|
+
*
|
|
83
|
+
* @param send â outbox send function (enqueueSend) for reliable delivery
|
|
84
|
+
* @param api â grammy Api instance for chat actions and reactions
|
|
85
|
+
* @param chatId â Telegram chat ID for this user
|
|
86
|
+
* @param config â bot config (for bridge resolution in hatch/reroll)
|
|
87
|
+
* @param userState â per-user state (for bridge resolution in hatch/reroll)
|
|
88
|
+
*/
|
|
89
|
+
export function createTelegramBuddyController(send, api, chatId, config, userState) {
|
|
90
|
+
const manager = new BuddyManager();
|
|
91
|
+
const callbacks = {
|
|
92
|
+
onSpeech(text) {
|
|
93
|
+
const name = manager.getName() ?? "Buddy";
|
|
94
|
+
const state = manager.getState();
|
|
95
|
+
const species = state?.species ?? "";
|
|
96
|
+
send(formatBuddySpeech(name, species, text));
|
|
97
|
+
},
|
|
98
|
+
onThinkingStart() {
|
|
99
|
+
// Fire-and-forget typing indicator
|
|
100
|
+
api.sendChatAction(chatId, "typing").catch(() => { });
|
|
101
|
+
},
|
|
102
|
+
onThinkingEnd() {
|
|
103
|
+
// No-op â can't cancel a chat action in Telegram
|
|
104
|
+
},
|
|
105
|
+
async onHatch(_mgr) {
|
|
106
|
+
const bridge = await ensureBridgeWithSession(config, userState);
|
|
107
|
+
return bridge.buddyHatch();
|
|
108
|
+
},
|
|
109
|
+
async onReroll(_mgr) {
|
|
110
|
+
const bridge = await ensureBridgeWithSession(config, userState);
|
|
111
|
+
return bridge.buddyReroll();
|
|
112
|
+
},
|
|
113
|
+
};
|
|
114
|
+
const controller = new BuddyController(manager, callbacks, {
|
|
115
|
+
idleTimeoutMs: 30_000,
|
|
116
|
+
reactionCooldownMs: 60_000,
|
|
117
|
+
contextMaxEntries: 20,
|
|
118
|
+
activityGateMs: 7_200_000, // 2 hours
|
|
119
|
+
reactionsPerHour: 3,
|
|
120
|
+
});
|
|
121
|
+
// Auto-load buddy from shared buddy.json (same file TUI uses).
|
|
122
|
+
// If a buddy exists and is visible, it'll be active immediately.
|
|
123
|
+
controller.start();
|
|
124
|
+
return controller;
|
|
125
|
+
}
|
|
126
|
+
//# sourceMappingURL=buddy.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"buddy.js","sourceRoot":"","sources":["../../src/handlers/buddy.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Telegram buddy handler â creates and configures a BuddyController\n * with Telegram-specific rendering callbacks and formatting.\n */\n\nimport type { BuddyCallbacks, BuddyState } from \"@dreb/coding-agent/buddy\";\nimport { BuddyController, BuddyManager, STAT_NAMES, type Stat } from \"@dreb/coding-agent/buddy\";\nimport type { Api } from \"grammy\";\nimport { ensureBridgeWithSession } from \"../bridge-lifecycle.js\";\nimport type { Config } from \"../config.js\";\nimport type { UserState } from \"../types.js\";\nimport type { SendFn } from \"./events.js\";\n\n// ---------------------------------------------------------------------------\n// Species emoji mapping\n// ---------------------------------------------------------------------------\n\nexport const SPECIES_EMOJI: Record<string, string> = {\n\tDuck: \"đĻ\",\n\tGoose: \"đĒŋ\",\n\tBlob: \"đĸ\",\n\tCat: \"đą\",\n\tDragon: \"đ\",\n\tOctopus: \"đ\",\n\tOwl: \"đĻ\",\n\tPenguin: \"đ§\",\n\tTurtle: \"đĸ\",\n\tSnail: \"đ\",\n\tGhost: \"đģ\",\n\tAxolotl: \"đĢ\",\n\tCapybara: \"đĻĢ\",\n\tCactus: \"đĩ\",\n\tRobot: \"đ¤\",\n\tRabbit: \"đ°\",\n\tMushroom: \"đ\",\n\tChonk: \"đš\",\n};\n\nfunction speciesEmoji(species: string): string {\n\treturn SPECIES_EMOJI[species] ?? \"đŖ\";\n}\n\n// ---------------------------------------------------------------------------\n// Formatting helpers\n// ---------------------------------------------------------------------------\n\n/**\n * Format a stats panel as a Telegram code block.\n * Shows stat bars using â and â characters.\n */\nexport function formatBuddyStats(state: BuddyState): string {\n\tconst emoji = speciesEmoji(state.species);\n\tconst shiny = state.shiny ? \" ⨠SHINY!\" : \"\";\n\tconst maxBar = 10;\n\tconst personality = state.personality.replace(/\\n/g, \" \");\n\tconst backstory = state.backstory.replace(/\\n/g, \" \");\n\tconst hatched = state.hatchedAt ? new Date(state.hatchedAt).toLocaleDateString() : \"unknown\";\n\n\tconst statLines = STAT_NAMES.map((s: string) => {\n\t\tconst value = state.stats[s as Stat];\n\t\tconst filled = Math.round((value / 100) * maxBar);\n\t\tconst bar = \"â\".repeat(filled) + \"â\".repeat(maxBar - filled);\n\t\treturn `â ${s.padEnd(12)} ${bar} ${value}`;\n\t});\n\n\treturn [\n\t\t`\\`\\`\\``,\n\t\t`ââ ${emoji} ${state.name} ${shiny} âââââââââââââââââââŽ`,\n\t\t`â Species: ${state.species}`,\n\t\t`â Rarity: ${state.rarity}`,\n\t\t`â Eyes: ${state.eyeStyle} Hat: ${state.hat || \"none\"}`,\n\t\t`â Hatched: ${hatched}`,\n\t\t`â Re-rolls: ${state.rerollCount}`,\n\t\t`â`,\n\t\t`â Stats:`,\n\t\t...statLines,\n\t\t`â`,\n\t\t`â Personality: ${personality}`,\n\t\t`â Backstory: ${backstory}`,\n\t\t`â°âââââââââââââââââââââââââââââââââââ¯`,\n\t\t`\\`\\`\\``,\n\t].join(\"\\n\");\n}\n\n/**\n * Format a buddy speech/reaction message.\n */\nexport function formatBuddySpeech(name: string, species: string, text: string): string {\n\treturn `${speciesEmoji(species)} ${name}: \"${text}\"`;\n}\n\n// ---------------------------------------------------------------------------\n// Factory\n// ---------------------------------------------------------------------------\n\n/**\n * Create a BuddyController wired up with Telegram-specific callbacks.\n *\n * @param send â outbox send function (enqueueSend) for reliable delivery\n * @param api â grammy Api instance for chat actions and reactions\n * @param chatId â Telegram chat ID for this user\n * @param config â bot config (for bridge resolution in hatch/reroll)\n * @param userState â per-user state (for bridge resolution in hatch/reroll)\n */\nexport function createTelegramBuddyController(\n\tsend: SendFn,\n\tapi: Api,\n\tchatId: number,\n\tconfig: Config,\n\tuserState: UserState,\n): BuddyController {\n\tconst manager = new BuddyManager();\n\n\tconst callbacks: BuddyCallbacks = {\n\t\tonSpeech(text: string): void {\n\t\t\tconst name = manager.getName() ?? \"Buddy\";\n\t\t\tconst state = manager.getState();\n\t\t\tconst species = state?.species ?? \"\";\n\t\t\tsend(formatBuddySpeech(name, species, text));\n\t\t},\n\t\tonThinkingStart(): void {\n\t\t\t// Fire-and-forget typing indicator\n\t\t\tapi.sendChatAction(chatId, \"typing\").catch(() => {});\n\t\t},\n\t\tonThinkingEnd(): void {\n\t\t\t// No-op â can't cancel a chat action in Telegram\n\t\t},\n\t\tasync onHatch(_mgr): Promise<BuddyState> {\n\t\t\tconst bridge = await ensureBridgeWithSession(config, userState);\n\t\t\treturn bridge.buddyHatch();\n\t\t},\n\t\tasync onReroll(_mgr): Promise<BuddyState> {\n\t\t\tconst bridge = await ensureBridgeWithSession(config, userState);\n\t\t\treturn bridge.buddyReroll();\n\t\t},\n\t};\n\n\tconst controller = new BuddyController(manager, callbacks, {\n\t\tidleTimeoutMs: 30_000,\n\t\treactionCooldownMs: 60_000,\n\t\tcontextMaxEntries: 20,\n\t\tactivityGateMs: 7_200_000, // 2 hours\n\t\treactionsPerHour: 3,\n\t});\n\n\t// Auto-load buddy from shared buddy.json (same file TUI uses).\n\t// If a buddy exists and is visible, it'll be active immediately.\n\tcontroller.start();\n\n\treturn controller;\n}\n"]}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Event display â translates RPC agent events into Telegram messages.
|
|
3
|
+
*
|
|
4
|
+
* Manages an ephemeral status message that shows tool use, task lists,
|
|
5
|
+
* and subagent activity. Text from the agent is sent as permanent messages.
|
|
6
|
+
*/
|
|
7
|
+
import type { Api } from "grammy";
|
|
8
|
+
import type { TrackedAgent } from "../types.js";
|
|
9
|
+
import { DebouncedEditor } from "../util/telegram.js";
|
|
10
|
+
/** Callback to queue a message for delivery â never blocks the event chain */
|
|
11
|
+
export type SendFn = (text: string, long?: boolean) => void;
|
|
12
|
+
/**
|
|
13
|
+
* RPC events include both core AgentEvent and session-specific events
|
|
14
|
+
* (tasks_update, background_agent_*, auto_compaction_*).
|
|
15
|
+
* We type loosely here since the RPC client types onEvent as AgentEvent
|
|
16
|
+
* but actually forwards all AgentSessionEvent types.
|
|
17
|
+
*/
|
|
18
|
+
type RpcEvent = {
|
|
19
|
+
type: string;
|
|
20
|
+
[key: string]: any;
|
|
21
|
+
};
|
|
22
|
+
export interface EventDisplayState {
|
|
23
|
+
/** Chat ID to send messages to */
|
|
24
|
+
chatId: number;
|
|
25
|
+
/** Message ID to reply to */
|
|
26
|
+
replyToId: number;
|
|
27
|
+
/** Ephemeral status message ID (edited in-place) */
|
|
28
|
+
statusMessageId: number | null;
|
|
29
|
+
/** Tool messages accumulated since last text */
|
|
30
|
+
toolsSinceText: string[];
|
|
31
|
+
/** Total tool count */
|
|
32
|
+
toolCount: number;
|
|
33
|
+
/** All text blocks received */
|
|
34
|
+
textBlocks: string[];
|
|
35
|
+
/** Current task list */
|
|
36
|
+
tasks: Array<{
|
|
37
|
+
id: string;
|
|
38
|
+
title: string;
|
|
39
|
+
status: string;
|
|
40
|
+
}>;
|
|
41
|
+
/** Background agents */
|
|
42
|
+
backgroundAgents: Map<string, TrackedAgent>;
|
|
43
|
+
/** Whether agent has finished */
|
|
44
|
+
done: boolean;
|
|
45
|
+
/** Debounced editor instance */
|
|
46
|
+
editor: DebouncedEditor;
|
|
47
|
+
/** Whether auto-retry is in progress (Layer 1: reactive â set by auto_retry_start) */
|
|
48
|
+
retryInProgress: boolean;
|
|
49
|
+
/** Whether a retry is expected (Layer 2: predictive â set by agent_end when error looks retryable) */
|
|
50
|
+
pendingRetry: boolean;
|
|
51
|
+
/** Current retry attempt number for display */
|
|
52
|
+
retryAttempt: number;
|
|
53
|
+
/** Buddy controller â receives agent events for context + reactions */
|
|
54
|
+
buddyController?: any;
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Create a fresh event display state for a new agent run.
|
|
58
|
+
*/
|
|
59
|
+
export declare function createEventDisplay(api: Api, chatId: number, replyToId: number, statusMessageId: number | null): EventDisplayState;
|
|
60
|
+
/**
|
|
61
|
+
* Process an agent event and update the display.
|
|
62
|
+
*/
|
|
63
|
+
export declare function handleAgentEvent(send: SendFn, api: Api, state: EventDisplayState, event: RpcEvent): Promise<void>;
|
|
64
|
+
export {};
|
|
65
|
+
//# sourceMappingURL=events.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"events.d.ts","sourceRoot":"","sources":["../../src/handlers/events.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAGH,OAAO,KAAK,EAAE,GAAG,EAAE,MAAM,QAAQ,CAAC;AAElC,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAEhD,OAAO,EAAE,eAAe,EAAmB,MAAM,qBAAqB,CAAC;AAEvE,gFAA8E;AAC9E,MAAM,MAAM,MAAM,GAAG,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,OAAO,KAAK,IAAI,CAAC;AAE5D;;;;;GAKG;AACH,KAAK,QAAQ,GAAG;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,CAAC,GAAG,EAAE,MAAM,GAAG,GAAG,CAAA;CAAE,CAAC;AAmErD,MAAM,WAAW,iBAAiB;IACjC,kCAAkC;IAClC,MAAM,EAAE,MAAM,CAAC;IACf,6BAA6B;IAC7B,SAAS,EAAE,MAAM,CAAC;IAClB,oDAAoD;IACpD,eAAe,EAAE,MAAM,GAAG,IAAI,CAAC;IAC/B,gDAAgD;IAChD,cAAc,EAAE,MAAM,EAAE,CAAC;IACzB,uBAAuB;IACvB,SAAS,EAAE,MAAM,CAAC;IAClB,+BAA+B;IAC/B,UAAU,EAAE,MAAM,EAAE,CAAC;IACrB,wBAAwB;IACxB,KAAK,EAAE,KAAK,CAAC;QAAE,EAAE,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;IAC5D,wBAAwB;IACxB,gBAAgB,EAAE,GAAG,CAAC,MAAM,EAAE,YAAY,CAAC,CAAC;IAC5C,iCAAiC;IACjC,IAAI,EAAE,OAAO,CAAC;IACd,gCAAgC;IAChC,MAAM,EAAE,eAAe,CAAC;IACxB,wFAAsF;IACtF,eAAe,EAAE,OAAO,CAAC;IACzB,wGAAsG;IACtG,YAAY,EAAE,OAAO,CAAC;IACtB,+CAA+C;IAC/C,YAAY,EAAE,MAAM,CAAC;IACrB,yEAAuE;IACvE,eAAe,CAAC,EAAE,GAAG,CAAC;CACtB;AAaD;;GAEG;AACH,wBAAgB,kBAAkB,CACjC,GAAG,EAAE,GAAG,EACR,MAAM,EAAE,MAAM,EACd,SAAS,EAAE,MAAM,EACjB,eAAe,EAAE,MAAM,GAAG,IAAI,GAC5B,iBAAiB,CAgBnB;AAED;;GAEG;AACH,wBAAsB,gBAAgB,CACrC,IAAI,EAAE,MAAM,EACZ,GAAG,EAAE,GAAG,EACR,KAAK,EAAE,iBAAiB,EACxB,KAAK,EAAE,QAAQ,GACb,OAAO,CAAC,IAAI,CAAC,CA6Qf","sourcesContent":["/**\n * Event display â translates RPC agent events into Telegram messages.\n *\n * Manages an ephemeral status message that shows tool use, task lists,\n * and subagent activity. Text from the agent is sent as permanent messages.\n */\n\nimport { existsSync } from \"node:fs\";\nimport type { Api } from \"grammy\";\nimport { InputFile } from \"grammy\";\nimport type { TrackedAgent } from \"../types.js\";\nimport { extractSendFiles } from \"../util/files.js\";\nimport { DebouncedEditor, log, safeDelete } from \"../util/telegram.js\";\n\n/** Callback to queue a message for delivery â never blocks the event chain */\nexport type SendFn = (text: string, long?: boolean) => void;\n\n/**\n * RPC events include both core AgentEvent and session-specific events\n * (tasks_update, background_agent_*, auto_compaction_*).\n * We type loosely here since the RPC client types onEvent as AgentEvent\n * but actually forwards all AgentSessionEvent types.\n */\ntype RpcEvent = { type: string; [key: string]: any };\n\n// Tool emoji mapping (tool names are lowercase in definitions)\nconst TOOL_EMOJI: Record<string, string> = {\n\tbash: \"đ§\",\n\tread: \"đ\",\n\tedit: \"âī¸\",\n\twrite: \"đ\",\n\tgrep: \"đ\",\n\tfind: \"đ\",\n\tls: \"đ\",\n\tweb_search: \"đ\",\n\tweb_fetch: \"đ\",\n\tsubagent: \"đ¤\",\n\ttasks_update: \"đ\",\n\tskill: \"âĄ\",\n};\n\nfunction toolEmoji(name: string): string {\n\treturn TOOL_EMOJI[name] || \"đ§\";\n}\n\n/** Format a tool call for display */\nfunction formatTool(name: string, args: Record<string, any>): string {\n\tconst emoji = toolEmoji(name);\n\tswitch (name) {\n\t\tcase \"bash\": {\n\t\t\tconst cmd = args.command || \"\";\n\t\t\treturn `${emoji} *bash*\\n\\`${cmd.slice(0, 500)}\\``;\n\t\t}\n\t\tcase \"read\":\n\t\t\treturn `${emoji} *read*: \\`${args.path || \"?\"}\\``;\n\t\tcase \"edit\":\n\t\t\treturn `${emoji} *edit*: \\`${args.path || \"?\"}\\``;\n\t\tcase \"write\":\n\t\t\treturn `${emoji} *write*: \\`${args.path || \"?\"}\\``;\n\t\tcase \"grep\":\n\t\t\treturn `${emoji} *grep*: \\`${args.pattern || \"?\"}\\``;\n\t\tcase \"find\":\n\t\t\treturn `${emoji} *find*: \\`${args.pattern || \"?\"}\\``;\n\t\tcase \"ls\":\n\t\t\treturn `${emoji} *ls*: \\`${args.path || \".\"}\\``;\n\t\tcase \"web_search\":\n\t\t\treturn `${emoji} *web\\\\_search*: ${args.query || \"?\"}`;\n\t\tcase \"web_fetch\":\n\t\t\treturn `${emoji} *web\\\\_fetch*: ${(args.url || \"?\").slice(0, 80)}`;\n\t\tcase \"subagent\":\n\t\t\treturn `${emoji} *subagent* (${args.agent || \"?\"}): ${(args.task || args.tasks?.[0]?.task || \"?\").slice(0, 200)}`;\n\t\tcase \"skill\":\n\t\t\treturn `${emoji} *skill*: ${args.skill || \"?\"}`;\n\t\tdefault:\n\t\t\treturn `${emoji} *${name}*`;\n\t}\n}\n\n/** Format task list as checklist */\nfunction formatTaskList(tasks: Array<{ id: string; title: string; status: string }>): string {\n\tif (!tasks.length) return \"đ *Tasks*: (empty)\";\n\tconst lines = [\"đ *Tasks*:\"];\n\tfor (const task of tasks) {\n\t\tif (task.status === \"completed\") lines.push(` â
${task.title}`);\n\t\telse if (task.status === \"in_progress\") lines.push(` đ ${task.title}`);\n\t\telse lines.push(` âŦ ${task.title}`);\n\t}\n\treturn lines.join(\"\\n\");\n}\n\nexport interface EventDisplayState {\n\t/** Chat ID to send messages to */\n\tchatId: number;\n\t/** Message ID to reply to */\n\treplyToId: number;\n\t/** Ephemeral status message ID (edited in-place) */\n\tstatusMessageId: number | null;\n\t/** Tool messages accumulated since last text */\n\ttoolsSinceText: string[];\n\t/** Total tool count */\n\ttoolCount: number;\n\t/** All text blocks received */\n\ttextBlocks: string[];\n\t/** Current task list */\n\ttasks: Array<{ id: string; title: string; status: string }>;\n\t/** Background agents */\n\tbackgroundAgents: Map<string, TrackedAgent>;\n\t/** Whether agent has finished */\n\tdone: boolean;\n\t/** Debounced editor instance */\n\teditor: DebouncedEditor;\n\t/** Whether auto-retry is in progress (Layer 1: reactive â set by auto_retry_start) */\n\tretryInProgress: boolean;\n\t/** Whether a retry is expected (Layer 2: predictive â set by agent_end when error looks retryable) */\n\tpendingRetry: boolean;\n\t/** Current retry attempt number for display */\n\tretryAttempt: number;\n\t/** Buddy controller â receives agent events for context + reactions */\n\tbuddyController?: any;\n}\n\n/**\n * Check if an error message looks retryable (overloaded, rate limit, server errors).\n * Mirrors the core's _isRetryableError check as a defensive Layer 2.\n */\nconst RETRYABLE_ERROR_PATTERN =\n\t/overloaded|provider.?returned.?error|rate.?limit|too many requests|429|500|502|503|504|service.?unavailable|server.?error|internal.?error|network.?error|connection.?error|connection.?refused|other side closed|fetch failed|upstream.?connect|reset before headers|socket hang up|timed? out|timeout|terminated|retry delay/i;\n\nfunction isRetryableError(errorMessage: string): boolean {\n\treturn RETRYABLE_ERROR_PATTERN.test(errorMessage);\n}\n\n/**\n * Create a fresh event display state for a new agent run.\n */\nexport function createEventDisplay(\n\tapi: Api,\n\tchatId: number,\n\treplyToId: number,\n\tstatusMessageId: number | null,\n): EventDisplayState {\n\treturn {\n\t\tchatId,\n\t\treplyToId,\n\t\tstatusMessageId,\n\t\ttoolsSinceText: [],\n\t\ttoolCount: 0,\n\t\ttextBlocks: [],\n\t\ttasks: [],\n\t\tbackgroundAgents: new Map(),\n\t\tdone: false,\n\t\teditor: new DebouncedEditor(api),\n\t\tretryInProgress: false,\n\t\tpendingRetry: false,\n\t\tretryAttempt: 0,\n\t};\n}\n\n/**\n * Process an agent event and update the display.\n */\nexport async function handleAgentEvent(\n\tsend: SendFn,\n\tapi: Api,\n\tstate: EventDisplayState,\n\tevent: RpcEvent,\n): Promise<void> {\n\tswitch (event.type) {\n\t\tcase \"tool_execution_start\": {\n\t\t\tconst name = event.toolName || \"?\";\n\t\t\tconst args = event.args || {};\n\t\t\tstate.toolCount++;\n\n\t\t\t// tasks_update is shown via the separate tasks_update event â skip from tool summary\n\t\t\tif (name !== \"tasks_update\") {\n\t\t\t\tconst toolMsg = formatTool(name, args);\n\t\t\t\tstate.toolsSinceText.push(toolMsg);\n\t\t\t}\n\n\t\t\t// Update status with tool count and recent tools\n\t\t\tupdateStatus(state);\n\t\t\tbreak;\n\t\t}\n\n\t\tcase \"tool_execution_end\": {\n\t\t\t// Feed event to buddy controller for context capture + error reactions\n\t\t\tstate.buddyController?.handleEvent(event);\n\t\t\tbreak;\n\t\t}\n\n\t\tcase \"message_end\": {\n\t\t\tconst msg = event.message;\n\n\t\t\t// Show subagent results â the parent agent references these but the\n\t\t\t// Telegram user can't see them otherwise. Send the full content.\n\t\t\tif (msg?.role === \"toolResult\" && msg?.toolName === \"subagent\") {\n\t\t\t\tconst content = msg?.content;\n\t\t\t\tif (content && Array.isArray(content)) {\n\t\t\t\t\tfor (const block of content) {\n\t\t\t\t\t\tif (block.type === \"text\" && block.text?.trim()) {\n\t\t\t\t\t\t\tsend(`đ¤ *Subagent result:*\\n${block.text.trim()}`, true);\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tbreak;\n\t\t\t}\n\n\t\t\t// Show background agent completion results â these arrive as user\n\t\t\t// messages injected by agent-session.ts via prompt()/steer() and\n\t\t\t// contain the actual subagent output the model sees.\n\t\t\tif (msg?.role === \"user\") {\n\t\t\t\tconst content = msg?.content;\n\t\t\t\tif (content && Array.isArray(content)) {\n\t\t\t\t\tfor (const block of content) {\n\t\t\t\t\t\tif (block.type === \"text\" && block.text?.includes(\"<background-agent-complete>\")) {\n\t\t\t\t\t\t\t// Extract the content between the XML tags\n\t\t\t\t\t\t\tconst match = block.text.match(\n\t\t\t\t\t\t\t\t/<background-agent-complete>\\n?([\\s\\S]*?)\\n?<\\/background-agent-complete>/,\n\t\t\t\t\t\t\t);\n\t\t\t\t\t\t\tif (match?.[1]?.trim()) {\n\t\t\t\t\t\t\t\tsend(`đ¤ *Background agent complete:*\\n${match[1].trim()}`, true);\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tbreak;\n\t\t\t}\n\n\t\t\t// Only display assistant messages â user messages are echoed back by RPC\n\t\t\tif (msg?.role !== \"assistant\") break;\n\t\t\tconst content = msg?.content;\n\t\t\tif (!content || !Array.isArray(content)) break;\n\n\t\t\tfor (const block of content) {\n\t\t\t\t// Display thinking blocks (collapsed summary)\n\t\t\t\tif (block.type === \"thinking\" && block.thinking?.trim() && !block.redacted) {\n\t\t\t\t\tconst thinking = block.thinking.trim();\n\t\t\t\t\tsend(`đ _${thinking}_`, true);\n\t\t\t\t}\n\n\t\t\t\tif (block.type === \"text\" && block.text?.trim()) {\n\t\t\t\t\tconst text = block.text.trim();\n\n\t\t\t\t\t// Flush accumulated tools as permanent summary\n\t\t\t\t\tif (state.toolsSinceText.length > 0) {\n\t\t\t\t\t\tconst summary = `đ *${state.toolsSinceText.length} tools*:\\n${state.toolsSinceText.join(\"\\n\")}`;\n\t\t\t\t\t\tsend(summary, true);\n\t\t\t\t\t\tstate.toolsSinceText = [];\n\t\t\t\t\t}\n\n\t\t\t\t\t// Send the text as a permanent message\n\t\t\t\t\tstate.textBlocks.push(text);\n\n\t\t\t\t\t// Check for file send markers\n\t\t\t\t\tconst [cleanText, filePaths] = extractSendFiles(text);\n\t\t\t\t\tif (cleanText) {\n\t\t\t\t\t\tsend(cleanText, true);\n\t\t\t\t\t}\n\n\t\t\t\t\t// Send any requested files (silently skip non-existent paths â\n\t\t\t\t\t// the pattern may appear in explanatory text)\n\t\t\t\t\tfor (const filePath of filePaths) {\n\t\t\t\t\t\ttry {\n\t\t\t\t\t\t\tif (existsSync(filePath)) {\n\t\t\t\t\t\t\t\tawait api.sendDocument(state.chatId, new InputFile(filePath));\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t} catch (e) {\n\t\t\t\t\t\t\tlog(`[EVENTS] Failed to send file ${filePath}: ${e}`);\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t\t// Feed event to buddy controller for context capture + reactions\n\t\t\tstate.buddyController?.handleEvent(event);\n\t\t\tbreak;\n\t\t}\n\n\t\tcase \"tasks_update\": {\n\t\t\tstate.tasks = (event as any).tasks || [];\n\t\t\tupdateStatus(state);\n\t\t\tbreak;\n\t\t}\n\n\t\tcase \"background_agent_start\": {\n\t\t\tconst { agentId, agentType, taskSummary } = event as any;\n\t\t\tstate.backgroundAgents.set(agentId, {\n\t\t\t\tagentId,\n\t\t\t\tagentType,\n\t\t\t\ttaskSummary,\n\t\t\t\tstartTime: Date.now(),\n\t\t\t});\n\t\t\tupdateStatus(state);\n\t\t\tbreak;\n\t\t}\n\n\t\tcase \"background_agent_end\": {\n\t\t\tconst { agentId } = event as any;\n\t\t\tstate.backgroundAgents.delete(agentId);\n\t\t\t// Background agents completing does not end the parent's turn.\n\t\t\t// Only agent_end sets done â same as TUI behavior.\n\t\t\tupdateStatus(state);\n\t\t\tbreak;\n\t\t}\n\n\t\tcase \"auto_compaction_start\": {\n\t\t\tupdateStatusText(state, \"đ _Compacting context..._\");\n\t\t\tbreak;\n\t\t}\n\n\t\tcase \"auto_compaction_end\": {\n\t\t\tconst result = (event as any).result;\n\t\t\tif (result) {\n\t\t\t\tconst before = result.tokensBefore || 0;\n\t\t\t\tconst msg = `đ Context compacted (was ${Math.round(before / 1000)}k tokens)`;\n\t\t\t\tsend(msg);\n\t\t\t}\n\t\t\tbreak;\n\t\t}\n\n\t\t// =====================================================================\n\t\t// Auto-retry â prevents agent_end from marking done during retries\n\t\t// =====================================================================\n\n\t\tcase \"auto_retry_start\": {\n\t\t\tconst { attempt, maxAttempts, delayMs, errorMessage } = event as any;\n\t\t\tstate.retryInProgress = true;\n\t\t\tstate.pendingRetry = false; // Layer 1 has taken over from Layer 2\n\t\t\tstate.retryAttempt = attempt;\n\t\t\tconst delaySec = Math.round(delayMs / 1000);\n\t\t\tconst shortErr = errorMessage?.length > 80 ? `${errorMessage.slice(0, 80)}âĻ` : errorMessage;\n\t\t\tupdateStatusText(state, `đ _Retrying (${attempt}/${maxAttempts}) in ${delaySec}s â ${shortErr || \"error\"}_`);\n\t\t\tbreak;\n\t\t}\n\n\t\tcase \"auto_retry_end\": {\n\t\t\tconst { success, attempt, finalError } = event as any;\n\t\t\tstate.retryInProgress = false;\n\t\t\tstate.retryAttempt = 0;\n\t\t\tif (!success && finalError) {\n\t\t\t\t// Max retries exhausted â show final error\n\t\t\t\tsend(`â _Retry failed (${attempt} attempts):_ ${finalError}`, true);\n\t\t\t}\n\t\t\t// On success, the retry's agent_start/agent_end cycle will handle display normally\n\t\t\tbreak;\n\t\t}\n\n\t\tcase \"agent_end\": {\n\t\t\t// Flush any remaining tools\n\t\t\tif (state.toolsSinceText.length > 0) {\n\t\t\t\tconst summary = `đ *${state.toolsSinceText.length} tools*:\\n${state.toolsSinceText.join(\"\\n\")}`;\n\t\t\t\tsend(summary, true);\n\t\t\t\tstate.toolsSinceText = [];\n\t\t\t}\n\n\t\t\t// Check for error in agent_end messages\n\t\t\tconst errorMsg = (event.messages as any[])?.find(\n\t\t\t\t(m: any) => m.stopReason === \"error\" || m.stopReason === \"aborted\",\n\t\t\t);\n\n\t\t\t// Layer 2 (defensive): If this error looks retryable and we're not already\n\t\t\t// tracking a retry via Layer 1, don't mark done â the core will auto-retry\n\t\t\t// and emit a new agent_start/agent_end cycle.\n\t\t\tconst errorIsRetryable = errorMsg?.errorMessage && isRetryableError(errorMsg.errorMessage);\n\n\t\t\tif (errorMsg?.errorMessage) {\n\t\t\t\t// Suppress the scary error message during retry â user already saw the\n\t\t\t\t// auto_retry_start status. Only show the error if retry tracking missed it\n\t\t\t\t// (defensive: shouldn't happen, but better than silence).\n\t\t\t\tif (!state.retryInProgress && !errorIsRetryable) {\n\t\t\t\t\tconst provider = errorMsg.provider ? `${errorMsg.provider}/${errorMsg.model}` : \"\";\n\t\t\t\t\tconst prefix = provider ? `${provider}: ` : \"\";\n\t\t\t\t\tconst errLower = errorMsg.errorMessage.toLowerCase();\n\t\t\t\t\tconst hint =\n\t\t\t\t\t\terrLower.includes(\"connection\") || errLower.includes(\"timeout\") || errLower.includes(\"network\")\n\t\t\t\t\t\t\t? \"\\n_Provider may be down â try /model to switch._\"\n\t\t\t\t\t\t\t: \"\";\n\t\t\t\t\tsend(`â ${prefix}${errorMsg.errorMessage}${hint}`, true);\n\t\t\t\t}\n\t\t\t} else if (state.textBlocks.length === 0 && state.backgroundAgents.size === 0) {\n\t\t\t\t// Only show \"(No response)\" when truly done â not between agent cycles\n\t\t\t\tif (!state.retryInProgress && !errorIsRetryable) {\n\t\t\t\t\tsend(\"(No response)\");\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// Feed event to buddy controller for context capture + reactions\n\t\t\tstate.buddyController?.handleEvent(event);\n\n\t\t\t// Don't mark done if auto-retry is in progress (Layer 1) or the error\n\t\t\t// looks retryable (Layer 2 â defensive catch in case events were missed).\n\t\t\t// The core will emit a new agent_start/agent_end cycle for the retry.\n\t\t\tif (state.retryInProgress || errorIsRetryable) {\n\t\t\t\t// Signal that a retry is expected â the completion check in\n\t\t\t\t// ensureSubscribed needs this because it runs in the eventChain\n\t\t\t\t// BEFORE auto_retry_start has been processed.\n\t\t\t\tif (errorIsRetryable) state.pendingRetry = true;\n\t\t\t\t// Reset per-cycle state for the next agent loop\n\t\t\t\tstate.textBlocks = [];\n\t\t\t\tstate.toolCount = 0;\n\t\t\t\tbreak;\n\t\t\t}\n\n\t\t\t// If background agents are still running, keep the subscription alive\n\t\t\t// and reset per-cycle state for the next agent loop\n\t\t\tif (state.backgroundAgents.size > 0) {\n\t\t\t\tstate.textBlocks = [];\n\t\t\t\tstate.toolCount = 0;\n\t\t\t\tbreak;\n\t\t\t}\n\n\t\t\t// Delete ephemeral status before signaling done\n\t\t\tif (state.statusMessageId) {\n\t\t\t\tawait state.editor.flush(state.chatId, state.statusMessageId);\n\t\t\t\tawait safeDelete(api, state.chatId, state.statusMessageId);\n\t\t\t\tstate.statusMessageId = null;\n\t\t\t}\n\n\t\t\t// Clean up editor\n\t\t\tstate.editor.clear();\n\n\t\t\t// Signal done AFTER cleanup â waitForCompletion checks this flag,\n\t\t\t// so setting it last ensures status message is deleted before DONE is sent\n\t\t\tstate.done = true;\n\t\t\tbreak;\n\t\t}\n\n\t\t// Handle error responses that leak through RPC (async prompt errors)\n\t\tcase \"response\": {\n\t\t\tconst resp = event as any;\n\t\t\tif (!resp.success && resp.error) {\n\t\t\t\tsend(`â ${resp.error}`, true);\n\t\t\t}\n\t\t\tbreak;\n\t\t}\n\t}\n}\n\n/**\n * Build and push a status update to the ephemeral message.\n */\nfunction updateStatus(state: EventDisplayState): void {\n\tif (!state.statusMessageId) return;\n\n\tconst parts: string[] = [];\n\n\t// Tool count header\n\tif (state.toolCount > 0) {\n\t\tparts.push(`đ§ *Tool ${state.toolCount}*`);\n\t}\n\n\t// Task list\n\tif (state.tasks.length > 0) {\n\t\tparts.push(formatTaskList(state.tasks));\n\t}\n\n\t// Background agents\n\tif (state.backgroundAgents.size > 0) {\n\t\tfor (const agent of state.backgroundAgents.values()) {\n\t\t\tparts.push(`đ¤ *${agent.agentType}*: ${agent.taskSummary.slice(0, 200)}`);\n\t\t}\n\t}\n\n\t// Recent tools (last 5)\n\tif (state.toolsSinceText.length > 0) {\n\t\tconst recent = state.toolsSinceText.slice(-5);\n\t\tparts.push(recent.join(\"\\n\\n\"));\n\t}\n\n\tif (parts.length === 0) return;\n\n\tconst text = parts.join(\"\\n\\n\").slice(0, 4000);\n\tstate.editor.edit(state.chatId, state.statusMessageId, text);\n}\n\nfunction updateStatusText(state: EventDisplayState, text: string): void {\n\tif (!state.statusMessageId) return;\n\tstate.editor.edit(state.chatId, state.statusMessageId, text);\n}\n"]}
|