talon-agent 3.20.0 → 3.22.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/package.json +4 -4
- package/prompts/base.md +1 -7
- package/prompts/identity.md +7 -5
- package/src/bootstrap.ts +8 -0
- package/src/core/background/cron.ts +16 -0
- package/src/core/background/job-oneshot.ts +79 -19
- package/src/core/tools/index.ts +2 -0
- package/src/core/tools/messaging.ts +154 -8
- package/src/core/tools/moderation.ts +131 -0
- package/src/core/tools/types.ts +1 -0
- package/src/frontend/telegram/actions/index.ts +2 -0
- package/src/frontend/telegram/actions/media.ts +143 -31
- package/src/frontend/telegram/actions/messaging.ts +78 -13
- package/src/frontend/telegram/actions/moderation.ts +255 -0
- package/src/frontend/telegram/actions/shared.ts +36 -0
- package/src/frontend/telegram/handlers/delivery.ts +6 -1
- package/src/frontend/telegram/index.ts +16 -8
- package/src/frontend/telegram/join-requests.ts +53 -0
- package/src/frontend/telegram/middleware.ts +25 -0
- package/src/frontend/telegram/topics.ts +63 -0
- package/src/frontend/telegram/userbot.ts +11 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "talon-agent",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.22.0",
|
|
4
4
|
"description": "Multi-frontend AI agent with full tool access, streaming, cron jobs, and plugin system",
|
|
5
5
|
"author": "Dylan Neve",
|
|
6
6
|
"license": "MIT",
|
|
@@ -102,7 +102,7 @@
|
|
|
102
102
|
"@grammyjs/transformer-throttler": "^1.2.1",
|
|
103
103
|
"@kilocode/sdk": "^7.2.22",
|
|
104
104
|
"@modelcontextprotocol/sdk": "^1.29.0",
|
|
105
|
-
"@openai/agents": "^0.
|
|
105
|
+
"@openai/agents": "^0.16.0",
|
|
106
106
|
"@openai/codex-sdk": "^0.147.0",
|
|
107
107
|
"@opencode-ai/sdk": "^1.17.4",
|
|
108
108
|
"@playwright/mcp": "0.0.79",
|
|
@@ -113,11 +113,11 @@
|
|
|
113
113
|
"cross-spawn": "^7.0.6",
|
|
114
114
|
"discord.js": "^14.16.3",
|
|
115
115
|
"file-type": "^22.0.1",
|
|
116
|
-
"grammy": "^1.
|
|
116
|
+
"grammy": "^1.45.1",
|
|
117
117
|
"liquidjs": "^10.27.0",
|
|
118
118
|
"marked": "^18.0.0",
|
|
119
119
|
"mem0ai": "^3.0.13",
|
|
120
|
-
"openai": "^
|
|
120
|
+
"openai": "^7.5.0",
|
|
121
121
|
"p-retry": "^8.0.0",
|
|
122
122
|
"picocolors": "^1.1.1",
|
|
123
123
|
"pino": "^10.3.1",
|
package/prompts/base.md
CHANGED
|
@@ -1,12 +1,6 @@
|
|
|
1
1
|
Be concise and direct. Lead with the substance, keep it as short as the moment needs, and stop when it's said.
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
- Answer first; context and caveats after, and only when they change something for the reader.
|
|
6
|
-
- Let depth follow the question: a quick ask gets a line or two, a real problem gets real work. When unsure, start small — people ask for more when they want it.
|
|
7
|
-
- Chat is not a document. Plain sentences usually beat headings, bullet cascades, and closing summaries; reach for structure only when it genuinely clarifies.
|
|
8
|
-
- Don't narrate your process or pad with filler — do the thing, then share what matters.
|
|
9
|
-
- A short clarifying question beats a long answer to the wrong question, but only when the ambiguity is real.
|
|
3
|
+
How you talk — voice, stances, what never to say — is the Identity section above. It applies on every backend and isn't repeated here.
|
|
10
4
|
|
|
11
5
|
## Tools
|
|
12
6
|
|
package/prompts/identity.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
## Who you are
|
|
2
2
|
|
|
3
|
-
You're a Talon agent — a peer with tools, not a service desk. The model and tools available to you depend on the active backend; only the tools listed below this prompt actually exist for this run. Tools for talking to your current platform (send, react, and the rest) are always provided by the frontend.
|
|
3
|
+
You're a Talon agent — a peer with tools, not a service desk. People talk to you all day; be someone worth talking to. The model and tools available to you depend on the active backend; only the tools listed below this prompt actually exist for this run. Tools for talking to your current platform (send, react, and the rest) are always provided by the frontend.
|
|
4
4
|
|
|
5
5
|
## Voice
|
|
6
6
|
|
|
@@ -10,13 +10,15 @@ Length follows the question, not habit: a quick ask gets a line or two, a real p
|
|
|
10
10
|
|
|
11
11
|
Have opinions and give reasons. "I'd use X, because Y" beats five options with no recommendation.
|
|
12
12
|
|
|
13
|
-
|
|
13
|
+
Engage with what was actually said, not the generic shape of it. The detail someone mentions in passing is often the interesting part; picking it up is the difference between a conversation and a ticket.
|
|
14
14
|
|
|
15
|
-
|
|
15
|
+
Match the room. Casual chat gets casual replies, technical questions get precise answers, and a tense thread doesn't need you adding heat. Humour, emoji, reactions, stickers — wherever the platform has them, use them the way a person would: as seasoning, not the meal.
|
|
16
|
+
|
|
17
|
+
Sound like yourself. Plain words, contractions, the occasional aside. A reply that reads like a person thinking beats one that reads like a product performing.
|
|
16
18
|
|
|
17
19
|
## Stances
|
|
18
20
|
|
|
19
|
-
|
|
21
|
+
Voice shows up in the awkward moments. Take these positions.
|
|
20
22
|
|
|
21
23
|
**Their plan is bad.** Say what's wrong in a sentence or two, then do the work as asked. Don't refuse to engage, don't lecture, and don't quietly do it a different way instead.
|
|
22
24
|
|
|
@@ -30,7 +32,7 @@ Situations are what define a voice. Take these positions.
|
|
|
30
32
|
|
|
31
33
|
**The request is ambiguous.** Make the call a careful colleague would make, and say which call you made. Ask only when different readings would mean materially different work.
|
|
32
34
|
|
|
33
|
-
**You have nothing to add.** Then don't
|
|
35
|
+
**You have nothing to add.** Then don't. "ok", "thanks", "lol" want a reaction or silence, not a reply. In groups you're a participant, not a host — don't answer for other people, and let conversations that aren't about you flow past.
|
|
34
36
|
|
|
35
37
|
## Never
|
|
36
38
|
|
package/src/bootstrap.ts
CHANGED
|
@@ -415,6 +415,14 @@ export async function initBackendAndDispatcher(
|
|
|
415
415
|
);
|
|
416
416
|
return { model, backendId: beId };
|
|
417
417
|
},
|
|
418
|
+
// ...and when that ambient backend turns out to be one that can't host an
|
|
419
|
+
// isolated run (e.g. the chat was switched to a provider with no background
|
|
420
|
+
// capability), the job reruns on the heartbeat role backend instead of
|
|
421
|
+
// being skipped.
|
|
422
|
+
resolveJobFallback: () => ({
|
|
423
|
+
backendId: config.heartbeatBackend ?? config.backend,
|
|
424
|
+
model: config.heartbeatModel ?? config.model ?? null,
|
|
425
|
+
}),
|
|
418
426
|
});
|
|
419
427
|
initTriggers({ execute: dispatcherExecute });
|
|
420
428
|
resumeTriggersAfterRestart().catch((err) =>
|
|
@@ -62,6 +62,13 @@ type CronDeps = {
|
|
|
62
62
|
resolveChatModel: (
|
|
63
63
|
chatId: string,
|
|
64
64
|
) => Promise<{ model: string | null; backendId: string }>;
|
|
65
|
+
/**
|
|
66
|
+
* Resolve the deployment's background-capable role backend (heartbeat, else
|
|
67
|
+
* the configured default). Used as a fallback for query jobs that inherited
|
|
68
|
+
* the chat's ambient backend and found it can't run isolated jobs — a
|
|
69
|
+
* `/model` switch in the chat shouldn't silently disable the schedule.
|
|
70
|
+
*/
|
|
71
|
+
resolveJobFallback?: () => { model: string | null; backendId: string };
|
|
65
72
|
};
|
|
66
73
|
|
|
67
74
|
let deps: CronDeps | null = null;
|
|
@@ -473,6 +480,10 @@ export async function executeJob(job: CronJob): Promise<ExecuteJobResult> {
|
|
|
473
480
|
// chat's backend + active model.
|
|
474
481
|
let backendId: string;
|
|
475
482
|
let model: string | null;
|
|
483
|
+
// A job that pinned its own provider is honoured as written — no fallback.
|
|
484
|
+
// One that inherited the chat's ambient backend gets a safety net, because
|
|
485
|
+
// that backend can change under it at any time (`/model`, a rebind).
|
|
486
|
+
let fallback: { backendId: string; model: string } | undefined;
|
|
476
487
|
if (job.provider) {
|
|
477
488
|
backendId = job.provider;
|
|
478
489
|
model = job.model ?? null;
|
|
@@ -480,6 +491,10 @@ export async function executeJob(job: CronJob): Promise<ExecuteJobResult> {
|
|
|
480
491
|
const chat = await deps.resolveChatModel(job.chatId);
|
|
481
492
|
backendId = chat.backendId;
|
|
482
493
|
model = job.model ?? chat.model;
|
|
494
|
+
const candidate = deps.resolveJobFallback?.();
|
|
495
|
+
if (candidate?.model) {
|
|
496
|
+
fallback = { backendId: candidate.backendId, model: candidate.model };
|
|
497
|
+
}
|
|
483
498
|
}
|
|
484
499
|
if (!model) {
|
|
485
500
|
throw new Error(
|
|
@@ -502,6 +517,7 @@ export async function executeJob(job: CronJob): Promise<ExecuteJobResult> {
|
|
|
502
517
|
label: job.name,
|
|
503
518
|
kind: "cron",
|
|
504
519
|
timeoutMs: CRON_JOB_TIMEOUT_MS,
|
|
520
|
+
...(fallback ? { fallback } : {}),
|
|
505
521
|
});
|
|
506
522
|
if (result.status === "skipped") {
|
|
507
523
|
await deps.sendMessage(
|
|
@@ -51,6 +51,19 @@ export interface JobOneShotParams {
|
|
|
51
51
|
readonly kind: JobKind;
|
|
52
52
|
/** Optional override of the hard timeout. */
|
|
53
53
|
readonly timeoutMs?: number;
|
|
54
|
+
/**
|
|
55
|
+
* Backend to retry on when the primary one can't host an isolated run.
|
|
56
|
+
*
|
|
57
|
+
* A cron `query` job with no provider override inherits the chat's *ambient*
|
|
58
|
+
* backend, which any `/model` switch can change out from under it — including
|
|
59
|
+
* to a provider with no background capability (or one where the chat's model
|
|
60
|
+
* isn't selectable). That has nothing to do with the job, so rather than skip
|
|
61
|
+
* it, fall back to the deployment's background-capable role backend
|
|
62
|
+
* (heartbeat, else the configured default). Omitted for jobs that pinned
|
|
63
|
+
* their own provider: an explicit choice is honoured or skipped, never
|
|
64
|
+
* silently rerouted.
|
|
65
|
+
*/
|
|
66
|
+
readonly fallback?: { readonly backendId: string; readonly model: string };
|
|
54
67
|
}
|
|
55
68
|
|
|
56
69
|
export type JobOneShotResult =
|
|
@@ -75,7 +88,17 @@ async function openJobLog(
|
|
|
75
88
|
return appendLog;
|
|
76
89
|
}
|
|
77
90
|
|
|
78
|
-
|
|
91
|
+
// The warning is deferred to runJobOneShot: an attempt that fails here may
|
|
92
|
+
// still succeed on the fallback backend, and a run that ultimately ran must
|
|
93
|
+
// not leave a "skipped" warning in the logs.
|
|
94
|
+
function skipJob(reason: string): JobOneShotResult {
|
|
95
|
+
return { status: "skipped", reason };
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function reportSkip(
|
|
99
|
+
params: JobOneShotParams,
|
|
100
|
+
reason: string,
|
|
101
|
+
): JobOneShotResult {
|
|
79
102
|
logWarn(
|
|
80
103
|
params.kind === "cron" ? "cron" : "triggers",
|
|
81
104
|
`isolated job "${params.label}" skipped: ${reason}`,
|
|
@@ -84,19 +107,20 @@ function skipJob(params: JobOneShotParams, reason: string): JobOneShotResult {
|
|
|
84
107
|
}
|
|
85
108
|
|
|
86
109
|
/**
|
|
87
|
-
*
|
|
88
|
-
*
|
|
110
|
+
* One attempt on one backend: acquire it, check it can actually host an
|
|
111
|
+
* isolated run, execute, and always release it afterwards.
|
|
89
112
|
*/
|
|
90
|
-
|
|
113
|
+
async function attemptJobOneShot(
|
|
91
114
|
params: JobOneShotParams,
|
|
115
|
+
backendId: string,
|
|
116
|
+
model: string,
|
|
92
117
|
): Promise<JobOneShotResult> {
|
|
93
118
|
let acquired: Awaited<ReturnType<typeof acquireBackendInstance>>;
|
|
94
119
|
try {
|
|
95
|
-
acquired = await acquireBackendInstance(
|
|
120
|
+
acquired = await acquireBackendInstance(backendId);
|
|
96
121
|
} catch (err) {
|
|
97
122
|
return skipJob(
|
|
98
|
-
|
|
99
|
-
`provider "${params.backendId}" is unavailable: ${err instanceof Error ? err.message : String(err)}`,
|
|
123
|
+
`provider "${backendId}" is unavailable: ${err instanceof Error ? err.message : String(err)}`,
|
|
100
124
|
);
|
|
101
125
|
}
|
|
102
126
|
|
|
@@ -105,32 +129,29 @@ export async function runJobOneShot(
|
|
|
105
129
|
const background = backend.background;
|
|
106
130
|
if (!background) {
|
|
107
131
|
return skipJob(
|
|
108
|
-
|
|
109
|
-
`provider "${params.backendId}" can't run isolated jobs (no background capability).`,
|
|
132
|
+
`provider "${backendId}" can't run isolated jobs (no background capability).`,
|
|
110
133
|
);
|
|
111
134
|
}
|
|
112
135
|
|
|
113
136
|
let modelValid = false;
|
|
114
137
|
try {
|
|
115
|
-
modelValid = await isModelValidForBackend(backend,
|
|
138
|
+
modelValid = await isModelValidForBackend(backend, model);
|
|
116
139
|
} catch (err) {
|
|
117
140
|
return skipJob(
|
|
118
|
-
|
|
119
|
-
`could not validate model "${params.model}" on provider "${params.backendId}": ${err instanceof Error ? err.message : String(err)}`,
|
|
141
|
+
`could not validate model "${model}" on provider "${backendId}": ${err instanceof Error ? err.message : String(err)}`,
|
|
120
142
|
);
|
|
121
143
|
}
|
|
122
144
|
if (!modelValid) {
|
|
123
145
|
return skipJob(
|
|
124
|
-
|
|
125
|
-
`model "${params.model}" is not selectable on provider "${params.backendId}".`,
|
|
146
|
+
`model "${model}" is not selectable on provider "${backendId}".`,
|
|
126
147
|
);
|
|
127
148
|
}
|
|
128
149
|
|
|
129
150
|
const appendLog = await openJobLog(
|
|
130
151
|
params.kind,
|
|
131
152
|
params.label,
|
|
132
|
-
|
|
133
|
-
|
|
153
|
+
backendId,
|
|
154
|
+
model,
|
|
134
155
|
);
|
|
135
156
|
|
|
136
157
|
const abortController = new AbortController();
|
|
@@ -140,7 +161,7 @@ export async function runJobOneShot(
|
|
|
140
161
|
chatId: params.chatId,
|
|
141
162
|
abort: () => abortController.abort(),
|
|
142
163
|
});
|
|
143
|
-
task.bind({ model
|
|
164
|
+
task.bind({ model, backendId });
|
|
144
165
|
|
|
145
166
|
const oneShot: OneShotAgentParams = {
|
|
146
167
|
prompt: params.payload,
|
|
@@ -151,7 +172,7 @@ export async function runJobOneShot(
|
|
|
151
172
|
),
|
|
152
173
|
contextLabel: JOB_CONTEXT_LABEL,
|
|
153
174
|
workspace: dirs.workspace,
|
|
154
|
-
model
|
|
175
|
+
model,
|
|
155
176
|
abortController,
|
|
156
177
|
appendLog,
|
|
157
178
|
};
|
|
@@ -172,10 +193,49 @@ export async function runJobOneShot(
|
|
|
172
193
|
}
|
|
173
194
|
log(
|
|
174
195
|
params.kind === "cron" ? "cron" : "triggers",
|
|
175
|
-
`isolated job "${params.label}" ran on ${
|
|
196
|
+
`isolated job "${params.label}" ran on ${backendId}/${model}`,
|
|
176
197
|
);
|
|
177
198
|
return { status: "ran" };
|
|
178
199
|
} finally {
|
|
179
200
|
await release();
|
|
180
201
|
}
|
|
181
202
|
}
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* Run a trigger/cron wake-up as an isolated one-shot.
|
|
206
|
+
*
|
|
207
|
+
* Tries the requested backend first. When that backend can't host the run at
|
|
208
|
+
* all — no background capability, unavailable, or the inherited model isn't
|
|
209
|
+
* selectable on it — and a distinct {@link JobOneShotParams.fallback} was
|
|
210
|
+
* supplied, the job is retried there instead of being skipped. A job is only
|
|
211
|
+
* reported as skipped once every candidate has been ruled out.
|
|
212
|
+
*/
|
|
213
|
+
export async function runJobOneShot(
|
|
214
|
+
params: JobOneShotParams,
|
|
215
|
+
): Promise<JobOneShotResult> {
|
|
216
|
+
const first = await attemptJobOneShot(params, params.backendId, params.model);
|
|
217
|
+
if (first.status === "ran") return first;
|
|
218
|
+
|
|
219
|
+
const { fallback } = params;
|
|
220
|
+
if (
|
|
221
|
+
!fallback ||
|
|
222
|
+
(fallback.backendId === params.backendId && fallback.model === params.model)
|
|
223
|
+
) {
|
|
224
|
+
return reportSkip(params, first.reason);
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
log(
|
|
228
|
+
params.kind === "cron" ? "cron" : "triggers",
|
|
229
|
+
`isolated job "${params.label}": ${params.backendId}/${params.model} can't host it (${first.reason}) — retrying on ${fallback.backendId}/${fallback.model}`,
|
|
230
|
+
);
|
|
231
|
+
const second = await attemptJobOneShot(
|
|
232
|
+
params,
|
|
233
|
+
fallback.backendId,
|
|
234
|
+
fallback.model,
|
|
235
|
+
);
|
|
236
|
+
if (second.status === "ran") return second;
|
|
237
|
+
return reportSkip(
|
|
238
|
+
params,
|
|
239
|
+
`${first.reason} Fallback ${fallback.backendId}/${fallback.model} also unusable: ${second.reason}`,
|
|
240
|
+
);
|
|
241
|
+
}
|
package/src/core/tools/index.ts
CHANGED
|
@@ -22,6 +22,7 @@ import { webTools } from "./web.js";
|
|
|
22
22
|
import { adminTools } from "./admin.js";
|
|
23
23
|
import { modelTools } from "./models.js";
|
|
24
24
|
import { meshTools } from "./mesh.js";
|
|
25
|
+
import { moderationTools } from "./moderation.js";
|
|
25
26
|
import { nativeTools } from "./native.js";
|
|
26
27
|
|
|
27
28
|
/** All built-in tool definitions. */
|
|
@@ -41,6 +42,7 @@ export const ALL_TOOLS: readonly ToolDefinition[] = [
|
|
|
41
42
|
...adminTools,
|
|
42
43
|
...modelTools,
|
|
43
44
|
...meshTools,
|
|
45
|
+
...moderationTools,
|
|
44
46
|
];
|
|
45
47
|
|
|
46
48
|
/**
|
|
@@ -135,7 +135,10 @@ Examples:
|
|
|
135
135
|
Dice: send(type="dice")
|
|
136
136
|
Location: send(type="location", latitude=37.7749, longitude=-122.4194)
|
|
137
137
|
Sticker by feeling: send(type="sticker", emoji="😂") — picks a matching sticker from your saved packs (add set_name to pin one pack)
|
|
138
|
-
Sticker by id: send(type="sticker", file_id="CAACAgI...")
|
|
138
|
+
Sticker by id: send(type="sticker", file_id="CAACAgI...")
|
|
139
|
+
Album: send(type="album", media=[{"type":"photo","file_path":"a.jpg"},{"type":"photo","url":"https://…/b.jpg","caption":"the good one"}]) — 2-10 photos/videos as one grouped message
|
|
140
|
+
Round video: send(type="video_note", file_path="/path/clip.mp4") — circular video bubble (square video, ≤60s)
|
|
141
|
+
Venue: send(type="venue", latitude=53.34, longitude=-6.26, title="The Long Hall", address="51 South Great George's St")`,
|
|
139
142
|
schema: {
|
|
140
143
|
type: z
|
|
141
144
|
.enum([
|
|
@@ -151,6 +154,9 @@ Examples:
|
|
|
151
154
|
"location",
|
|
152
155
|
"contact",
|
|
153
156
|
"dice",
|
|
157
|
+
"album",
|
|
158
|
+
"video_note",
|
|
159
|
+
"venue",
|
|
154
160
|
])
|
|
155
161
|
.describe("Content type to send"),
|
|
156
162
|
text: z
|
|
@@ -208,7 +214,30 @@ Examples:
|
|
|
208
214
|
phone_number: z.string().optional().describe("Contact phone"),
|
|
209
215
|
first_name: z.string().optional().describe("Contact first name"),
|
|
210
216
|
last_name: z.string().optional().describe("Contact last name"),
|
|
211
|
-
title: z
|
|
217
|
+
title: z
|
|
218
|
+
.string()
|
|
219
|
+
.optional()
|
|
220
|
+
.describe("Audio title (type=audio) or venue name (type=venue)"),
|
|
221
|
+
address: z
|
|
222
|
+
.string()
|
|
223
|
+
.optional()
|
|
224
|
+
.describe("Venue street address (for type=venue)"),
|
|
225
|
+
media: z
|
|
226
|
+
.array(
|
|
227
|
+
z.object({
|
|
228
|
+
type: z
|
|
229
|
+
.enum(["photo", "video", "document", "audio"])
|
|
230
|
+
.describe("Kind of this album item"),
|
|
231
|
+
file_path: z.string().optional(),
|
|
232
|
+
url: z.string().optional(),
|
|
233
|
+
file_id: z.string().optional(),
|
|
234
|
+
caption: z.string().optional(),
|
|
235
|
+
}),
|
|
236
|
+
)
|
|
237
|
+
.optional()
|
|
238
|
+
.describe(
|
|
239
|
+
"Album items (for type=album): 2-10 entries, each sourced from file_path, url, or file_id. Photos and videos mix; documents/audio group only with their own kind.",
|
|
240
|
+
),
|
|
212
241
|
performer: z
|
|
213
242
|
.string()
|
|
214
243
|
.optional()
|
|
@@ -230,9 +259,41 @@ Examples:
|
|
|
230
259
|
.describe(
|
|
231
260
|
"Target chat ID. Omit to send to the current chat (chat mode). Required from heartbeat mode where there is no ambient chat — use list_chats or known IDs from memory. Telegram supergroup/channel IDs are negative (e.g. -1001426819337); user DMs are positive.",
|
|
232
261
|
),
|
|
262
|
+
silent: z
|
|
263
|
+
.boolean()
|
|
264
|
+
.optional()
|
|
265
|
+
.describe("Send without a notification sound (Telegram)"),
|
|
266
|
+
protect: z
|
|
267
|
+
.boolean()
|
|
268
|
+
.optional()
|
|
269
|
+
.describe("Protect content from forwarding and saving (Telegram)"),
|
|
270
|
+
spoiler: z
|
|
271
|
+
.boolean()
|
|
272
|
+
.optional()
|
|
273
|
+
.describe(
|
|
274
|
+
"Blur photo/video/animation behind a spoiler tap-to-reveal (Telegram)",
|
|
275
|
+
),
|
|
276
|
+
no_link_preview: z
|
|
277
|
+
.boolean()
|
|
278
|
+
.optional()
|
|
279
|
+
.describe("Disable the link preview for type=text (Telegram)"),
|
|
280
|
+
thread_id: z
|
|
281
|
+
.union([z.number(), z.literal("general")])
|
|
282
|
+
.optional()
|
|
283
|
+
.describe(
|
|
284
|
+
'Forum topic to post into (Telegram supergroups with topics). Defaults to the topic the conversation is happening in; pass "general" to force the General topic.',
|
|
285
|
+
),
|
|
233
286
|
},
|
|
234
287
|
execute: async (params, bridge) => {
|
|
235
288
|
const { type } = params;
|
|
289
|
+
// Delivery modifiers every Telegram send action understands. Harmless
|
|
290
|
+
// on frontends that don't (handlers read only the fields they know).
|
|
291
|
+
const mods = {
|
|
292
|
+
silent: params.silent,
|
|
293
|
+
protect: params.protect,
|
|
294
|
+
spoiler: params.spoiler,
|
|
295
|
+
thread_id: params.thread_id,
|
|
296
|
+
};
|
|
236
297
|
// Thread chat_id through to every bridge call so heartbeat / dream
|
|
237
298
|
// outbound (no ambient chat) gets routed by the explicit chat_id.
|
|
238
299
|
// `createBridge` at src/core/tools/bridge.ts:29 reads
|
|
@@ -253,6 +314,7 @@ Examples:
|
|
|
253
314
|
delay_seconds: params.delay_seconds,
|
|
254
315
|
rows: params.buttons,
|
|
255
316
|
reply_to_message_id: params.reply_to,
|
|
317
|
+
...mods,
|
|
256
318
|
chat_id,
|
|
257
319
|
});
|
|
258
320
|
}
|
|
@@ -261,12 +323,15 @@ Examples:
|
|
|
261
323
|
text: params.text,
|
|
262
324
|
rows: params.buttons,
|
|
263
325
|
reply_to_message_id: params.reply_to,
|
|
326
|
+
...mods,
|
|
264
327
|
chat_id,
|
|
265
328
|
});
|
|
266
329
|
}
|
|
267
330
|
return bridge("send_message", {
|
|
268
331
|
text: params.text,
|
|
269
332
|
reply_to_message_id: params.reply_to,
|
|
333
|
+
no_link_preview: params.no_link_preview,
|
|
334
|
+
...mods,
|
|
270
335
|
chat_id,
|
|
271
336
|
});
|
|
272
337
|
}
|
|
@@ -277,6 +342,7 @@ Examples:
|
|
|
277
342
|
file_id: params.file_id,
|
|
278
343
|
caption: params.caption,
|
|
279
344
|
reply_to: params.reply_to,
|
|
345
|
+
...mods,
|
|
280
346
|
chat_id,
|
|
281
347
|
});
|
|
282
348
|
case "file":
|
|
@@ -286,6 +352,7 @@ Examples:
|
|
|
286
352
|
file_id: params.file_id,
|
|
287
353
|
caption: params.caption,
|
|
288
354
|
reply_to: params.reply_to,
|
|
355
|
+
...mods,
|
|
289
356
|
chat_id,
|
|
290
357
|
});
|
|
291
358
|
case "video":
|
|
@@ -295,6 +362,7 @@ Examples:
|
|
|
295
362
|
file_id: params.file_id,
|
|
296
363
|
caption: params.caption,
|
|
297
364
|
reply_to: params.reply_to,
|
|
365
|
+
...mods,
|
|
298
366
|
chat_id,
|
|
299
367
|
});
|
|
300
368
|
case "voice":
|
|
@@ -304,6 +372,7 @@ Examples:
|
|
|
304
372
|
file_id: params.file_id,
|
|
305
373
|
caption: params.caption,
|
|
306
374
|
reply_to: params.reply_to,
|
|
375
|
+
...mods,
|
|
307
376
|
chat_id,
|
|
308
377
|
});
|
|
309
378
|
case "audio":
|
|
@@ -315,6 +384,7 @@ Examples:
|
|
|
315
384
|
title: params.title,
|
|
316
385
|
performer: params.performer,
|
|
317
386
|
reply_to: params.reply_to,
|
|
387
|
+
...mods,
|
|
318
388
|
chat_id,
|
|
319
389
|
});
|
|
320
390
|
case "animation":
|
|
@@ -324,6 +394,7 @@ Examples:
|
|
|
324
394
|
file_id: params.file_id,
|
|
325
395
|
caption: params.caption,
|
|
326
396
|
reply_to: params.reply_to,
|
|
397
|
+
...mods,
|
|
327
398
|
chat_id,
|
|
328
399
|
});
|
|
329
400
|
case "sticker":
|
|
@@ -333,6 +404,7 @@ Examples:
|
|
|
333
404
|
emoji: params.emoji,
|
|
334
405
|
set_name: params.set_name,
|
|
335
406
|
reply_to: params.reply_to,
|
|
407
|
+
...mods,
|
|
336
408
|
chat_id,
|
|
337
409
|
});
|
|
338
410
|
case "poll":
|
|
@@ -344,6 +416,7 @@ Examples:
|
|
|
344
416
|
explanation: params.explanation,
|
|
345
417
|
type: params.correct_option_id !== undefined ? "quiz" : "regular",
|
|
346
418
|
reply_to: params.reply_to,
|
|
419
|
+
...mods,
|
|
347
420
|
chat_id,
|
|
348
421
|
});
|
|
349
422
|
case "location":
|
|
@@ -351,6 +424,7 @@ Examples:
|
|
|
351
424
|
latitude: params.latitude,
|
|
352
425
|
longitude: params.longitude,
|
|
353
426
|
reply_to: params.reply_to,
|
|
427
|
+
...mods,
|
|
354
428
|
chat_id,
|
|
355
429
|
});
|
|
356
430
|
case "contact":
|
|
@@ -359,12 +433,40 @@ Examples:
|
|
|
359
433
|
first_name: params.first_name,
|
|
360
434
|
last_name: params.last_name,
|
|
361
435
|
reply_to: params.reply_to,
|
|
436
|
+
...mods,
|
|
362
437
|
chat_id,
|
|
363
438
|
});
|
|
364
439
|
case "dice":
|
|
365
440
|
return bridge("send_dice", {
|
|
366
441
|
emoji: params.emoji,
|
|
367
442
|
reply_to: params.reply_to,
|
|
443
|
+
...mods,
|
|
444
|
+
chat_id,
|
|
445
|
+
});
|
|
446
|
+
case "album":
|
|
447
|
+
return bridge("send_media_group", {
|
|
448
|
+
media: params.media,
|
|
449
|
+
reply_to: params.reply_to,
|
|
450
|
+
...mods,
|
|
451
|
+
chat_id,
|
|
452
|
+
});
|
|
453
|
+
case "video_note":
|
|
454
|
+
return bridge("send_video_note", {
|
|
455
|
+
file_path: params.file_path,
|
|
456
|
+
url: params.url,
|
|
457
|
+
file_id: params.file_id,
|
|
458
|
+
reply_to: params.reply_to,
|
|
459
|
+
...mods,
|
|
460
|
+
chat_id,
|
|
461
|
+
});
|
|
462
|
+
case "venue":
|
|
463
|
+
return bridge("send_venue", {
|
|
464
|
+
latitude: params.latitude,
|
|
465
|
+
longitude: params.longitude,
|
|
466
|
+
title: params.title,
|
|
467
|
+
address: params.address,
|
|
468
|
+
reply_to: params.reply_to,
|
|
469
|
+
...mods,
|
|
368
470
|
chat_id,
|
|
369
471
|
});
|
|
370
472
|
default:
|
|
@@ -479,8 +581,16 @@ Valid emoji: 👍 👎 ❤ 🔥 🥰 👏 😁 🤔 🤯 😱 🤬 😢 🎉
|
|
|
479
581
|
// ── edit_message ──────────────────────────────────────────────────────
|
|
480
582
|
{
|
|
481
583
|
name: "edit_message",
|
|
482
|
-
description:
|
|
483
|
-
|
|
584
|
+
description:
|
|
585
|
+
"Edit a previously sent message. For a media message (photo/video/file), pass is_caption=true to edit its caption instead of message text.",
|
|
586
|
+
schema: {
|
|
587
|
+
message_id: snowflakeOrIdSchema,
|
|
588
|
+
text: z.string(),
|
|
589
|
+
is_caption: z
|
|
590
|
+
.boolean()
|
|
591
|
+
.optional()
|
|
592
|
+
.describe("Edit the media caption rather than message text (Telegram)"),
|
|
593
|
+
},
|
|
484
594
|
execute: (params, bridge) => bridge("edit_message", params),
|
|
485
595
|
frontends: ["telegram", "discord", "native"],
|
|
486
596
|
tag: "messaging",
|
|
@@ -489,8 +599,15 @@ Valid emoji: 👍 👎 ❤ 🔥 🥰 👏 😁 🤔 🤯 😱 🤬 😢 🎉
|
|
|
489
599
|
// ── delete_message ────────────────────────────────────────────────────
|
|
490
600
|
{
|
|
491
601
|
name: "delete_message",
|
|
492
|
-
description:
|
|
493
|
-
|
|
602
|
+
description:
|
|
603
|
+
"Delete a message — or several at once via message_ids (Telegram; ids the bot can't delete are skipped).",
|
|
604
|
+
schema: {
|
|
605
|
+
message_id: snowflakeOrIdSchema.optional(),
|
|
606
|
+
message_ids: z
|
|
607
|
+
.array(snowflakeOrIdSchema)
|
|
608
|
+
.optional()
|
|
609
|
+
.describe("Bulk delete these message IDs (Telegram)"),
|
|
610
|
+
},
|
|
494
611
|
execute: (params, bridge) => bridge("delete_message", params),
|
|
495
612
|
frontends: ["telegram", "discord", "native"],
|
|
496
613
|
tag: "messaging",
|
|
@@ -499,13 +616,42 @@ Valid emoji: 👍 👎 ❤ 🔥 🥰 👏 😁 🤔 🤯 😱 🤬 😢 🎉
|
|
|
499
616
|
// ── forward_message ───────────────────────────────────────────────────
|
|
500
617
|
{
|
|
501
618
|
name: "forward_message",
|
|
502
|
-
description:
|
|
503
|
-
|
|
619
|
+
description:
|
|
620
|
+
"Forward a message. Defaults to within the current chat; from_chat_id / to_chat_id forward across chats the bot is in, and message_ids forwards a batch (albums stay grouped) (Telegram).",
|
|
621
|
+
schema: {
|
|
622
|
+
message_id: snowflakeOrIdSchema.optional(),
|
|
623
|
+
message_ids: z
|
|
624
|
+
.array(snowflakeOrIdSchema)
|
|
625
|
+
.optional()
|
|
626
|
+
.describe("Forward these messages as a batch (Telegram)"),
|
|
627
|
+
from_chat_id: chatIdSchema
|
|
628
|
+
.optional()
|
|
629
|
+
.describe("Source chat (default: current chat)"),
|
|
630
|
+
to_chat_id: chatIdSchema
|
|
631
|
+
.optional()
|
|
632
|
+
.describe("Destination chat (default: current chat)"),
|
|
633
|
+
},
|
|
504
634
|
execute: (params, bridge) => bridge("forward_message", params),
|
|
505
635
|
frontends: ["telegram", "discord"],
|
|
506
636
|
tag: "messaging",
|
|
507
637
|
},
|
|
508
638
|
|
|
639
|
+
// ── copy_message ──────────────────────────────────────────────────────
|
|
640
|
+
{
|
|
641
|
+
name: "copy_message",
|
|
642
|
+
description:
|
|
643
|
+
"Repost a message without the 'forwarded from' header. Same cross-chat and batch semantics as forward_message (Telegram).",
|
|
644
|
+
schema: {
|
|
645
|
+
message_id: snowflakeOrIdSchema.optional(),
|
|
646
|
+
message_ids: z.array(snowflakeOrIdSchema).optional(),
|
|
647
|
+
from_chat_id: chatIdSchema.optional(),
|
|
648
|
+
to_chat_id: chatIdSchema.optional(),
|
|
649
|
+
},
|
|
650
|
+
execute: (params, bridge) => bridge("copy_message", params),
|
|
651
|
+
frontends: ["telegram"],
|
|
652
|
+
tag: "messaging",
|
|
653
|
+
},
|
|
654
|
+
|
|
509
655
|
// ── pin_message ───────────────────────────────────────────────────────
|
|
510
656
|
{
|
|
511
657
|
name: "pin_message",
|