@timqi/pier 0.1.3 → 0.1.5
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 +12 -7
- package/dist/agent/config.js +9 -3
- package/dist/agent/events.js +13 -5
- package/dist/agent/listing.js +10 -2
- package/dist/agent/packages.js +4 -20
- package/dist/agent/pi.js +18 -86
- package/dist/channels/attach.js +2 -2
- package/dist/channels/commands.js +10 -8
- package/dist/channels/config.js +32 -24
- package/dist/channels/control.js +60 -14
- package/dist/channels/conversations.js +60 -13
- package/dist/channels/handoff.js +90 -0
- package/dist/channels/lark-api.js +7 -0
- package/dist/channels/lark-outbound.js +19 -0
- package/dist/channels/lark-panel.js +41 -26
- package/dist/channels/lark-render.js +2 -1
- package/dist/channels/lark.js +35 -26
- package/dist/channels/lines.js +1 -1
- package/dist/channels/panel.js +324 -115
- package/dist/channels/receipts.js +25 -12
- package/dist/channels/routes.js +21 -7
- package/dist/channels/runtime.js +21 -7
- package/dist/channels/slack-api.js +19 -10
- package/dist/channels/slack-cli.js +503 -0
- package/dist/channels/slack-directory.js +2 -2
- package/dist/channels/slack-outbound.js +11 -1
- package/dist/channels/slack-panel.js +81 -40
- package/dist/channels/slack-render.js +3 -1
- package/dist/channels/slack-thread.js +41 -0
- package/dist/channels/slack-transcript.js +107 -0
- package/dist/channels/slack.js +38 -31
- package/dist/channels/types.js +1 -3
- package/dist/cli.js +166 -7
- package/dist/core/identity.js +61 -17
- package/dist/core/reply.js +19 -15
- package/dist/core/router.js +23 -3
- package/dist/db.js +33 -0
- package/dist/main.js +61 -41
- package/dist/paths.js +3 -0
- package/dist/secrets.js +2 -1
- package/dist/settings.js +3 -12
- package/dist/socket.js +99 -0
- package/dist/tasks/agent.js +5 -6
- package/dist/tasks/callbacks.js +2 -2
- package/dist/tasks/cli.js +225 -0
- package/dist/tasks/definitions.js +3 -3
- package/dist/tasks/execution.js +1 -3
- package/dist/tasks/groups.js +2 -5
- package/dist/tasks/messages.js +18 -158
- package/dist/tasks/operations.js +335 -0
- package/dist/tasks/routes.js +1 -11
- package/dist/tasks/runs.js +5 -20
- package/dist/tasks/service.js +18 -43
- package/dist/tasks/store.js +15 -15
- package/dist/tools.js +18 -3
- package/dist/vault.js +107 -0
- package/dist/web/auth.js +2 -1
- package/dist/web/public/assets/{activity-BSMeRcN2.js → activity-CrybM-E8.js} +2 -2
- package/dist/web/public/assets/activity-CrybM-E8.js.br +0 -0
- package/dist/web/public/assets/activity-CrybM-E8.js.gz +0 -0
- package/dist/web/public/assets/{boards-BCWQMZry.js → boards-Cw7_6J6L.js} +1 -1
- package/dist/web/public/assets/boards-Cw7_6J6L.js.br +0 -0
- package/dist/web/public/assets/boards-Cw7_6J6L.js.gz +0 -0
- package/dist/web/public/assets/{explorer-Cr4XTi4j.js → explorer-DV066dUD.js} +1 -1
- package/dist/web/public/assets/explorer-DV066dUD.js.br +0 -0
- package/dist/web/public/assets/explorer-DV066dUD.js.gz +0 -0
- package/dist/web/public/assets/index-BrNHu2qj.js +85 -0
- package/dist/web/public/assets/index-BrNHu2qj.js.br +0 -0
- package/dist/web/public/assets/index-BrNHu2qj.js.gz +0 -0
- package/dist/web/public/assets/index-DLszkDUV.css +2 -0
- package/dist/web/public/assets/index-DLszkDUV.css.br +0 -0
- package/dist/web/public/assets/index-DLszkDUV.css.gz +0 -0
- package/dist/web/public/assets/{runs-DdERzeac.js → runs-DDagTNaM.js} +1 -1
- package/dist/web/public/assets/runs-DDagTNaM.js.br +0 -0
- package/dist/web/public/assets/runs-DDagTNaM.js.gz +0 -0
- package/dist/web/public/assets/settings-BEdSeXpm.js +5 -0
- package/dist/web/public/assets/settings-BEdSeXpm.js.br +0 -0
- package/dist/web/public/assets/settings-BEdSeXpm.js.gz +0 -0
- package/dist/web/public/assets/{task-runs-C-dGUDsH.js → task-runs-BnMack9t.js} +1 -1
- package/dist/web/public/assets/task-runs-BnMack9t.js.br +0 -0
- package/dist/web/public/assets/task-runs-BnMack9t.js.gz +0 -0
- package/dist/web/public/assets/{tasks-CY30H1u1.js → tasks-BkW7YShZ.js} +1 -1
- package/dist/web/public/assets/tasks-BkW7YShZ.js.br +0 -0
- package/dist/web/public/assets/tasks-BkW7YShZ.js.gz +0 -0
- package/dist/web/public/index.html +2 -2
- package/dist/web/public/index.html.br +0 -0
- package/dist/web/public/index.html.gz +0 -0
- package/dist/web/push.js +4 -10
- package/dist/web/vault.js +68 -0
- package/dist/{extensions/web → websearch}/artifacts.js +2 -2
- package/dist/websearch/cli.js +73 -0
- package/dist/websearch/run.js +273 -0
- package/docs/deploy.md +34 -11
- package/package.json +2 -3
- package/skills/pier-help/SKILL.md +32 -16
- package/skills/pier-slack/SKILL.md +52 -71
- package/skills/pier-tasks/SKILL.md +60 -148
- package/skills/pier-vault/SKILL.md +36 -0
- package/skills/pier-web/SKILL.md +50 -0
- package/dist/channels/slack-tool.js +0 -416
- package/dist/channels/telegram-api.js +0 -86
- package/dist/channels/telegram-panel.js +0 -97
- package/dist/channels/telegram-render.js +0 -69
- package/dist/channels/telegram.js +0 -421
- package/dist/extensions/index.js +0 -10
- package/dist/extensions/web/index.js +0 -9
- package/dist/extensions/web/tools.js +0 -265
- package/dist/tasks/tool.js +0 -416
- package/dist/web/public/assets/activity-BSMeRcN2.js.br +0 -0
- package/dist/web/public/assets/activity-BSMeRcN2.js.gz +0 -0
- package/dist/web/public/assets/boards-BCWQMZry.js.br +0 -0
- package/dist/web/public/assets/boards-BCWQMZry.js.gz +0 -0
- package/dist/web/public/assets/explorer-Cr4XTi4j.js.br +0 -0
- package/dist/web/public/assets/explorer-Cr4XTi4j.js.gz +0 -0
- package/dist/web/public/assets/index-C7tA0Ufu.js +0 -85
- package/dist/web/public/assets/index-C7tA0Ufu.js.br +0 -0
- package/dist/web/public/assets/index-C7tA0Ufu.js.gz +0 -0
- package/dist/web/public/assets/index-DVIt5Gio.css +0 -2
- package/dist/web/public/assets/index-DVIt5Gio.css.br +0 -0
- package/dist/web/public/assets/index-DVIt5Gio.css.gz +0 -0
- package/dist/web/public/assets/runs-DdERzeac.js.br +0 -0
- package/dist/web/public/assets/runs-DdERzeac.js.gz +0 -0
- package/dist/web/public/assets/settings-CQDAHoMM.js +0 -5
- package/dist/web/public/assets/settings-CQDAHoMM.js.br +0 -0
- package/dist/web/public/assets/settings-CQDAHoMM.js.gz +0 -0
- package/dist/web/public/assets/task-runs-C-dGUDsH.js.br +0 -0
- package/dist/web/public/assets/task-runs-C-dGUDsH.js.gz +0 -0
- package/dist/web/public/assets/tasks-CY30H1u1.js.br +0 -0
- package/dist/web/public/assets/tasks-CY30H1u1.js.gz +0 -0
- /package/dist/{extensions/web → websearch}/anthropic.js +0 -0
- /package/dist/{extensions/web → websearch}/content.js +0 -0
- /package/dist/{extensions/web → websearch}/http.js +0 -0
- /package/dist/{extensions/web → websearch}/json.js +0 -0
- /package/dist/{extensions/web → websearch}/language.js +0 -0
- /package/dist/{extensions/web → websearch}/openai.js +0 -0
- /package/dist/{extensions/web → websearch}/provider.js +0 -0
|
@@ -1,421 +0,0 @@
|
|
|
1
|
-
// Telegram adapter: normalize inbound updates, render outbound turns.
|
|
2
|
-
// Telegram-specific: topic mode, where a message in a forum group's General
|
|
3
|
-
// opens a topic that becomes the session; 👀 receipts on every message that
|
|
4
|
-
// entered a turn; `/bind <code>` in a DM.
|
|
5
|
-
import { awaitsTurn, formatTurnMeta, isSilentReply, originLabel, quietLabel } from "../core/reply.js";
|
|
6
|
-
import { saveInboundAll } from "../core/inbox.js";
|
|
7
|
-
import { MAX_INBOUND_BYTES } from "../core/inbound-file.js";
|
|
8
|
-
import { sendAttachments, splitAttachments } from "./attach.js";
|
|
9
|
-
import { bindHint, bindResult, picked, STALE_OPTION, STOPPED } from "./lines.js";
|
|
10
|
-
import { logger } from "../log.js";
|
|
11
|
-
import { Chains } from "./chains.js";
|
|
12
|
-
import { parseCommand } from "./commands.js";
|
|
13
|
-
import { Gatekeeper } from "./gatekeeper.js";
|
|
14
|
-
import { ReceiptLedger, Receipts } from "./receipts.js";
|
|
15
|
-
import { TelegramPanel } from "./telegram-panel.js";
|
|
16
|
-
import { chunk, escapeHtml, keyboard, offeredLabel, toTelegramHtml } from "./telegram-render.js";
|
|
17
|
-
import { TelegramApi, } from "./telegram-api.js";
|
|
18
|
-
const WORKING = "👀";
|
|
19
|
-
const POLL_SECONDS = 30;
|
|
20
|
-
// The poll loop waits here without advancing the offset past what it accepted.
|
|
21
|
-
const MAX_ACTIVE_CHATS = 16;
|
|
22
|
-
// Generous: a real coding turn can be long.
|
|
23
|
-
const RECEIPT_STALE_MS = 30 * 60_000;
|
|
24
|
-
const DRAIN_TIMEOUT_MS = 5000;
|
|
25
|
-
const TOPIC_TITLE_MAX = 60;
|
|
26
|
-
/** `<chatId>/<topicId>` or `<chatId>`; the only definition of the format. */
|
|
27
|
-
const conversationId = (chatId, topicId) => topicId ? `${chatId}/${topicId}` : String(chatId);
|
|
28
|
-
export const parseConversation = (id) => {
|
|
29
|
-
const [chatId = "", topic] = id.split("/");
|
|
30
|
-
const topicId = topic ? Number(topic) : undefined;
|
|
31
|
-
return { chatId, topicId: Number.isSafeInteger(topicId) ? topicId : undefined };
|
|
32
|
-
};
|
|
33
|
-
/** Telegram numbers General as topic 1 and omits the id on plain groups. */
|
|
34
|
-
const inGeneral = (msg) => !msg.message_thread_id || msg.message_thread_id === 1;
|
|
35
|
-
function topicTitle(text) {
|
|
36
|
-
const line = (text.split("\n").find((l) => l.trim()) ?? "").trim();
|
|
37
|
-
if (!line)
|
|
38
|
-
return `Session ${new Date().toISOString().slice(5, 16).replace("T", " ")}`;
|
|
39
|
-
return line.length > TOPIC_TITLE_MAX ? `${line.slice(0, TOPIC_TITLE_MAX - 3).trimEnd()}...` : line;
|
|
40
|
-
}
|
|
41
|
-
export class TelegramChannel {
|
|
42
|
-
deps;
|
|
43
|
-
id = "telegram";
|
|
44
|
-
api;
|
|
45
|
-
log;
|
|
46
|
-
receipts;
|
|
47
|
-
chains;
|
|
48
|
-
gate;
|
|
49
|
-
/** Absent when no control was wired (tests). */
|
|
50
|
-
panel;
|
|
51
|
-
me;
|
|
52
|
-
offset;
|
|
53
|
-
running = false;
|
|
54
|
-
constructor(deps) {
|
|
55
|
-
this.deps = deps;
|
|
56
|
-
this.api = deps.client ?? new TelegramApi(deps.store.get("telegram").token);
|
|
57
|
-
this.log = deps.log ?? ((m) => logger("telegram").warn(m));
|
|
58
|
-
// No cap: the poll loop applies backpressure itself.
|
|
59
|
-
this.chains = new Chains(this.log);
|
|
60
|
-
this.gate = new Gatekeeper(deps.store, "telegram", this.log);
|
|
61
|
-
this.receipts = new Receipts(
|
|
62
|
-
// The ledger keeps message ids as opaque strings (a Slack ts is not a number).
|
|
63
|
-
{ setReaction: (chatId, messageId, emoji) => this.api.setReaction(chatId, Number(messageId), emoji) }, deps.receipts ?? new ReceiptLedger("telegram"), this.log, WORKING, RECEIPT_STALE_MS);
|
|
64
|
-
if (deps.control) {
|
|
65
|
-
this.panel = new TelegramPanel({
|
|
66
|
-
api: this.api,
|
|
67
|
-
control: deps.control,
|
|
68
|
-
store: deps.store,
|
|
69
|
-
log: this.log,
|
|
70
|
-
});
|
|
71
|
-
}
|
|
72
|
-
}
|
|
73
|
-
async start(onMessage) {
|
|
74
|
-
const me = await this.api.getMe();
|
|
75
|
-
this.me = { id: me.id, username: me.username ?? "" };
|
|
76
|
-
if (!this.me.username) {
|
|
77
|
-
// Every group with require-mention on goes silent; loud, not a debug line.
|
|
78
|
-
this.log("bot has no username: mention detection is disabled");
|
|
79
|
-
}
|
|
80
|
-
this.running = true;
|
|
81
|
-
void this.receipts.sweep(true);
|
|
82
|
-
void this.poll(onMessage);
|
|
83
|
-
}
|
|
84
|
-
async stop() {
|
|
85
|
-
this.running = false;
|
|
86
|
-
// reload() starts a replacement right after; two adapters handling one
|
|
87
|
-
// message would prompt the session twice. Bounded: a stuck handler must not
|
|
88
|
-
// hold the Console's save request open.
|
|
89
|
-
await this.chains.drain(DRAIN_TIMEOUT_MS);
|
|
90
|
-
}
|
|
91
|
-
// --- inbound ---------------------------------------------------------------
|
|
92
|
-
async poll(onMessage) {
|
|
93
|
-
while (this.running) {
|
|
94
|
-
try {
|
|
95
|
-
// Floor on an empty round trip: a proxy that answers instantly would
|
|
96
|
-
// otherwise turn the long poll into a hot loop.
|
|
97
|
-
const startedAt = Date.now();
|
|
98
|
-
const updates = await this.api.getUpdates(this.offset, POLL_SECONDS);
|
|
99
|
-
if (!this.running)
|
|
100
|
-
return;
|
|
101
|
-
void this.receipts.sweep();
|
|
102
|
-
if (!updates.length && Date.now() - startedAt < 1000) {
|
|
103
|
-
await new Promise((r) => setTimeout(r, 1000));
|
|
104
|
-
}
|
|
105
|
-
for (const update of updates) {
|
|
106
|
-
if (!this.running)
|
|
107
|
-
return;
|
|
108
|
-
while (this.chains.size >= MAX_ACTIVE_CHATS)
|
|
109
|
-
await this.chains.oldest();
|
|
110
|
-
this.offset = update.update_id + 1;
|
|
111
|
-
const chat = update.message?.chat.id ?? update.callback_query?.message?.chat.id;
|
|
112
|
-
if (chat === undefined)
|
|
113
|
-
continue; // malformed: no chat to answer in
|
|
114
|
-
this.chains.run(String(chat), async () => {
|
|
115
|
-
try {
|
|
116
|
-
if (update.callback_query)
|
|
117
|
-
await this.onCallback(update.callback_query, onMessage);
|
|
118
|
-
else if (update.message)
|
|
119
|
-
await this.onMessage(update.message, onMessage);
|
|
120
|
-
}
|
|
121
|
-
catch (err) {
|
|
122
|
-
this.log(`update ${update.update_id} dropped: ${String(err)}`);
|
|
123
|
-
}
|
|
124
|
-
});
|
|
125
|
-
}
|
|
126
|
-
}
|
|
127
|
-
catch (err) {
|
|
128
|
-
if (!this.running)
|
|
129
|
-
return;
|
|
130
|
-
this.log(`poll failed, retrying: ${String(err)}`);
|
|
131
|
-
await new Promise((r) => setTimeout(r, 3000));
|
|
132
|
-
}
|
|
133
|
-
}
|
|
134
|
-
}
|
|
135
|
-
async onMessage(msg, onMessage) {
|
|
136
|
-
if (!msg.from || msg.from.id === this.me?.id)
|
|
137
|
-
return; // own echo, or malformed
|
|
138
|
-
const raw = (msg.text ?? msg.caption ?? "").trim();
|
|
139
|
-
if (!raw && !msg.photo?.length && !msg.document)
|
|
140
|
-
return;
|
|
141
|
-
const chatId = String(msg.chat.id);
|
|
142
|
-
const isDm = msg.chat.type === "private";
|
|
143
|
-
const kind = isDm ? "dm" : msg.chat.is_forum ? "forum" : "group";
|
|
144
|
-
const name = msg.chat.title ?? [msg.from.first_name, msg.from.last_name].filter(Boolean).join(" ");
|
|
145
|
-
if (this.gate.mayDiscover({ isDm, userId: String(msg.from.id) })) {
|
|
146
|
-
this.deps.store.discoverChat("telegram", { id: chatId, name: name || chatId, kind });
|
|
147
|
-
}
|
|
148
|
-
const text = this.stripMention(raw);
|
|
149
|
-
const command = parseCommand(text);
|
|
150
|
-
const mine = !command?.target || command.target.toLowerCase() === this.me?.username.toLowerCase();
|
|
151
|
-
const bindRequest = mine && command?.name === "bind" && isDm;
|
|
152
|
-
const admitted = this.gate.admit("message", chatId, {
|
|
153
|
-
isDm,
|
|
154
|
-
addressed: this.addressed(raw, msg),
|
|
155
|
-
userId: String(msg.from.id),
|
|
156
|
-
bindRequest,
|
|
157
|
-
});
|
|
158
|
-
if (!admitted) {
|
|
159
|
-
if (isDm)
|
|
160
|
-
await this.hintBind(msg);
|
|
161
|
-
return;
|
|
162
|
-
}
|
|
163
|
-
if (bindRequest)
|
|
164
|
-
return this.bind(msg, command?.args ?? "");
|
|
165
|
-
if (mine && command?.name === "stop")
|
|
166
|
-
return this.abortTurn(msg);
|
|
167
|
-
const here = {
|
|
168
|
-
channelId: this.id,
|
|
169
|
-
conversationId: conversationId(chatId, msg.message_thread_id),
|
|
170
|
-
};
|
|
171
|
-
if (await this.panel?.consumeCwdReply(msg, here))
|
|
172
|
-
return;
|
|
173
|
-
// A bare `@bot` and `/settings` are the same request.
|
|
174
|
-
if (this.panel && mine && (command?.name === "settings" || (!text && !msg.photo?.length && !msg.document))) {
|
|
175
|
-
return this.panel.open(here, chatId, msg.message_thread_id);
|
|
176
|
-
}
|
|
177
|
-
// Downloading only past the gate: an unauthorized sender must not make the
|
|
178
|
-
// bot pull bytes on their behalf.
|
|
179
|
-
const markers = await this.saveAttachments(msg);
|
|
180
|
-
const topicId = await this.routeTopic(msg, text);
|
|
181
|
-
const key = { channelId: this.id, conversationId: conversationId(chatId, topicId) };
|
|
182
|
-
this.receipts.mark(key.conversationId, chatId, String(msg.message_id));
|
|
183
|
-
// Steer: a follow-up is the wrong default when the human is watching a 👀.
|
|
184
|
-
onMessage({
|
|
185
|
-
key,
|
|
186
|
-
senderId: String(msg.from.id),
|
|
187
|
-
sender: { id: String(msg.from.id), name: senderName(msg.from) },
|
|
188
|
-
text: [text, ...markers].filter(Boolean).join("\n"),
|
|
189
|
-
mode: "steer",
|
|
190
|
-
});
|
|
191
|
-
}
|
|
192
|
-
async onCallback(query, onMessage) {
|
|
193
|
-
// Unanswered, the tapped button keeps its spinner until Telegram gives up.
|
|
194
|
-
await this.api.answerCallbackQuery(query.id)
|
|
195
|
-
.catch((err) => this.log(`callback ack failed: ${String(err)}`));
|
|
196
|
-
const msg = query.message;
|
|
197
|
-
if (!msg || !query.data)
|
|
198
|
-
return;
|
|
199
|
-
const chatId = String(msg.chat.id);
|
|
200
|
-
const admitted = this.gate.admit("callback", chatId, {
|
|
201
|
-
isDm: msg.chat.type === "private",
|
|
202
|
-
addressed: true, // pressing the bot's own button is addressing it
|
|
203
|
-
userId: String(query.from.id),
|
|
204
|
-
});
|
|
205
|
-
if (!admitted)
|
|
206
|
-
return;
|
|
207
|
-
const key = {
|
|
208
|
-
channelId: this.id,
|
|
209
|
-
conversationId: conversationId(chatId, msg.message_thread_id),
|
|
210
|
-
};
|
|
211
|
-
if (await this.panel?.onCallback(query, key))
|
|
212
|
-
return;
|
|
213
|
-
const text = offeredLabel(msg, query.data);
|
|
214
|
-
if (text === undefined) {
|
|
215
|
-
// A callback query may be answered exactly once, and the ack above was
|
|
216
|
-
// it; a second answer is silently dropped.
|
|
217
|
-
await this.api.sendMessage({
|
|
218
|
-
chat_id: chatId,
|
|
219
|
-
message_thread_id: msg.message_thread_id,
|
|
220
|
-
text: STALE_OPTION,
|
|
221
|
-
}).catch((err) => this.log(`stale-option notice failed: ${String(err)}`));
|
|
222
|
-
return;
|
|
223
|
-
}
|
|
224
|
-
await this.api.clearKeyboard(chatId, msg.message_id)
|
|
225
|
-
.catch((err) => this.log(`retiring options failed: ${String(err)}`));
|
|
226
|
-
// A bot cannot post as the user, so the pick is echoed: otherwise the chat
|
|
227
|
-
// shows an answer to a request nobody can see, with nothing to carry the eyes.
|
|
228
|
-
const echo = await this.api.sendMessage({
|
|
229
|
-
chat_id: chatId,
|
|
230
|
-
message_thread_id: msg.message_thread_id,
|
|
231
|
-
text: picked(escapeHtml(text)),
|
|
232
|
-
parse_mode: "HTML",
|
|
233
|
-
}).catch((err) => {
|
|
234
|
-
this.log(`option echo failed: ${String(err)}`);
|
|
235
|
-
return undefined;
|
|
236
|
-
});
|
|
237
|
-
if (echo)
|
|
238
|
-
this.receipts.mark(key.conversationId, chatId, String(echo.message_id));
|
|
239
|
-
onMessage({ key, senderId: String(query.from.id), text, mode: "steer" });
|
|
240
|
-
}
|
|
241
|
-
/** The abort ends the turn, which reaches send() and clears the receipts. */
|
|
242
|
-
async abortTurn(msg) {
|
|
243
|
-
const key = {
|
|
244
|
-
channelId: this.id,
|
|
245
|
-
conversationId: conversationId(msg.chat.id, msg.message_thread_id),
|
|
246
|
-
};
|
|
247
|
-
await this.deps.control?.abort(key);
|
|
248
|
-
await this.api.sendMessage({
|
|
249
|
-
chat_id: msg.chat.id,
|
|
250
|
-
message_thread_id: msg.message_thread_id,
|
|
251
|
-
text: STOPPED,
|
|
252
|
-
});
|
|
253
|
-
}
|
|
254
|
-
/** Groups stay silent, but a DM that swallows every message looks broken
|
|
255
|
-
* rather than locked. */
|
|
256
|
-
async hintBind(msg) {
|
|
257
|
-
if (!this.gate.mayHint(String(msg.from?.id ?? "")))
|
|
258
|
-
return;
|
|
259
|
-
await this.api.sendMessage({
|
|
260
|
-
chat_id: msg.chat.id,
|
|
261
|
-
text: bindHint("/bind <code>"),
|
|
262
|
-
}).catch((err) => this.log(`bind hint failed: ${String(err)}`));
|
|
263
|
-
}
|
|
264
|
-
async bind(msg, code) {
|
|
265
|
-
const user = msg.from;
|
|
266
|
-
const name = senderName(user);
|
|
267
|
-
const outcome = this.deps.store.redeemBindCode("telegram", code, { id: String(user.id), name });
|
|
268
|
-
await this.api.sendMessage({
|
|
269
|
-
chat_id: msg.chat.id,
|
|
270
|
-
text: bindResult(outcome, name),
|
|
271
|
-
});
|
|
272
|
-
}
|
|
273
|
-
/** A reply or a command stays put; failure falls back to the current thread. */
|
|
274
|
-
async routeTopic(msg, text) {
|
|
275
|
-
// Every reason to decline is logged: six invisible conditions are
|
|
276
|
-
// indistinguishable from a bug.
|
|
277
|
-
const decline = !this.deps.store.policy("telegram", String(msg.chat.id)).topicMode
|
|
278
|
-
? "topic mode off for this chat"
|
|
279
|
-
: msg.chat.type !== "supergroup"
|
|
280
|
-
? `chat is a ${msg.chat.type}, not a supergroup`
|
|
281
|
-
: !msg.chat.is_forum
|
|
282
|
-
? "group has Topics disabled in Telegram"
|
|
283
|
-
: !inGeneral(msg)
|
|
284
|
-
? `already inside topic ${msg.message_thread_id}`
|
|
285
|
-
: msg.reply_to_message
|
|
286
|
-
? "message is a reply, so it continues an existing thread"
|
|
287
|
-
: text.startsWith("/")
|
|
288
|
-
? "message is a command"
|
|
289
|
-
: "";
|
|
290
|
-
if (decline) {
|
|
291
|
-
this.log(`no new topic in chat ${msg.chat.id}: ${decline}`);
|
|
292
|
-
return msg.message_thread_id;
|
|
293
|
-
}
|
|
294
|
-
const title = topicTitle(text);
|
|
295
|
-
try {
|
|
296
|
-
const topic = await this.api.createForumTopic(msg.chat.id, title);
|
|
297
|
-
await this.api.sendMessage({
|
|
298
|
-
chat_id: msg.chat.id,
|
|
299
|
-
text: `→ <a href="${topicLink(msg.chat, topic.message_thread_id)}">${escapeHtml(title)}</a>`,
|
|
300
|
-
parse_mode: "HTML",
|
|
301
|
-
message_thread_id: msg.message_thread_id,
|
|
302
|
-
reply_to_message_id: msg.message_id,
|
|
303
|
-
// Without the pointer, General shows a question whose answer went elsewhere.
|
|
304
|
-
}).catch((err) => this.log(`topic pointer failed: ${String(err)}`));
|
|
305
|
-
return topic.message_thread_id;
|
|
306
|
-
}
|
|
307
|
-
catch (err) {
|
|
308
|
-
this.log(`topic creation failed, staying in General: ${String(err)}`);
|
|
309
|
-
return msg.message_thread_id;
|
|
310
|
-
}
|
|
311
|
-
}
|
|
312
|
-
saveAttachments(msg) {
|
|
313
|
-
// A photo is a size ladder; the last entry is the largest.
|
|
314
|
-
const photo = msg.photo?.at(-1);
|
|
315
|
-
const doc = msg.document;
|
|
316
|
-
return saveInboundAll(this.id, [
|
|
317
|
-
...(photo ? [{
|
|
318
|
-
label: "photo",
|
|
319
|
-
mimeType: "image/jpeg",
|
|
320
|
-
size: photo.file_size,
|
|
321
|
-
// A filename comes only from getFile, so the fetch's wins.
|
|
322
|
-
fetch: async () => this.api.downloadFile(photo.file_id, MAX_INBOUND_BYTES),
|
|
323
|
-
}] : []),
|
|
324
|
-
...(doc ? [{
|
|
325
|
-
label: doc.file_name ?? "file",
|
|
326
|
-
name: doc.file_name,
|
|
327
|
-
mimeType: doc.mime_type ?? "application/octet-stream",
|
|
328
|
-
size: doc.file_size,
|
|
329
|
-
fetch: async () => this.api.downloadFile(doc.file_id, MAX_INBOUND_BYTES),
|
|
330
|
-
}] : []),
|
|
331
|
-
], this.log);
|
|
332
|
-
}
|
|
333
|
-
// --- addressing ------------------------------------------------------------
|
|
334
|
-
addressed(text, msg) {
|
|
335
|
-
if (msg.reply_to_message?.from?.id === this.me?.id)
|
|
336
|
-
return true;
|
|
337
|
-
if (text.startsWith("/")) {
|
|
338
|
-
const target = /^\/\S+?@(\S+)/.exec(text)?.[1];
|
|
339
|
-
return !target || target.toLowerCase() === this.me?.username.toLowerCase();
|
|
340
|
-
}
|
|
341
|
-
const handle = `@${this.me?.username.toLowerCase()}`;
|
|
342
|
-
return !!this.me?.username && text.toLowerCase().includes(handle);
|
|
343
|
-
}
|
|
344
|
-
stripMention(text) {
|
|
345
|
-
const handle = `@${this.me?.username ?? ""}`;
|
|
346
|
-
if (!this.me?.username)
|
|
347
|
-
return text;
|
|
348
|
-
return text.toLowerCase().startsWith(handle.toLowerCase())
|
|
349
|
-
? text.slice(handle.length).replace(/^[\s,:-]+/, "")
|
|
350
|
-
: text;
|
|
351
|
-
}
|
|
352
|
-
// --- outbound --------------------------------------------------------------
|
|
353
|
-
async send(conversation, reply) {
|
|
354
|
-
const { chatId, topicId } = parseConversation(conversation);
|
|
355
|
-
// A local file link is dead in Telegram: the bytes are uploaded instead.
|
|
356
|
-
const { text: spoken, paths } = splitAttachments(reply.text);
|
|
357
|
-
const text = spoken.trim();
|
|
358
|
-
// An empty turn still posts its footer and says which kind of nothing (§5).
|
|
359
|
-
const buttons = keyboard(reply.suggestions);
|
|
360
|
-
const quiet = isSilentReply(reply)
|
|
361
|
-
? `<i>${quietLabel(reply.silence && escapeHtml(reply.silence))}</i>`
|
|
362
|
-
: "";
|
|
363
|
-
// A keyboard cannot ride a message that was never sent.
|
|
364
|
-
const body = ((text ? toTelegramHtml(text) : quiet) + turnFooter(reply.meta)) ||
|
|
365
|
-
(buttons ? "…" : "");
|
|
366
|
-
// A 👀 left up by a failed send looks like work until the stale sweep.
|
|
367
|
-
await this.receipts.settleAfter(conversation, async () => {
|
|
368
|
-
if (body.trim()) {
|
|
369
|
-
const parts = chunk(body);
|
|
370
|
-
for (const [i, part] of parts.entries()) {
|
|
371
|
-
await this.api.sendMessage({
|
|
372
|
-
chat_id: chatId,
|
|
373
|
-
message_thread_id: topicId,
|
|
374
|
-
text: part,
|
|
375
|
-
parse_mode: "HTML",
|
|
376
|
-
reply_markup: i === parts.length - 1 ? buttons : undefined,
|
|
377
|
-
});
|
|
378
|
-
}
|
|
379
|
-
}
|
|
380
|
-
const lost = await sendAttachments(paths, (file) => this.api.sendFile({ chat_id: chatId, message_thread_id: topicId, file }), this.log);
|
|
381
|
-
if (lost) {
|
|
382
|
-
await this.api.sendMessage({
|
|
383
|
-
chat_id: chatId,
|
|
384
|
-
message_thread_id: topicId,
|
|
385
|
-
text: escapeHtml(lost),
|
|
386
|
-
parse_mode: "HTML",
|
|
387
|
-
});
|
|
388
|
-
}
|
|
389
|
-
}, reply.meta);
|
|
390
|
-
}
|
|
391
|
-
/** No footer: the turn this input triggers has not ended. The 👀 goes on the
|
|
392
|
-
* note itself, since that turn has no message of the user's to carry them. */
|
|
393
|
-
async notify(conversation, note) {
|
|
394
|
-
const { chatId, topicId } = parseConversation(conversation);
|
|
395
|
-
const label = originLabel(note.origin);
|
|
396
|
-
let posted;
|
|
397
|
-
for (const part of chunk(`<i>${label}</i>\n<blockquote>${toTelegramHtml(note.text)}</blockquote>`)) {
|
|
398
|
-
posted = await this.api.sendMessage({
|
|
399
|
-
chat_id: chatId,
|
|
400
|
-
message_thread_id: topicId,
|
|
401
|
-
text: part,
|
|
402
|
-
parse_mode: "HTML",
|
|
403
|
-
});
|
|
404
|
-
}
|
|
405
|
-
if (posted && awaitsTurn(note.origin)) {
|
|
406
|
-
this.receipts.mark(conversation, chatId, String(posted.message_id));
|
|
407
|
-
}
|
|
408
|
-
}
|
|
409
|
-
}
|
|
410
|
-
const senderName = (user) => [user.first_name, user.last_name].filter(Boolean).join(" ") || user.username || String(user.id);
|
|
411
|
-
/** A private supergroup links as `/c/<internal id>`: the chat id with its
|
|
412
|
-
* `-100` prefix removed. */
|
|
413
|
-
function topicLink(chat, topicId) {
|
|
414
|
-
if (chat.username)
|
|
415
|
-
return `https://t.me/${chat.username}/${topicId}`;
|
|
416
|
-
const internal = String(chat.id).replace(/^-100(?=\d)/, "").replace(/^-/, "");
|
|
417
|
-
return `https://t.me/c/${internal}/${topicId}`;
|
|
418
|
-
}
|
|
419
|
-
/** One newline, not a blank line: Telegram has no muted text, so tucking it
|
|
420
|
-
* against the reply is the only way it reads as a footnote. */
|
|
421
|
-
const turnFooter = (meta) => meta ? `\n<i>${formatTurnMeta(meta)}</i>` : "";
|
package/dist/extensions/index.js
DELETED
|
@@ -1,10 +0,0 @@
|
|
|
1
|
-
// The extensions Pier ships with. Bundled rather than dropped in
|
|
2
|
-
// <agentDir>/extensions because a copy on disk has an owner problem: an update
|
|
3
|
-
// either clobbers the user's edits or skips them forever.
|
|
4
|
-
import web from "./web/index.js";
|
|
5
|
-
export const BUNDLED = [
|
|
6
|
-
{ name: "web", factory: web },
|
|
7
|
-
];
|
|
8
|
-
/** The enabled ones as Pi inline extensions; unknown names are not ours. */
|
|
9
|
-
export const inlineExtensions = (enabled) => BUNDLED.filter((ext) => enabled.includes(ext.name))
|
|
10
|
-
.map(({ name, factory }) => ({ name, factory }));
|
|
@@ -1,9 +0,0 @@
|
|
|
1
|
-
// Two tools — web_search and web_fetch — served by the provider's own hosted
|
|
2
|
-
// web stack, so an agent reaches the public web with no extra key, service or
|
|
3
|
-
// dependency. Registered as an inline extension by agent/pi.ts when the
|
|
4
|
-
// Console has it switched on.
|
|
5
|
-
import { webFetch, webSearch } from "./tools.js";
|
|
6
|
-
export default function web(pi) {
|
|
7
|
-
pi.registerTool(webSearch);
|
|
8
|
-
pi.registerTool(webFetch);
|
|
9
|
-
}
|