@agentprojectcontext/apx 1.33.0 → 1.33.1

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.
@@ -0,0 +1,130 @@
1
+ // Stateless helpers for the Telegram plugin. Extracted from index.js so the
2
+ // big poller class stays focused on lifecycle + message dispatch. Each
3
+ // function is pure (no `this`) — instances import them and call as needed.
4
+ import fs from "node:fs";
5
+ import { TELEGRAM_STATE_PATH } from "#core/config/index.js";
6
+
7
+ const nowIso = () => new Date().toISOString().replace(/\.\d{3}Z$/, "Z");
8
+
9
+ /**
10
+ * Build the channelMeta block the super-agent loop receives for a Telegram
11
+ * turn. The prompt template at src/core/agent/prompts/channels/telegram.md
12
+ * interpolates `{{projectBlock}}` and `{{routeBlock}}` verbatim, so we
13
+ * pre-render them as plain text (the template engine doesn't do conditionals).
14
+ */
15
+ export function buildTelegramMeta({ channelName, author, chatId, target, routeToAgent }) {
16
+ const projectBlock = target
17
+ ? `\nProject pin: **${target.name || "(unnamed)"}** (\`${target.path || "?"}\`).\n` +
18
+ "This Telegram channel belongs to that project. Default any " +
19
+ "project-scoped tool call (list_agents, list_tasks, list_mcps, " +
20
+ "list_skills, create_task, list_routines, …) to " +
21
+ `\`${target.name || target.path}\` without asking the user "which ` +
22
+ 'project?". Only ask when they explicitly reference another project ' +
23
+ "by name."
24
+ : "";
25
+ const routeBlock = routeToAgent
26
+ ? `\nMaster agent for this channel: **${routeToAgent}**. Prefer ` +
27
+ `delegating substantive work to that agent via call_agent({ project: ` +
28
+ `"${target?.name || target?.path || ""}", agent: "${routeToAgent}", ` +
29
+ "prompt: <user message> }) rather than answering yourself, unless " +
30
+ "the message is small-talk or a quick factual reply."
31
+ : "";
32
+ return {
33
+ channelName,
34
+ author,
35
+ chatId,
36
+ projectBlock,
37
+ routeBlock,
38
+ ...(target ? {
39
+ projectId: String(target.id),
40
+ projectName: target.name || "",
41
+ projectPath: target.path || "",
42
+ } : {}),
43
+ ...(routeToAgent ? { routeToAgent } : {}),
44
+ };
45
+ }
46
+
47
+ /** Load the cross-channel offset state from ~/.apx/telegram-state.json. */
48
+ export function loadState() {
49
+ if (!fs.existsSync(TELEGRAM_STATE_PATH)) return { channels: {} };
50
+ try {
51
+ const raw = JSON.parse(fs.readFileSync(TELEGRAM_STATE_PATH, "utf8"));
52
+ return { channels: raw.channels || {}, _legacy_offset: raw.offset || 0 };
53
+ } catch {
54
+ return { channels: {} };
55
+ }
56
+ }
57
+
58
+ /** Write the cross-channel offset state. Adds an `updated_at` timestamp. */
59
+ export function saveState(state) {
60
+ fs.writeFileSync(
61
+ TELEGRAM_STATE_PATH,
62
+ JSON.stringify({ ...state, updated_at: nowIso() }, null, 2) + "\n"
63
+ );
64
+ }
65
+
66
+ export function resolveBotToken(channel) {
67
+ return (
68
+ channel.bot_token ||
69
+ process.env.BOT_TELEGRAM_TOKEN ||
70
+ process.env.TELEGRAM_BOT_TOKEN ||
71
+ ""
72
+ );
73
+ }
74
+
75
+ export function resolveChatId(channel) {
76
+ return (
77
+ channel.chat_id ||
78
+ process.env.TELEGRAM_CHAT_ID ||
79
+ process.env.BOT_TELEGRAM_CHAT_ID ||
80
+ ""
81
+ );
82
+ }
83
+
84
+ export function tokenSource(channel) {
85
+ if (channel.bot_token) return "config";
86
+ if (process.env.BOT_TELEGRAM_TOKEN) return "env:BOT_TELEGRAM_TOKEN";
87
+ if (process.env.TELEGRAM_BOT_TOKEN) return "env:TELEGRAM_BOT_TOKEN";
88
+ return null;
89
+ }
90
+
91
+ /**
92
+ * Resolve the list of telegram channels to poll, honouring both the
93
+ * canonical telegram.channels[] and the legacy single-channel mode.
94
+ */
95
+ export function resolveChannels(globalConfig) {
96
+ const tg = globalConfig.telegram || {};
97
+ if (Array.isArray(tg.channels) && tg.channels.length > 0) {
98
+ return tg.channels.map((c, i) => ({
99
+ name: c.name || `channel-${i + 1}`,
100
+ bot_token: c.bot_token || "",
101
+ chat_id: c.chat_id || "",
102
+ route_to_agent: c.route_to_agent || "",
103
+ project: c.project || null,
104
+ respond_with_engine:
105
+ c.respond_with_engine !== undefined
106
+ ? c.respond_with_engine
107
+ : tg.respond_with_engine !== false,
108
+ poll_interval_ms: c.poll_interval_ms || tg.poll_interval_ms || 1500,
109
+ }));
110
+ }
111
+ // Legacy single-channel mode
112
+ if (!tg.bot_token && !process.env.BOT_TELEGRAM_TOKEN && !process.env.TELEGRAM_BOT_TOKEN) {
113
+ return [];
114
+ }
115
+ return [
116
+ {
117
+ name: "default",
118
+ bot_token: tg.bot_token || "",
119
+ chat_id: tg.chat_id || "",
120
+ route_to_agent: tg.route_to_agent || "",
121
+ project: null,
122
+ respond_with_engine: tg.respond_with_engine !== false,
123
+ poll_interval_ms: tg.poll_interval_ms || 1500,
124
+ },
125
+ ];
126
+ }
127
+
128
+ export function sleep(ms) {
129
+ return new Promise((resolve) => setTimeout(resolve, ms));
130
+ }