@andreprado/agentkit 0.1.0-alpha.7 → 0.1.0-alpha.9
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 +1 -1
- package/src/cli/commands/channels.ts +349 -34
- package/src/cli/help.ts +2 -0
- package/src/runtime/channels/telegram.ts +125 -13
- package/src/runtime/channels.ts +6 -1
- package/src/templates/skills/agentkit-channels/SKILL.md +5 -5
- package/src/templates/skills/agentkit-channels/references/channel-buffering.md +2 -2
- package/src/templates/skills/agentkit-channels/references/channel-debugging.md +7 -3
- package/src/templates/skills/agentkit-channels/references/telegram.md +5 -4
package/package.json
CHANGED
|
@@ -2,10 +2,11 @@ import { readFile } from "node:fs/promises";
|
|
|
2
2
|
import { resolve } from "node:path";
|
|
3
3
|
|
|
4
4
|
import type { ParsedArgs } from "../args";
|
|
5
|
-
import { cloudFetch, cloudGet, cloudPost } from "../cloud-client";
|
|
5
|
+
import { cloudFetch, cloudGet, cloudPost, readCloudError } from "../cloud-client";
|
|
6
6
|
import { parseCloudApiUrl } from "../flags";
|
|
7
7
|
import { loadAgentCapsule } from "../../runtime/config";
|
|
8
8
|
import { readLocalDeployStateIfExists, type LocalDeployState } from "../../runtime/core/deploy-state";
|
|
9
|
+
import { loadCapsuleEnv } from "../../runtime/env";
|
|
9
10
|
import type { AgentChannel } from "../../index";
|
|
10
11
|
|
|
11
12
|
type DeployState = LocalDeployState;
|
|
@@ -30,9 +31,51 @@ type CloudDelivery = {
|
|
|
30
31
|
event_id?: string;
|
|
31
32
|
};
|
|
32
33
|
received_at: string;
|
|
34
|
+
updated_at?: string;
|
|
35
|
+
error?: {
|
|
36
|
+
code: string;
|
|
37
|
+
message: string;
|
|
38
|
+
};
|
|
39
|
+
events?: Array<{
|
|
40
|
+
state: string;
|
|
41
|
+
message: string;
|
|
42
|
+
created_at: string;
|
|
43
|
+
}>;
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
type ChannelSetupResponse = {
|
|
47
|
+
channel: CloudChannel;
|
|
48
|
+
setup?: {
|
|
49
|
+
provider: "telegram";
|
|
50
|
+
webhook_url: string;
|
|
51
|
+
status: "configured";
|
|
52
|
+
bot_username?: string;
|
|
53
|
+
get_webhook_info?: {
|
|
54
|
+
url: string;
|
|
55
|
+
pending_update_count: number;
|
|
56
|
+
};
|
|
57
|
+
};
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
type ChannelDoctorReport = {
|
|
61
|
+
channel: CloudChannel;
|
|
62
|
+
checks: Array<{
|
|
63
|
+
name: string;
|
|
64
|
+
status: "ok" | "warn" | "fail" | "unknown";
|
|
65
|
+
message: string;
|
|
66
|
+
fix?: string;
|
|
67
|
+
}>;
|
|
68
|
+
last_delivery: {
|
|
69
|
+
inbound: CloudDelivery | null;
|
|
70
|
+
outbound: CloudDelivery | null;
|
|
71
|
+
};
|
|
72
|
+
probable_error: string | null;
|
|
73
|
+
suggested_fix: string | null;
|
|
33
74
|
};
|
|
34
75
|
|
|
35
76
|
export async function handleChannelsCommand(args: ParsedArgs): Promise<void> {
|
|
77
|
+
await loadLocalEnvIntoProcess(process.cwd());
|
|
78
|
+
|
|
36
79
|
const [subcommand, first, second] = args.positional;
|
|
37
80
|
|
|
38
81
|
if (subcommand === "list") {
|
|
@@ -70,23 +113,55 @@ export async function handleChannelsCommand(args: ParsedArgs): Promise<void> {
|
|
|
70
113
|
throw new Error("Usage: agentkit channels add <website|telegram|whatsapp> <name> [--provider zapster|meta]");
|
|
71
114
|
}
|
|
72
115
|
|
|
73
|
-
const
|
|
74
|
-
const provider = channelProviderForCli(type, args.flags.provider);
|
|
75
|
-
const localChannel = await readLocalChannelConfig(process.cwd(), type, provider, name);
|
|
76
|
-
const payload = {
|
|
77
|
-
name,
|
|
78
|
-
type,
|
|
79
|
-
provider,
|
|
80
|
-
secrets: localChannel?.secrets ?? defaultChannelSecretsForCli(type, provider),
|
|
81
|
-
...(localChannel?.limits ? { limits: channelLimitsForApi(localChannel.limits) } : {}),
|
|
82
|
-
...(localChannel?.buffer ? { buffer: localChannel.buffer } : {}),
|
|
83
|
-
};
|
|
84
|
-
const response = await cloudPost<{ channel: CloudChannel }>(
|
|
85
|
-
parseCloudApiUrl(args.flags.api),
|
|
86
|
-
`/v1/deploys/${encodeURIComponent(deployState.deploy_id)}/channels`,
|
|
87
|
-
payload,
|
|
88
|
-
);
|
|
116
|
+
const response = await createHostedChannel(type, name, args.flags.provider, args.flags.api);
|
|
89
117
|
printChannelStatus(response.channel);
|
|
118
|
+
printSecretRepairHint(response.channel);
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
if (subcommand === "connect") {
|
|
123
|
+
const type = first;
|
|
124
|
+
const name = second;
|
|
125
|
+
|
|
126
|
+
if (!type || !name || (type !== "website" && type !== "telegram" && type !== "whatsapp")) {
|
|
127
|
+
throw new Error("Usage: agentkit channels connect <website|telegram|whatsapp> <name> [--provider zapster|meta]");
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
const created = await createHostedChannel(type, name, args.flags.provider, args.flags.api);
|
|
131
|
+
let channel = created.channel;
|
|
132
|
+
printChannelStatus(channel);
|
|
133
|
+
printSecretRepairHint(channel);
|
|
134
|
+
|
|
135
|
+
if (hasMissingRequiredSecrets(channel)) {
|
|
136
|
+
console.log("");
|
|
137
|
+
console.log("Connect blocked: set the missing managed secrets, then rerun this same command.");
|
|
138
|
+
console.log("Run: npm run agentkit -- secret sync --from-local");
|
|
139
|
+
process.exitCode = 1;
|
|
140
|
+
return;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
if (channel.type === "telegram") {
|
|
144
|
+
const setup = await applyTelegramWebhook(channel, args.flags.api);
|
|
145
|
+
channel = setup.channel ?? channel;
|
|
146
|
+
console.log("Telegram webhook confirmed.");
|
|
147
|
+
if (setup.setup?.get_webhook_info) {
|
|
148
|
+
console.log(`Telegram pending updates: ${setup.setup.get_webhook_info.pending_update_count}`);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
const smoke = await runChannelSmoke(channel, args.flags.api, args.flags.message);
|
|
152
|
+
console.log(`Smoke: ${smoke.status}${smoke.delivery_id ? ` (${smoke.delivery_id})` : ""}`);
|
|
153
|
+
printTelegramHumanNextStep(channel.name, setup.setup?.bot_username);
|
|
154
|
+
return;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
if (channel.type === "whatsapp") {
|
|
158
|
+
console.log("Smoke: skipped until the provider webhook is configured.");
|
|
159
|
+
console.log(`Next human step: paste ${channel.webhook_url} into ${channel.provider}'s webhook settings.`);
|
|
160
|
+
} else {
|
|
161
|
+
const smoke = await runChannelSmoke(channel, args.flags.api, args.flags.message);
|
|
162
|
+
console.log(`Smoke: ${smoke.status}${smoke.delivery_id ? ` (${smoke.delivery_id})` : ""}`);
|
|
163
|
+
console.log(`Next human step: use ${channel.webhook_url} from your website client.`);
|
|
164
|
+
}
|
|
90
165
|
return;
|
|
91
166
|
}
|
|
92
167
|
|
|
@@ -99,8 +174,10 @@ export async function handleChannelsCommand(args: ParsedArgs): Promise<void> {
|
|
|
99
174
|
printChannelStatus(channel);
|
|
100
175
|
if (channel.type === "telegram") {
|
|
101
176
|
if (args.flags.apply) {
|
|
102
|
-
await applyTelegramWebhook(channel, args.flags.api);
|
|
103
|
-
console.log("Telegram webhook
|
|
177
|
+
const setup = await applyTelegramWebhook(channel, args.flags.api);
|
|
178
|
+
console.log("Telegram webhook confirmed.");
|
|
179
|
+
printChannelStatus(setup.channel ?? channel);
|
|
180
|
+
printTelegramHumanNextStep(channel.name, setup.setup?.bot_username);
|
|
104
181
|
} else {
|
|
105
182
|
console.log(`Setup: call Telegram setWebhook with ${channel.webhook_url}`);
|
|
106
183
|
console.log("Use --apply to mutate Telegram webhook settings.");
|
|
@@ -125,6 +202,24 @@ export async function handleChannelsCommand(args: ParsedArgs): Promise<void> {
|
|
|
125
202
|
return;
|
|
126
203
|
}
|
|
127
204
|
|
|
205
|
+
if (subcommand === "doctor") {
|
|
206
|
+
if (!first) {
|
|
207
|
+
throw new Error("Usage: agentkit channels doctor <name>");
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
const channel = await resolveCloudChannelByName(first, args.flags.api);
|
|
211
|
+
const report = await cloudGet<{ doctor: ChannelDoctorReport }>(
|
|
212
|
+
parseCloudApiUrl(args.flags.api),
|
|
213
|
+
`/v1/channels/${encodeURIComponent(channel.id)}/doctor`,
|
|
214
|
+
);
|
|
215
|
+
printChannelDoctor(report.doctor);
|
|
216
|
+
|
|
217
|
+
if (report.doctor.checks.some((check) => check.status === "fail")) {
|
|
218
|
+
process.exitCode = 1;
|
|
219
|
+
}
|
|
220
|
+
return;
|
|
221
|
+
}
|
|
222
|
+
|
|
128
223
|
if (subcommand === "test") {
|
|
129
224
|
if (!first) {
|
|
130
225
|
throw new Error('Usage: agentkit channels test <name> [--message "hello"] [--fixture <path>]');
|
|
@@ -133,14 +228,9 @@ export async function handleChannelsCommand(args: ParsedArgs): Promise<void> {
|
|
|
133
228
|
const channel = await resolveCloudChannelByName(first, args.flags.api);
|
|
134
229
|
const apiUrl = parseCloudApiUrl(args.flags.api);
|
|
135
230
|
const body = await channelTestBody(channel, args.flags.message, args.flags.fixture);
|
|
136
|
-
const
|
|
137
|
-
const
|
|
138
|
-
const
|
|
139
|
-
method: channel.type === "whatsapp" && channel.provider === "meta" ? "POST" : "POST",
|
|
140
|
-
headers,
|
|
141
|
-
body,
|
|
142
|
-
});
|
|
143
|
-
const payload = await response.json();
|
|
231
|
+
const signed = await runServerSignedChannelTest(channel, apiUrl, body);
|
|
232
|
+
const response = signed.response;
|
|
233
|
+
const payload = signed.payload;
|
|
144
234
|
|
|
145
235
|
console.log(JSON.stringify(payload, null, 2));
|
|
146
236
|
if (!response.ok) {
|
|
@@ -197,21 +287,113 @@ export async function handleChannelsCommand(args: ParsedArgs): Promise<void> {
|
|
|
197
287
|
}
|
|
198
288
|
|
|
199
289
|
throw new Error(
|
|
200
|
-
"Usage: agentkit channels list | add | setup | status | test | deliveries list | deliveries show",
|
|
290
|
+
"Usage: agentkit channels list | add | connect | setup | status | doctor | test | deliveries list | deliveries show",
|
|
291
|
+
);
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
async function createHostedChannel(
|
|
295
|
+
type: "website" | "telegram" | "whatsapp",
|
|
296
|
+
name: string,
|
|
297
|
+
providerFlag: string | boolean | undefined,
|
|
298
|
+
apiFlag: string | boolean | undefined,
|
|
299
|
+
): Promise<{ channel: CloudChannel }> {
|
|
300
|
+
const deployState = await readDeployStateRequired(process.cwd());
|
|
301
|
+
const provider = channelProviderForCli(type, providerFlag);
|
|
302
|
+
const localChannel = await readLocalChannelConfig(process.cwd(), type, provider, name);
|
|
303
|
+
const payload = {
|
|
304
|
+
name,
|
|
305
|
+
type,
|
|
306
|
+
provider,
|
|
307
|
+
secrets: localChannel?.secrets ?? defaultChannelSecretsForCli(type, provider),
|
|
308
|
+
...(localChannel?.limits ? { limits: channelLimitsForApi(localChannel.limits) } : {}),
|
|
309
|
+
...(localChannel?.buffer ? { buffer: localChannel.buffer } : {}),
|
|
310
|
+
};
|
|
311
|
+
|
|
312
|
+
return cloudPost<{ channel: CloudChannel }>(
|
|
313
|
+
parseCloudApiUrl(apiFlag),
|
|
314
|
+
`/v1/deploys/${encodeURIComponent(deployState.deploy_id)}/channels`,
|
|
315
|
+
payload,
|
|
316
|
+
);
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
async function runChannelSmoke(
|
|
320
|
+
channel: CloudChannel,
|
|
321
|
+
apiFlag: string | boolean | undefined,
|
|
322
|
+
messageFlag: string | boolean | undefined,
|
|
323
|
+
): Promise<{ status: string; delivery_id?: string }> {
|
|
324
|
+
const body = await channelTestBody(channel, messageFlag, undefined);
|
|
325
|
+
const { response, payload } = await runServerSignedChannelTest(channel, parseCloudApiUrl(apiFlag), body);
|
|
326
|
+
|
|
327
|
+
if (!response.ok) {
|
|
328
|
+
throw new Error(readCloudError(payload, response.status));
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
if (isRecord(payload) && typeof payload.status === "string") {
|
|
332
|
+
return {
|
|
333
|
+
status: payload.status,
|
|
334
|
+
...(typeof payload.delivery_id === "string" ? { delivery_id: payload.delivery_id } : {}),
|
|
335
|
+
};
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
return { status: "unknown" };
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
async function runServerSignedChannelTest(
|
|
342
|
+
channel: CloudChannel,
|
|
343
|
+
apiUrl: string,
|
|
344
|
+
rawBody: string,
|
|
345
|
+
): Promise<{ response: Response; payload: unknown }> {
|
|
346
|
+
const response = await cloudFetch(apiUrl, `/v1/channels/${encodeURIComponent(channel.id)}/test`, {
|
|
347
|
+
method: "POST",
|
|
348
|
+
headers: { "Content-Type": "application/json" },
|
|
349
|
+
body: JSON.stringify({ raw_body: rawBody }),
|
|
350
|
+
});
|
|
351
|
+
const payload = await response.json().catch(() => null);
|
|
352
|
+
|
|
353
|
+
if (!response.ok && isUnsupportedChannelTestRoute(payload)) {
|
|
354
|
+
return runLocallySignedChannelTest(channel, apiUrl, rawBody);
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
return { response, payload };
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
async function runLocallySignedChannelTest(
|
|
361
|
+
channel: CloudChannel,
|
|
362
|
+
apiUrl: string,
|
|
363
|
+
rawBody: string,
|
|
364
|
+
): Promise<{ response: Response; payload: unknown }> {
|
|
365
|
+
const path = new URL(channel.webhook_url).pathname;
|
|
366
|
+
const headers = channelTestHeaders(channel);
|
|
367
|
+
const response = await cloudFetch(apiUrl, path, {
|
|
368
|
+
method: "POST",
|
|
369
|
+
headers,
|
|
370
|
+
body: rawBody,
|
|
371
|
+
});
|
|
372
|
+
const payload = await response.json().catch(() => null);
|
|
373
|
+
|
|
374
|
+
return { response, payload };
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
function isUnsupportedChannelTestRoute(payload: unknown): boolean {
|
|
378
|
+
return (
|
|
379
|
+
isRecord(payload) &&
|
|
380
|
+
isRecord(payload.error) &&
|
|
381
|
+
(payload.error.code === "not_found" ||
|
|
382
|
+
payload.error.code === "channel_store_not_configured" ||
|
|
383
|
+
payload.error.code === "channel_delivery_not_found")
|
|
201
384
|
);
|
|
202
385
|
}
|
|
203
386
|
|
|
204
387
|
async function applyTelegramWebhook(
|
|
205
388
|
channel: CloudChannel,
|
|
206
389
|
apiFlag: string | boolean | undefined,
|
|
207
|
-
): Promise<
|
|
390
|
+
): Promise<ChannelSetupResponse> {
|
|
208
391
|
try {
|
|
209
|
-
await cloudPost(
|
|
392
|
+
return await cloudPost<ChannelSetupResponse>(
|
|
210
393
|
parseCloudApiUrl(apiFlag),
|
|
211
394
|
`/v1/channels/${encodeURIComponent(channel.id)}/setup`,
|
|
212
395
|
{},
|
|
213
396
|
);
|
|
214
|
-
return;
|
|
215
397
|
} catch (error) {
|
|
216
398
|
if (!shouldFallbackToLocalTelegramSetup(error)) {
|
|
217
399
|
throw error;
|
|
@@ -226,9 +408,10 @@ async function applyTelegramWebhook(
|
|
|
226
408
|
}
|
|
227
409
|
|
|
228
410
|
let response: Response;
|
|
411
|
+
const apiBaseUrl = telegramApiBaseUrlFromEnv();
|
|
229
412
|
|
|
230
413
|
try {
|
|
231
|
-
response = await fetch(
|
|
414
|
+
response = await fetch(new URL(`/bot${botToken}/setWebhook`, apiBaseUrl).href, {
|
|
232
415
|
method: "POST",
|
|
233
416
|
headers: { "Content-Type": "application/json" },
|
|
234
417
|
body: JSON.stringify({
|
|
@@ -246,6 +429,40 @@ async function applyTelegramWebhook(
|
|
|
246
429
|
if (!response.ok || !isTelegramOk(payload)) {
|
|
247
430
|
throw new Error(readTelegramSetupError(payload, response.status));
|
|
248
431
|
}
|
|
432
|
+
|
|
433
|
+
const webhookInfo = await fetch(new URL(`/bot${botToken}/getWebhookInfo`, apiBaseUrl).href)
|
|
434
|
+
.then((telegramResponse) => telegramResponse.json())
|
|
435
|
+
.catch(() => null);
|
|
436
|
+
const webhookUrl =
|
|
437
|
+
webhookInfo &&
|
|
438
|
+
typeof webhookInfo === "object" &&
|
|
439
|
+
"result" in webhookInfo &&
|
|
440
|
+
webhookInfo.result &&
|
|
441
|
+
typeof webhookInfo.result === "object" &&
|
|
442
|
+
"url" in webhookInfo.result &&
|
|
443
|
+
typeof webhookInfo.result.url === "string"
|
|
444
|
+
? webhookInfo.result.url
|
|
445
|
+
: "";
|
|
446
|
+
|
|
447
|
+
if (webhookUrl !== channel.webhook_url) {
|
|
448
|
+
throw new Error(`Telegram getWebhookInfo returned ${webhookUrl || "no webhook URL"} instead of ${channel.webhook_url}.`);
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
return {
|
|
452
|
+
channel: {
|
|
453
|
+
...channel,
|
|
454
|
+
status: "connected",
|
|
455
|
+
},
|
|
456
|
+
setup: {
|
|
457
|
+
provider: "telegram",
|
|
458
|
+
webhook_url: channel.webhook_url,
|
|
459
|
+
status: "configured",
|
|
460
|
+
get_webhook_info: {
|
|
461
|
+
url: webhookUrl,
|
|
462
|
+
pending_update_count: 0,
|
|
463
|
+
},
|
|
464
|
+
},
|
|
465
|
+
};
|
|
249
466
|
}
|
|
250
467
|
|
|
251
468
|
function shouldFallbackToLocalTelegramSetup(error: unknown): boolean {
|
|
@@ -270,6 +487,33 @@ function readTelegramSetupError(payload: unknown, status: number): string {
|
|
|
270
487
|
return `Telegram setWebhook failed with HTTP ${status}.`;
|
|
271
488
|
}
|
|
272
489
|
|
|
490
|
+
function telegramApiBaseUrlFromEnv(): string {
|
|
491
|
+
return process.env.AGENTKIT_TELEGRAM_API_BASE_URL ?? "https://api.telegram.org";
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
async function loadLocalEnvIntoProcess(cwd: string): Promise<void> {
|
|
495
|
+
const env = await loadCapsuleEnv(cwd, process.env);
|
|
496
|
+
|
|
497
|
+
for (const [key, value] of Object.entries(env)) {
|
|
498
|
+
if (value !== undefined && process.env[key] === undefined) {
|
|
499
|
+
process.env[key] = value;
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
function printTelegramHumanNextStep(channelName: string, botUsername: string | undefined): void {
|
|
505
|
+
console.log("");
|
|
506
|
+
console.log("Next human step:");
|
|
507
|
+
console.log(` Open ${botUsername ? `@${botUsername}` : "your Telegram bot"} in Telegram`);
|
|
508
|
+
console.log(" Send /start");
|
|
509
|
+
console.log(' Send "hello"');
|
|
510
|
+
console.log(` Then run: npm run agentkit -- channels deliveries list ${channelName}`);
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
514
|
+
return Boolean(value && typeof value === "object" && !Array.isArray(value));
|
|
515
|
+
}
|
|
516
|
+
|
|
273
517
|
async function readDeployStateIfExists(cwd: string): Promise<DeployState | null> {
|
|
274
518
|
return readLocalDeployStateIfExists(cwd);
|
|
275
519
|
}
|
|
@@ -308,8 +552,14 @@ function printChannelStatus(channel: CloudChannel): void {
|
|
|
308
552
|
console.log(`Webhook URL: ${channel.webhook_url}`);
|
|
309
553
|
}
|
|
310
554
|
console.log("Required secrets:");
|
|
311
|
-
|
|
312
|
-
console.log(
|
|
555
|
+
if (channel.required_secrets.length === 0 && defaultChannelSecretsForCli(channel.type, channel.provider).length > 0) {
|
|
556
|
+
console.log(
|
|
557
|
+
` control-plane channel metadata bug: required_secrets is empty for ${channel.type}/${channel.provider}`,
|
|
558
|
+
);
|
|
559
|
+
} else {
|
|
560
|
+
for (const secret of channel.required_secrets) {
|
|
561
|
+
console.log(` ${secret.name}: ${secret.status}`);
|
|
562
|
+
}
|
|
313
563
|
}
|
|
314
564
|
if (channel.buffer) {
|
|
315
565
|
if (channel.buffer.mode === "off") {
|
|
@@ -322,6 +572,26 @@ function printChannelStatus(channel: CloudChannel): void {
|
|
|
322
572
|
}
|
|
323
573
|
}
|
|
324
574
|
|
|
575
|
+
function printSecretRepairHint(channel: CloudChannel): void {
|
|
576
|
+
if (channel.required_secrets.length > 0 || defaultChannelSecretsForCli(channel.type, channel.provider).length === 0) {
|
|
577
|
+
return;
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
console.log(
|
|
581
|
+
"Hint: control-plane channel metadata bug. Rerun add/connect against the updated control-plane to repair required_secrets.",
|
|
582
|
+
);
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
function hasMissingRequiredSecrets(channel: CloudChannel): boolean {
|
|
586
|
+
const expected = defaultChannelSecretsForCli(channel.type, channel.provider);
|
|
587
|
+
|
|
588
|
+
if (channel.required_secrets.length === 0 && expected.length > 0) {
|
|
589
|
+
return true;
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
return channel.required_secrets.some((secret) => secret.status !== "set");
|
|
593
|
+
}
|
|
594
|
+
|
|
325
595
|
async function printChannelDeliverySummary(
|
|
326
596
|
channel: CloudChannel,
|
|
327
597
|
apiFlag: string | boolean | undefined,
|
|
@@ -339,6 +609,51 @@ async function printChannelDeliverySummary(
|
|
|
339
609
|
console.log("Provider health: not_checked");
|
|
340
610
|
}
|
|
341
611
|
|
|
612
|
+
function printChannelDoctor(report: ChannelDoctorReport): void {
|
|
613
|
+
console.log(`Channel doctor: ${report.channel.name}`);
|
|
614
|
+
console.log("");
|
|
615
|
+
console.log("Checks:");
|
|
616
|
+
for (const check of report.checks) {
|
|
617
|
+
console.log(` ${formatCheckStatus(check.status)} ${check.name}: ${check.message}`);
|
|
618
|
+
if (check.fix) {
|
|
619
|
+
console.log(` Fix: ${check.fix}`);
|
|
620
|
+
}
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
console.log("");
|
|
624
|
+
console.log("Last delivery:");
|
|
625
|
+
console.log(
|
|
626
|
+
` Inbound: ${report.last_delivery.inbound ? `${report.last_delivery.inbound.status} ${report.last_delivery.inbound.updated_at ?? report.last_delivery.inbound.received_at}` : "none"}`,
|
|
627
|
+
);
|
|
628
|
+
console.log(
|
|
629
|
+
` Outbound: ${report.last_delivery.outbound ? `${report.last_delivery.outbound.status} ${report.last_delivery.outbound.updated_at ?? report.last_delivery.outbound.received_at}` : "none"}`,
|
|
630
|
+
);
|
|
631
|
+
|
|
632
|
+
if (report.probable_error) {
|
|
633
|
+
console.log("");
|
|
634
|
+
console.log(`Probable error: ${report.probable_error}`);
|
|
635
|
+
}
|
|
636
|
+
if (report.suggested_fix) {
|
|
637
|
+
console.log(`Suggested fix: ${report.suggested_fix}`);
|
|
638
|
+
}
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
function formatCheckStatus(status: ChannelDoctorReport["checks"][number]["status"]): string {
|
|
642
|
+
if (status === "ok") {
|
|
643
|
+
return "OK";
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
if (status === "warn") {
|
|
647
|
+
return "WARN";
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
if (status === "fail") {
|
|
651
|
+
return "FAIL";
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
return "UNKNOWN";
|
|
655
|
+
}
|
|
656
|
+
|
|
342
657
|
async function resolveCloudChannelByName(name: string, apiFlag: string | boolean | undefined): Promise<CloudChannel> {
|
|
343
658
|
const deployState = await readDeployStateRequired(process.cwd());
|
|
344
659
|
const response = await cloudGet<{ channels: CloudChannel[] }>(
|
package/src/cli/help.ts
CHANGED
|
@@ -77,8 +77,10 @@ Usage:
|
|
|
77
77
|
agentkit conversations trace <conversation-id>
|
|
78
78
|
agentkit channels list
|
|
79
79
|
agentkit channels add <website|telegram|whatsapp> <name> [--provider zapster|meta] [--api <url>]
|
|
80
|
+
agentkit channels connect <website|telegram|whatsapp> <name> [--provider zapster|meta] [--api <url>]
|
|
80
81
|
agentkit channels setup <name> [--apply] [--api <url>]
|
|
81
82
|
agentkit channels status <name> [--api <url>]
|
|
83
|
+
agentkit channels doctor <name> [--api <url>]
|
|
82
84
|
agentkit channels test <name> [--message <text>] [--fixture <path>] [--api <url>]
|
|
83
85
|
agentkit channels deliveries list <name> [--api <url>]
|
|
84
86
|
agentkit channels deliveries show <delivery-id> [--api <url>]
|
|
@@ -112,18 +112,7 @@ export const telegramChannelAdapter: ChannelAdapter = {
|
|
|
112
112
|
];
|
|
113
113
|
},
|
|
114
114
|
async sendMessage(input) {
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
return {
|
|
118
|
-
ok: true,
|
|
119
|
-
status: "sent",
|
|
120
|
-
providerRequestId: request.idempotencyKey,
|
|
121
|
-
providerMetadata: {
|
|
122
|
-
method: request.method,
|
|
123
|
-
url: request.url,
|
|
124
|
-
body: request.body,
|
|
125
|
-
},
|
|
126
|
-
};
|
|
115
|
+
return sendTelegramMessage(input);
|
|
127
116
|
},
|
|
128
117
|
getStatus(input) {
|
|
129
118
|
if (!input.secrets.TELEGRAM_BOT_TOKEN) {
|
|
@@ -152,7 +141,130 @@ export type TelegramSendMessageRequest = {
|
|
|
152
141
|
idempotencyKey: string;
|
|
153
142
|
};
|
|
154
143
|
|
|
144
|
+
type TelegramFetch = (url: string, init: RequestInit) => Promise<Response>;
|
|
145
|
+
|
|
155
146
|
export function buildTelegramSendMessageRequest(input: ChannelSendInput): TelegramSendMessageRequest {
|
|
147
|
+
return buildTelegramSendMessageRequestInternal(input, "<redacted>");
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
export async function sendTelegramMessage(
|
|
151
|
+
input: ChannelSendInput,
|
|
152
|
+
fetcher: TelegramFetch = fetch,
|
|
153
|
+
): Promise<Awaited<ReturnType<ChannelAdapter["sendMessage"]>>> {
|
|
154
|
+
if (input.secrets.AGENTKIT_CHANNEL_SEND_DRY_RUN === "1") {
|
|
155
|
+
const request = buildTelegramSendMessageRequest(input);
|
|
156
|
+
|
|
157
|
+
return {
|
|
158
|
+
ok: true,
|
|
159
|
+
status: "sent",
|
|
160
|
+
providerRequestId: request.idempotencyKey,
|
|
161
|
+
providerMetadata: {
|
|
162
|
+
method: request.method,
|
|
163
|
+
url: request.url,
|
|
164
|
+
body: request.body,
|
|
165
|
+
},
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
const token = input.secrets.TELEGRAM_BOT_TOKEN;
|
|
170
|
+
|
|
171
|
+
if (!token) {
|
|
172
|
+
return {
|
|
173
|
+
ok: false,
|
|
174
|
+
retryable: false,
|
|
175
|
+
code: "channel_secret_missing",
|
|
176
|
+
message: "Secret TELEGRAM_BOT_TOKEN is not set for this channel.",
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
let request: TelegramSendMessageRequest;
|
|
181
|
+
let actualRequest: TelegramSendMessageRequest;
|
|
182
|
+
|
|
183
|
+
try {
|
|
184
|
+
request = buildTelegramSendMessageRequest(input);
|
|
185
|
+
actualRequest = buildTelegramSendMessageRequestInternal(input, token);
|
|
186
|
+
} catch (error) {
|
|
187
|
+
return {
|
|
188
|
+
ok: false,
|
|
189
|
+
retryable: false,
|
|
190
|
+
code: "channel_send_failed",
|
|
191
|
+
message: error instanceof Error ? error.message : String(error),
|
|
192
|
+
};
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
let response: Response;
|
|
196
|
+
|
|
197
|
+
try {
|
|
198
|
+
response = await fetcher(actualRequest.url, {
|
|
199
|
+
method: actualRequest.method,
|
|
200
|
+
headers: {
|
|
201
|
+
"Content-Type": "application/json",
|
|
202
|
+
},
|
|
203
|
+
body: JSON.stringify(actualRequest.body),
|
|
204
|
+
});
|
|
205
|
+
} catch (error) {
|
|
206
|
+
const errorMessage = (error instanceof Error ? error.message : String(error)).replace(token, "<redacted>");
|
|
207
|
+
|
|
208
|
+
return {
|
|
209
|
+
ok: false,
|
|
210
|
+
retryable: true,
|
|
211
|
+
code: "channel_provider_unavailable",
|
|
212
|
+
message: `Telegram sendMessage request failed before a response: ${errorMessage}`,
|
|
213
|
+
providerRequestId: request.idempotencyKey,
|
|
214
|
+
providerMetadata: {
|
|
215
|
+
method: request.method,
|
|
216
|
+
url: request.url,
|
|
217
|
+
body: request.body,
|
|
218
|
+
},
|
|
219
|
+
};
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
const payload = await response.json().catch(() => null);
|
|
223
|
+
|
|
224
|
+
if (!response.ok || !isRecord(payload) || payload.ok !== true) {
|
|
225
|
+
const description =
|
|
226
|
+
isRecord(payload) && typeof payload.description === "string"
|
|
227
|
+
? payload.description
|
|
228
|
+
: `Telegram sendMessage returned HTTP ${response.status}.`;
|
|
229
|
+
const syntheticExpectedFailure = isSyntheticTelegramChatId(request.body.chat_id) && /chat not found/i.test(description);
|
|
230
|
+
|
|
231
|
+
return {
|
|
232
|
+
ok: false,
|
|
233
|
+
retryable: syntheticExpectedFailure ? false : response.status === 429 || response.status >= 500,
|
|
234
|
+
code: syntheticExpectedFailure ? "synthetic_expected_failure" : "channel_send_failed",
|
|
235
|
+
message: syntheticExpectedFailure
|
|
236
|
+
? "Synthetic Telegram smoke reached AgentKit. Telegram correctly rejected the fake chat ID."
|
|
237
|
+
: description,
|
|
238
|
+
providerRequestId: request.idempotencyKey,
|
|
239
|
+
providerMetadata: {
|
|
240
|
+
method: request.method,
|
|
241
|
+
url: request.url,
|
|
242
|
+
body: request.body,
|
|
243
|
+
},
|
|
244
|
+
};
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
const result = isRecord(payload.result) ? payload.result : undefined;
|
|
248
|
+
const messageId = readNumberishString(result ?? {}, "message_id");
|
|
249
|
+
|
|
250
|
+
return {
|
|
251
|
+
ok: true,
|
|
252
|
+
status: "sent",
|
|
253
|
+
providerRequestId: request.idempotencyKey,
|
|
254
|
+
...(messageId ? { providerMessageId: messageId } : {}),
|
|
255
|
+
providerMetadata: {
|
|
256
|
+
method: request.method,
|
|
257
|
+
url: request.url,
|
|
258
|
+
body: request.body,
|
|
259
|
+
},
|
|
260
|
+
};
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
function isSyntheticTelegramChatId(chatId: string): boolean {
|
|
264
|
+
return chatId === "456" || chatId.startsWith("agentkit-synthetic-");
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
function buildTelegramSendMessageRequestInternal(input: ChannelSendInput, token: string): TelegramSendMessageRequest {
|
|
156
268
|
const chatId = input.externalIdentity.key.match(/:chat:([^:]+)$/)?.[1];
|
|
157
269
|
|
|
158
270
|
if (!chatId) {
|
|
@@ -161,7 +273,7 @@ export function buildTelegramSendMessageRequest(input: ChannelSendInput): Telegr
|
|
|
161
273
|
|
|
162
274
|
return {
|
|
163
275
|
method: "POST",
|
|
164
|
-
url:
|
|
276
|
+
url: `https://api.telegram.org/bot${token}/sendMessage`,
|
|
165
277
|
body: {
|
|
166
278
|
chat_id: chatId,
|
|
167
279
|
text: input.message.content,
|
package/src/runtime/channels.ts
CHANGED
|
@@ -5,12 +5,17 @@ export type ChannelMessageContentType = "text";
|
|
|
5
5
|
export type ChannelEventKind = "message" | "delivery_status" | "unsupported";
|
|
6
6
|
export type ChannelDeliveryDirection = "inbound" | "outbound";
|
|
7
7
|
export type ChannelDeliveryState =
|
|
8
|
+
| "webhook_received"
|
|
8
9
|
| "received"
|
|
9
10
|
| "validated"
|
|
10
11
|
| "duplicate"
|
|
11
12
|
| "buffered"
|
|
12
13
|
| "queued"
|
|
13
14
|
| "running"
|
|
15
|
+
| "agent_completed"
|
|
16
|
+
| "outbound_sent"
|
|
17
|
+
| "provider_failed"
|
|
18
|
+
| "synthetic_expected_failure"
|
|
14
19
|
| "sent"
|
|
15
20
|
| "delivered"
|
|
16
21
|
| "failed"
|
|
@@ -90,7 +95,7 @@ export type ChannelSendResult =
|
|
|
90
95
|
| {
|
|
91
96
|
ok: false;
|
|
92
97
|
retryable: boolean;
|
|
93
|
-
code: "channel_secret_missing" | "channel_provider_unavailable" | "channel_send_failed";
|
|
98
|
+
code: "channel_secret_missing" | "channel_provider_unavailable" | "channel_send_failed" | "synthetic_expected_failure";
|
|
94
99
|
message: string;
|
|
95
100
|
providerRequestId?: string;
|
|
96
101
|
providerMetadata?: Record<string, unknown>;
|
|
@@ -12,9 +12,9 @@ Channels receive user messages. Tools let the agent call external systems. Keep
|
|
|
12
12
|
1. Add channel helpers in `agentkit.config.ts`.
|
|
13
13
|
2. Keep `runtime: "edge"` and `storage.driver: "agentkit"`.
|
|
14
14
|
3. Deploy before hosted channel creation.
|
|
15
|
-
4.
|
|
16
|
-
5.
|
|
17
|
-
6. Test and inspect delivery logs.
|
|
15
|
+
4. Configure provider secrets as managed secrets.
|
|
16
|
+
5. Connect channel resources through the CLI.
|
|
17
|
+
6. Test, doctor, and inspect delivery logs.
|
|
18
18
|
|
|
19
19
|
## Buffering
|
|
20
20
|
|
|
@@ -43,9 +43,9 @@ npm run agentkit -- inspect
|
|
|
43
43
|
npm run agentkit -- deploy
|
|
44
44
|
npm run agentkit -- channels list
|
|
45
45
|
npm run agentkit -- channels add website website-chat
|
|
46
|
-
npm run agentkit -- channels
|
|
46
|
+
npm run agentkit -- channels connect telegram support-telegram
|
|
47
47
|
npm run agentkit -- channels add whatsapp support-whatsapp --provider zapster
|
|
48
|
-
npm run agentkit -- channels
|
|
48
|
+
npm run agentkit -- channels doctor support-telegram
|
|
49
49
|
npm run agentkit -- channels test support-telegram --message "hello"
|
|
50
50
|
npm run agentkit -- channels deliveries list support-telegram
|
|
51
51
|
```
|
|
@@ -5,6 +5,7 @@ Start with:
|
|
|
5
5
|
```sh
|
|
6
6
|
agentkit channels list
|
|
7
7
|
agentkit channels status <name>
|
|
8
|
+
agentkit channels doctor <name>
|
|
8
9
|
agentkit channels test <name> --message "hello"
|
|
9
10
|
agentkit channels deliveries list <name> --since 24h
|
|
10
11
|
agentkit channels deliveries show <delivery-id>
|
|
@@ -13,15 +14,17 @@ agentkit channels deliveries show <delivery-id>
|
|
|
13
14
|
Common states:
|
|
14
15
|
|
|
15
16
|
```txt
|
|
16
|
-
|
|
17
|
+
webhook_received
|
|
17
18
|
validated
|
|
18
19
|
duplicate
|
|
19
20
|
buffered
|
|
20
21
|
queued
|
|
21
22
|
running
|
|
22
|
-
|
|
23
|
+
agent_completed
|
|
24
|
+
outbound_sent
|
|
23
25
|
delivered
|
|
24
|
-
|
|
26
|
+
provider_failed
|
|
27
|
+
synthetic_expected_failure
|
|
25
28
|
dead_lettered
|
|
26
29
|
skipped
|
|
27
30
|
```
|
|
@@ -34,4 +37,5 @@ Common errors:
|
|
|
34
37
|
- `channel_payload_invalid`: malformed or unsupported provider payload.
|
|
35
38
|
- `channel_event_duplicate`: provider retry; do not create a second run.
|
|
36
39
|
- `channel_limit_exceeded`: backpressure skipped the message.
|
|
40
|
+
- `synthetic_expected_failure`: a synthetic test reached AgentKit, but the provider correctly rejected a fake test recipient.
|
|
37
41
|
- `buffered` delivery state: message is waiting for the channel quiet window or max wait before one coalesced agent run is queued.
|
|
@@ -11,15 +11,16 @@ Commands:
|
|
|
11
11
|
|
|
12
12
|
```sh
|
|
13
13
|
agentkit deploy
|
|
14
|
-
agentkit
|
|
15
|
-
agentkit
|
|
16
|
-
agentkit channels
|
|
14
|
+
agentkit secret set TELEGRAM_BOT_TOKEN --stdin
|
|
15
|
+
agentkit secret set TELEGRAM_WEBHOOK_SECRET --stdin
|
|
16
|
+
agentkit channels connect telegram support-telegram
|
|
17
|
+
agentkit channels doctor support-telegram
|
|
17
18
|
agentkit channels status support-telegram
|
|
18
19
|
agentkit channels test support-telegram --message "hello"
|
|
19
20
|
agentkit channels deliveries list support-telegram
|
|
20
21
|
```
|
|
21
22
|
|
|
22
|
-
`
|
|
23
|
+
`connect` creates or reuses the hosted channel, validates managed secrets, calls Telegram `setWebhook`, confirms `getWebhookInfo`, runs a synthetic smoke, and prints the human Telegram steps. Use `setup --apply` only when you need to repeat webhook registration without running smoke.
|
|
23
24
|
|
|
24
25
|
Buffer rapid Telegram messages:
|
|
25
26
|
|