@andreprado/agentkit 0.1.0-alpha.2 → 0.1.0-alpha.20

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.
Files changed (135) hide show
  1. package/README.md +67 -6
  2. package/docs/guides/add-channel.md +118 -7
  3. package/docs/guides/add-knowledge.md +144 -0
  4. package/docs/guides/add-managed-composio.md +163 -0
  5. package/docs/guides/add-tool.md +1 -1
  6. package/docs/guides/channel-security.md +97 -32
  7. package/docs/guides/connect-discord.md +178 -0
  8. package/docs/guides/connect-slack.md +126 -0
  9. package/docs/guides/connect-telegram.md +78 -1
  10. package/docs/guides/connect-whatsapp-zapster.md +112 -8
  11. package/docs/guides/create-agent.md +45 -4
  12. package/docs/guides/debug-channel.md +147 -0
  13. package/docs/guides/improve-from-production.md +151 -0
  14. package/docs/guides/prepare-deploy.md +47 -17
  15. package/docs/guides/replay-production-traces.md +72 -0
  16. package/docs/guides/run-evals.md +147 -20
  17. package/docs/guides/security-rules.md +7 -6
  18. package/docs/guides/send-feedback.md +135 -0
  19. package/docs/guides/use-provider.md +27 -3
  20. package/docs/llms-full.txt +303 -55
  21. package/docs/llms.txt +57 -7
  22. package/package.json +2 -5
  23. package/src/cli/args.ts +57 -0
  24. package/src/cli/cloud-client.ts +377 -0
  25. package/src/cli/commands/channels.ts +1315 -0
  26. package/src/cli/commands/feedback.ts +438 -0
  27. package/src/cli/commands/knowledge.ts +136 -0
  28. package/src/cli/commands/transcribe.ts +171 -0
  29. package/src/cli/constants.ts +4 -0
  30. package/src/cli/deploy-chat-ui.ts +535 -0
  31. package/src/cli/deploy-readiness.ts +481 -0
  32. package/src/cli/flags.ts +162 -0
  33. package/src/cli/help.ts +236 -0
  34. package/src/cli/index.ts +1167 -1005
  35. package/src/cli/process.ts +31 -0
  36. package/src/cloud/artifact.ts +139 -0
  37. package/src/cloud/client.ts +80 -0
  38. package/src/cloud/contracts.ts +63 -0
  39. package/src/cloud/index.ts +3 -0
  40. package/src/create-project.ts +21 -6
  41. package/src/index.ts +479 -7
  42. package/src/providers/pi.ts +70 -16
  43. package/src/providers/test.ts +88 -1
  44. package/src/providers/types.ts +7 -0
  45. package/src/runtime/channel-buffer.ts +30 -0
  46. package/src/runtime/channel-test-harness.ts +8 -1
  47. package/src/runtime/channels/discord.ts +896 -0
  48. package/src/runtime/channels/slack.ts +646 -0
  49. package/src/runtime/channels/telegram.ts +466 -23
  50. package/src/runtime/channels/whatsapp-meta.ts +9 -0
  51. package/src/runtime/channels/whatsapp-zapster.ts +677 -40
  52. package/src/runtime/channels.ts +86 -3
  53. package/src/runtime/chat.ts +130 -38
  54. package/src/runtime/config.ts +483 -18
  55. package/src/runtime/core/manifest.ts +103 -5
  56. package/src/runtime/core/targets.ts +5 -5
  57. package/src/runtime/database.ts +93 -2
  58. package/src/runtime/db-commands.ts +9 -0
  59. package/src/runtime/deploy-readiness.ts +46 -4
  60. package/src/runtime/deploy.ts +1 -1
  61. package/src/runtime/dev-server.ts +759 -41
  62. package/src/runtime/env.ts +8 -3
  63. package/src/runtime/evals.ts +589 -43
  64. package/src/runtime/improve.ts +868 -0
  65. package/src/runtime/inspect.ts +194 -4
  66. package/src/runtime/integrations/composio.ts +423 -0
  67. package/src/runtime/knowledge/chunk.ts +333 -0
  68. package/src/runtime/knowledge/config.ts +135 -0
  69. package/src/runtime/knowledge/embeddings.ts +133 -0
  70. package/src/runtime/knowledge/ingest.ts +521 -0
  71. package/src/runtime/knowledge/prompt-policy.ts +30 -0
  72. package/src/runtime/knowledge/retrieve.ts +303 -0
  73. package/src/runtime/knowledge/schema.ts +100 -0
  74. package/src/runtime/knowledge/tool.ts +64 -0
  75. package/src/runtime/knowledge/vector.ts +258 -0
  76. package/src/runtime/prompt-context.ts +141 -0
  77. package/src/runtime/runtime-contract.ts +86 -8
  78. package/src/runtime/skills.ts +95 -0
  79. package/src/runtime/spec.ts +152 -0
  80. package/src/runtime/sync.ts +144 -0
  81. package/src/runtime/targets/cloudflare/build.ts +1430 -185
  82. package/src/runtime/targets/container/server.ts +1 -1
  83. package/src/runtime/targets/vps/deploy.ts +26 -9
  84. package/src/runtime/tool-runner.ts +9 -1
  85. package/src/runtime/tools.ts +128 -2
  86. package/src/runtime/traces.ts +41 -0
  87. package/src/runtime/transcription.ts +483 -0
  88. package/src/storage/sqlite.ts +149 -3
  89. package/src/templates/blank.ts +76 -17
  90. package/src/templates/dentista.ts +1011 -0
  91. package/src/templates/index.ts +2 -0
  92. package/src/templates/skills/agentkit-build-agent/SKILL.md +52 -0
  93. package/src/templates/skills/agentkit-build-agent/templates/appointment-intake.instructions.md +21 -0
  94. package/src/templates/skills/agentkit-build-agent/templates/sales-qualifier.instructions.md +17 -0
  95. package/src/templates/skills/agentkit-build-agent/templates/support-agent.instructions.md +16 -0
  96. package/src/templates/skills/agentkit-capsule/SKILL.md +70 -0
  97. package/src/templates/skills/agentkit-capsule/references/docs-router.md +15 -0
  98. package/src/templates/skills/agentkit-channels/SKILL.md +104 -0
  99. package/src/templates/skills/agentkit-channels/references/channel-buffering.md +65 -0
  100. package/src/templates/skills/agentkit-channels/references/channel-debugging.md +66 -0
  101. package/src/templates/skills/agentkit-channels/references/discord.md +93 -0
  102. package/src/templates/skills/agentkit-channels/references/slack.md +56 -0
  103. package/src/templates/skills/agentkit-channels/references/telegram.md +72 -0
  104. package/src/templates/skills/agentkit-channels/references/whatsapp-zapster.md +77 -0
  105. package/src/templates/skills/agentkit-database/SKILL.md +45 -0
  106. package/src/templates/skills/agentkit-database/templates/appointments.schema.sql +15 -0
  107. package/src/templates/skills/agentkit-database/templates/leads.schema.sql +17 -0
  108. package/src/templates/skills/agentkit-deploy/SKILL.md +50 -0
  109. package/src/templates/skills/agentkit-evals/SKILL.md +109 -0
  110. package/src/templates/skills/agentkit-evals/templates/multi-turn.eval.md +29 -0
  111. package/src/templates/skills/agentkit-evals/templates/no-leak.eval.md +18 -0
  112. package/src/templates/skills/agentkit-evals/templates/smoke.eval.md +18 -0
  113. package/src/templates/skills/agentkit-evals/templates/tool-call.eval.md +27 -0
  114. package/src/templates/skills/agentkit-improve/SKILL.md +86 -0
  115. package/src/templates/skills/agentkit-improve/references/replay-side-effects.md +18 -0
  116. package/src/templates/skills/agentkit-improve/references/trace-packets.md +22 -0
  117. package/src/templates/skills/agentkit-improve/templates/regression.eval.md +18 -0
  118. package/src/templates/skills/agentkit-integrations/SKILL.md +76 -0
  119. package/src/templates/skills/agentkit-knowledge/SKILL.md +43 -0
  120. package/src/templates/skills/agentkit-knowledge/templates/faq.md +14 -0
  121. package/src/templates/skills/agentkit-knowledge/templates/policies.md +14 -0
  122. package/src/templates/skills/agentkit-knowledge/templates/prices.csv +3 -0
  123. package/src/templates/skills/agentkit-prompts/SKILL.md +47 -0
  124. package/src/templates/skills/agentkit-prompts/templates/knowledge-grounded-faq.instructions.md +11 -0
  125. package/src/templates/skills/agentkit-provider/SKILL.md +60 -0
  126. package/src/templates/skills/agentkit-security/SKILL.md +56 -0
  127. package/src/templates/skills/agentkit-tools/SKILL.md +37 -0
  128. package/src/templates/skills/agentkit-tools/examples/database-write.tool.md +35 -0
  129. package/src/templates/skills/agentkit-tools/examples/eval-safe-external-action.tool.md +37 -0
  130. package/src/templates/skills/agentkit-tools/examples/lookup-order.tool.md +46 -0
  131. package/src/templates/skills/agentkit-troubleshooting/SKILL.md +76 -0
  132. package/src/templates/support.ts +77 -18
  133. package/docs/guides/channels-production-handoff.md +0 -99
  134. package/docs/portable-deploy-release-checklist.md +0 -41
  135. package/src/runtime/targets/cloudflare/deploy.ts +0 -5475
@@ -0,0 +1,1315 @@
1
+ import { createHmac } from "node:crypto";
2
+ import { readFile } from "node:fs/promises";
3
+ import { resolve } from "node:path";
4
+
5
+ import type { ParsedArgs } from "../args";
6
+ import { cloudFetch, cloudGet, cloudPost, readCloudError } from "../cloud-client";
7
+ import { parseCloudApiUrl } from "../flags";
8
+ import { loadAgentCapsule } from "../../runtime/config";
9
+ import { readLocalDeployStateIfExists, type LocalDeployState } from "../../runtime/core/deploy-state";
10
+ import { loadCapsuleEnv } from "../../runtime/env";
11
+ import type { AgentChannel } from "../../index";
12
+
13
+ type DeployState = LocalDeployState;
14
+
15
+ type CloudChannel = {
16
+ id: string;
17
+ name: string;
18
+ type: "website" | "telegram" | "whatsapp" | "discord" | "slack";
19
+ provider: "agentkit" | "telegram" | "zapster" | "meta" | "discord" | "slack";
20
+ mode?: "interactions" | "bot";
21
+ status: string;
22
+ webhook_url: string;
23
+ required_secrets: Array<{ name: string; status: string }>;
24
+ buffer?: AgentChannel["buffer"];
25
+ audio?: AgentChannel["audio"];
26
+ };
27
+
28
+ type CloudDelivery = {
29
+ id: string;
30
+ channel_id: string;
31
+ direction: string;
32
+ status: string;
33
+ provider: {
34
+ event_id?: string;
35
+ };
36
+ received_at: string;
37
+ updated_at?: string;
38
+ error?: {
39
+ code: string;
40
+ message: string;
41
+ };
42
+ events?: Array<{
43
+ state: string;
44
+ message: string;
45
+ created_at: string;
46
+ }>;
47
+ };
48
+
49
+ type CloudChannelBuffer = {
50
+ channel_id: string;
51
+ deploy_id: string;
52
+ conversation_id: string;
53
+ job_id?: string;
54
+ delivery_id?: string;
55
+ buffered_delivery_ids: string[];
56
+ buffered_message_count: number;
57
+ buffered_char_count: number;
58
+ status: string;
59
+ first_received_at: string;
60
+ last_received_at: string;
61
+ available_at?: string;
62
+ age_ms: number;
63
+ stale: boolean;
64
+ message_preview: string;
65
+ };
66
+
67
+ type ChannelBufferActionResponse = {
68
+ buffer: CloudChannelBuffer | null;
69
+ job: {
70
+ id: string;
71
+ status: string;
72
+ conversation_id: string;
73
+ } | null;
74
+ cleared?: boolean;
75
+ retried?: boolean;
76
+ };
77
+
78
+ type ChannelSetupResponse = {
79
+ channel: CloudChannel;
80
+ setup?: {
81
+ provider: "telegram";
82
+ webhook_url: string;
83
+ status: "configured";
84
+ bot_username?: string;
85
+ get_webhook_info?: {
86
+ url: string;
87
+ pending_update_count: number;
88
+ };
89
+ };
90
+ };
91
+
92
+ type ChannelDoctorReport = {
93
+ channel: CloudChannel;
94
+ checks: Array<{
95
+ name: string;
96
+ status: "ok" | "warn" | "fail" | "unknown";
97
+ message: string;
98
+ fix?: string;
99
+ }>;
100
+ buffers: CloudChannelBuffer[];
101
+ last_delivery: {
102
+ inbound: CloudDelivery | null;
103
+ outbound: CloudDelivery | null;
104
+ };
105
+ probable_error: string | null;
106
+ suggested_fix: string | null;
107
+ };
108
+
109
+ export async function handleChannelsCommand(args: ParsedArgs): Promise<void> {
110
+ await loadLocalEnvIntoProcess(process.cwd());
111
+
112
+ const [subcommand, first, second, third] = args.positional;
113
+
114
+ if (subcommand === "list") {
115
+ const deployState = await readDeployStateIfExists(process.cwd());
116
+
117
+ if (deployState) {
118
+ const channels = await cloudGet<{ channels: CloudChannel[] }>(
119
+ parseCloudApiUrl(args.flags.api),
120
+ `/v1/deploys/${encodeURIComponent(deployState.deploy_id)}/channels`,
121
+ );
122
+ printChannelsTable(channels.channels);
123
+ return;
124
+ }
125
+
126
+ const capsule = await loadAgentCapsule(process.cwd());
127
+ printChannelsTable(
128
+ (capsule.config.channels ?? []).map((channel) => ({
129
+ id: "",
130
+ name: channel.name,
131
+ type: channel.type,
132
+ provider: channel.provider,
133
+ ...(channel.type === "discord" ? { mode: channel.mode } : {}),
134
+ status: "local_config",
135
+ webhook_url: "",
136
+ required_secrets: channel.secrets.map((name) => ({ name, status: "missing" })),
137
+ })),
138
+ );
139
+ return;
140
+ }
141
+
142
+ if (subcommand === "add") {
143
+ const type = first;
144
+ const name = second;
145
+
146
+ if (!type || !name || !isCliChannelType(type)) {
147
+ throw new Error("Usage: agentkit channels add <website|telegram|whatsapp|discord|slack> <name> [--provider zapster|meta] [--mode interactions|bot]");
148
+ }
149
+
150
+ const response = await createHostedChannel(type, name, args.flags.provider, args.flags.mode, args.flags.api);
151
+ printChannelStatus(response.channel);
152
+ printSecretRepairHint(response.channel);
153
+ return;
154
+ }
155
+
156
+ if (subcommand === "connect") {
157
+ const type = first;
158
+ const name = second;
159
+
160
+ if (!type || !name || !isCliChannelType(type)) {
161
+ throw new Error("Usage: agentkit channels connect <website|telegram|whatsapp|discord|slack> <name> [--provider zapster|meta] [--mode interactions|bot]");
162
+ }
163
+
164
+ const created = await createHostedChannel(type, name, args.flags.provider, args.flags.mode, args.flags.api);
165
+ let channel = created.channel;
166
+ printChannelStatus(channel);
167
+ printSecretRepairHint(channel);
168
+ console.log("Channel resource: created_or_updated");
169
+
170
+ if (hasMissingRequiredSecrets(channel)) {
171
+ console.log("Secrets: missing");
172
+ console.log("");
173
+ console.log("Connect blocked: set the missing managed secrets, then rerun this same command.");
174
+ console.log("Run: npm run agentkit -- secret sync --from-local");
175
+ process.exitCode = 1;
176
+ return;
177
+ }
178
+
179
+ console.log("Secrets: validated");
180
+
181
+ if (channel.type === "telegram") {
182
+ const setup = await applyTelegramWebhook(channel, args.flags.api);
183
+ channel = setup.channel ?? channel;
184
+ console.log("Telegram setWebhook: OK");
185
+ console.log("Telegram webhook confirmed.");
186
+ if (setup.setup?.get_webhook_info) {
187
+ console.log(`Telegram pending updates: ${setup.setup.get_webhook_info.pending_update_count}`);
188
+ }
189
+
190
+ const smoke = await runConnectSmoke(() => runChannelSmoke(channel, args.flags.api, args.flags.message));
191
+ printTelegramHumanNextStep(channel.name, setup.setup?.bot_username);
192
+ if (!smoke) {
193
+ process.exitCode = 1;
194
+ }
195
+ return;
196
+ }
197
+
198
+ if (channel.type === "discord") {
199
+ const smoke = await runConnectSmoke(() => runChannelSmoke(channel, args.flags.api, args.flags.message));
200
+ printDiscordHumanNextStep(channel);
201
+ if (!smoke) {
202
+ process.exitCode = 1;
203
+ }
204
+ return;
205
+ }
206
+
207
+ if (channel.type === "slack") {
208
+ const smoke = await runConnectSmoke(() => runChannelSmoke(channel, args.flags.api, args.flags.message));
209
+ printSlackHumanNextStep(channel);
210
+ if (!smoke) {
211
+ process.exitCode = 1;
212
+ }
213
+ return;
214
+ }
215
+
216
+ if (channel.type === "whatsapp" && channel.provider === "zapster") {
217
+ const smoke = await runConnectSmoke(() => runChannelSmoke(channel, args.flags.api, args.flags.message));
218
+ console.log(`Next human step: paste ${channel.webhook_url} into ${channel.provider}'s webhook settings.`);
219
+ if (!smoke) {
220
+ process.exitCode = 1;
221
+ }
222
+ } else if (channel.type === "whatsapp") {
223
+ console.log("Smoke: skipped until the provider webhook is configured.");
224
+ console.log(`Next human step: paste ${channel.webhook_url} into ${channel.provider}'s webhook settings.`);
225
+ } else {
226
+ const smoke = await runConnectSmoke(() => runChannelSmoke(channel, args.flags.api, args.flags.message));
227
+ console.log(`Next human step: use ${channel.webhook_url} from your website client.`);
228
+ if (!smoke) {
229
+ process.exitCode = 1;
230
+ }
231
+ }
232
+ return;
233
+ }
234
+
235
+ if (subcommand === "setup") {
236
+ if (!first) {
237
+ throw new Error("Usage: agentkit channels setup <name> [--apply]");
238
+ }
239
+
240
+ const channel = await resolveCloudChannelByName(first, args.flags.api);
241
+ printChannelStatus(channel);
242
+ if (channel.type === "telegram") {
243
+ if (args.flags.apply) {
244
+ const setup = await applyTelegramWebhook(channel, args.flags.api);
245
+ console.log("Telegram webhook confirmed.");
246
+ printChannelStatus(setup.channel ?? channel);
247
+ printTelegramHumanNextStep(channel.name, setup.setup?.bot_username);
248
+ } else {
249
+ console.log(`Setup: call Telegram setWebhook with ${channel.webhook_url}`);
250
+ console.log("Use --apply to mutate Telegram webhook settings.");
251
+ }
252
+ } else if (channel.type === "whatsapp") {
253
+ console.log(`Next step: paste this URL into ${channel.provider}'s webhook settings.`);
254
+ } else if (channel.type === "discord") {
255
+ printDiscordHumanNextStep(channel);
256
+ } else if (channel.type === "slack") {
257
+ printSlackHumanNextStep(channel);
258
+ } else {
259
+ console.log(`Website endpoint: ${channel.webhook_url}`);
260
+ console.log("Use this endpoint from the AgentKit website client or a signed server-to-server test.");
261
+ }
262
+ return;
263
+ }
264
+
265
+ if (subcommand === "status") {
266
+ if (!first) {
267
+ throw new Error("Usage: agentkit channels status <name>");
268
+ }
269
+
270
+ const channel = await resolveCloudChannelByName(first, args.flags.api);
271
+ printChannelStatus(channel);
272
+ await printChannelDeliverySummary(channel, args.flags.api);
273
+ return;
274
+ }
275
+
276
+ if (subcommand === "doctor") {
277
+ if (!first) {
278
+ throw new Error("Usage: agentkit channels doctor <name>");
279
+ }
280
+
281
+ const channel = await resolveCloudChannelByName(first, args.flags.api);
282
+ const report = await cloudGet<{ doctor: ChannelDoctorReport }>(
283
+ parseCloudApiUrl(args.flags.api),
284
+ `/v1/channels/${encodeURIComponent(channel.id)}/doctor`,
285
+ );
286
+ printChannelDoctor(report.doctor);
287
+
288
+ if (report.doctor.checks.some((check) => check.status === "fail")) {
289
+ process.exitCode = 1;
290
+ }
291
+ return;
292
+ }
293
+
294
+ if (subcommand === "test") {
295
+ if (!first) {
296
+ throw new Error('Usage: agentkit channels test <name> [--message "hello"] [--fixture <path>]');
297
+ }
298
+
299
+ const channel = await resolveCloudChannelByName(first, args.flags.api);
300
+ const apiUrl = parseCloudApiUrl(args.flags.api);
301
+ const body = await channelTestBody(channel, args.flags.message, args.flags.fixture);
302
+ const signed = await runServerSignedChannelTest(channel, apiUrl, body);
303
+ const response = signed.response;
304
+ const payload = signed.payload;
305
+
306
+ console.log(JSON.stringify(payload, null, 2));
307
+ if (!response.ok) {
308
+ process.exitCode = 1;
309
+ }
310
+ return;
311
+ }
312
+
313
+ if (subcommand === "test-audio") {
314
+ if (!first) {
315
+ throw new Error("Usage: agentkit channels test-audio <name> [--fixture voice-note|audio-file|<path>]");
316
+ }
317
+
318
+ const channel = await resolveCloudChannelByName(first, args.flags.api);
319
+ const apiUrl = parseCloudApiUrl(args.flags.api);
320
+ const body = await channelAudioTestBody(channel, args.flags.fixture);
321
+ const signed = await runServerSignedChannelTest(channel, apiUrl, body);
322
+ const response = signed.response;
323
+ const payload = signed.payload;
324
+
325
+ console.log(JSON.stringify(payload, null, 2));
326
+ if (!response.ok) {
327
+ process.exitCode = 1;
328
+ }
329
+ return;
330
+ }
331
+
332
+ if (subcommand === "deliveries") {
333
+ if (first === "list") {
334
+ if (!second) {
335
+ throw new Error("Usage: agentkit channels deliveries list <name>");
336
+ }
337
+
338
+ const channel = await resolveCloudChannelByName(second, args.flags.api);
339
+ const response = await cloudGet<{ deliveries: CloudDelivery[] }>(
340
+ parseCloudApiUrl(args.flags.api),
341
+ `/v1/channels/${encodeURIComponent(channel.id)}/deliveries`,
342
+ );
343
+
344
+ if (response.deliveries.length === 0) {
345
+ console.log("No channel deliveries found.");
346
+ return;
347
+ }
348
+
349
+ console.log("ID\tCHANNEL\tDIRECTION\tSTATUS\tPROVIDER_EVENT\tRECEIVED");
350
+ for (const delivery of response.deliveries) {
351
+ console.log(
352
+ [
353
+ delivery.id,
354
+ delivery.channel_id,
355
+ delivery.direction,
356
+ delivery.status,
357
+ delivery.provider.event_id ?? "",
358
+ delivery.received_at,
359
+ ].join("\t"),
360
+ );
361
+ }
362
+ return;
363
+ }
364
+
365
+ if (first === "show") {
366
+ if (!second) {
367
+ throw new Error("Usage: agentkit channels deliveries show <delivery-id>");
368
+ }
369
+
370
+ const response = await cloudGet<{ delivery: CloudDelivery }>(
371
+ parseCloudApiUrl(args.flags.api),
372
+ `/v1/channel-deliveries/${encodeURIComponent(second)}`,
373
+ );
374
+ console.log(JSON.stringify(response.delivery, null, 2));
375
+ return;
376
+ }
377
+ }
378
+
379
+ if (subcommand === "buffers") {
380
+ if (first === "list") {
381
+ if (!second) {
382
+ throw new Error("Usage: agentkit channels buffers list <name>");
383
+ }
384
+
385
+ const channel = await resolveCloudChannelByName(second, args.flags.api);
386
+ const response = await cloudGet<{ buffers: CloudChannelBuffer[] }>(
387
+ parseCloudApiUrl(args.flags.api),
388
+ `/v1/channels/${encodeURIComponent(channel.id)}/buffers`,
389
+ );
390
+ printChannelBuffers(response.buffers);
391
+ return;
392
+ }
393
+
394
+ if (first === "show") {
395
+ if (!second) {
396
+ throw new Error("Usage: agentkit channels buffers show [<name>] <conversation-id>");
397
+ }
398
+
399
+ const target = await resolveChannelBufferTarget(second, third, args.flags.api);
400
+ const response = await cloudGet<{ buffer: CloudChannelBuffer }>(
401
+ parseCloudApiUrl(args.flags.api),
402
+ `/v1/channels/${encodeURIComponent(target.channel.id)}/buffers/${encodeURIComponent(target.conversationId)}`,
403
+ );
404
+ console.log(JSON.stringify(response.buffer, null, 2));
405
+ return;
406
+ }
407
+
408
+ if (first === "flush" || first === "clear" || first === "retry") {
409
+ if (!second) {
410
+ throw new Error(`Usage: agentkit channels buffers ${first} [<name>] <conversation-id>`);
411
+ }
412
+
413
+ const target = await resolveChannelBufferTarget(second, third, args.flags.api);
414
+ const response = await cloudPost<ChannelBufferActionResponse>(
415
+ parseCloudApiUrl(args.flags.api),
416
+ `/v1/channels/${encodeURIComponent(target.channel.id)}/buffers/${encodeURIComponent(target.conversationId)}/${first}`,
417
+ {},
418
+ );
419
+ printChannelBufferAction(first, response);
420
+ return;
421
+ }
422
+ }
423
+
424
+ throw new Error(
425
+ "Usage: agentkit channels list | add | connect | setup | status | doctor | test | test-audio | deliveries list | deliveries show | buffers list | buffers show | buffers flush | buffers clear | buffers retry",
426
+ );
427
+ }
428
+
429
+ async function createHostedChannel(
430
+ type: "website" | "telegram" | "whatsapp" | "discord" | "slack",
431
+ name: string,
432
+ providerFlag: string | boolean | undefined,
433
+ modeFlag: string | boolean | undefined,
434
+ apiFlag: string | boolean | undefined,
435
+ ): Promise<{ channel: CloudChannel }> {
436
+ const deployState = await readDeployStateRequired(process.cwd());
437
+ const provider = channelProviderForCli(type, providerFlag);
438
+ const localChannel = await readLocalChannelConfig(process.cwd(), type, provider, name);
439
+ const mode = discordModeForCli(type, modeFlag, localChannel);
440
+ const payload = {
441
+ name,
442
+ type,
443
+ provider,
444
+ ...(mode ? { mode } : {}),
445
+ secrets: localChannel?.secrets ?? defaultChannelSecretsForCli(type, provider, mode),
446
+ ...(localChannel?.limits ? { limits: channelLimitsForApi(localChannel.limits) } : {}),
447
+ ...(localChannel?.buffer ? { buffer: localChannel.buffer } : {}),
448
+ ...(localChannel?.audio ? { audio: localChannel.audio } : {}),
449
+ };
450
+
451
+ return cloudPost<{ channel: CloudChannel }>(
452
+ parseCloudApiUrl(apiFlag),
453
+ `/v1/deploys/${encodeURIComponent(deployState.deploy_id)}/channels`,
454
+ payload,
455
+ );
456
+ }
457
+
458
+ async function runChannelSmoke(
459
+ channel: CloudChannel,
460
+ apiFlag: string | boolean | undefined,
461
+ messageFlag: string | boolean | undefined,
462
+ ): Promise<{ status: string; delivery_id?: string }> {
463
+ const body = await channelTestBody(channel, messageFlag, undefined);
464
+ const { response, payload } = await runServerSignedChannelTest(channel, parseCloudApiUrl(apiFlag), body);
465
+
466
+ if (!response.ok) {
467
+ throw new Error(readCloudError(payload, response.status));
468
+ }
469
+
470
+ if (isRecord(payload) && typeof payload.status === "string") {
471
+ return {
472
+ status: payload.status,
473
+ ...(typeof payload.delivery_id === "string" ? { delivery_id: payload.delivery_id } : {}),
474
+ };
475
+ }
476
+
477
+ return { status: "unknown" };
478
+ }
479
+
480
+ async function runConnectSmoke(
481
+ run: () => Promise<{ status: string; delivery_id?: string }>,
482
+ ): Promise<{ status: string; delivery_id?: string } | null> {
483
+ try {
484
+ const smoke = await run();
485
+ console.log(`Synthetic smoke: ${smoke.status}${smoke.delivery_id ? ` (${smoke.delivery_id})` : ""}`);
486
+ return smoke;
487
+ } catch (error) {
488
+ console.log("Synthetic smoke: failed");
489
+ console.log(` ${formatCommandError(error)}`);
490
+ return null;
491
+ }
492
+ }
493
+
494
+ async function runServerSignedChannelTest(
495
+ channel: CloudChannel,
496
+ apiUrl: string,
497
+ rawBody: string,
498
+ ): Promise<{ response: Response; payload: unknown }> {
499
+ const response = await cloudFetch(apiUrl, `/v1/channels/${encodeURIComponent(channel.id)}/test`, {
500
+ method: "POST",
501
+ headers: { "Content-Type": "application/json" },
502
+ body: JSON.stringify({ raw_body: rawBody }),
503
+ });
504
+ const payload = await response.json().catch(() => null);
505
+
506
+ if (!response.ok && isUnsupportedChannelTestRoute(payload)) {
507
+ return runLocallySignedChannelTest(channel, apiUrl, rawBody);
508
+ }
509
+
510
+ return { response, payload };
511
+ }
512
+
513
+ async function runLocallySignedChannelTest(
514
+ channel: CloudChannel,
515
+ apiUrl: string,
516
+ rawBody: string,
517
+ ): Promise<{ response: Response; payload: unknown }> {
518
+ const webhookUrl = new URL(channel.webhook_url);
519
+
520
+ if (channel.required_secrets.some((secret) => secret.name === "ZAPSTER_WEBHOOK_TOKEN")) {
521
+ webhookUrl.searchParams.set("token", process.env.ZAPSTER_WEBHOOK_TOKEN ?? "");
522
+ }
523
+
524
+ const path = `${webhookUrl.pathname}${webhookUrl.search}`;
525
+ const headers = channelTestHeaders(channel, rawBody);
526
+ const response = await cloudFetch(apiUrl, path, {
527
+ method: "POST",
528
+ headers,
529
+ body: rawBody,
530
+ });
531
+ const payload = await response.json().catch(() => null);
532
+
533
+ return { response, payload };
534
+ }
535
+
536
+ function isUnsupportedChannelTestRoute(payload: unknown): boolean {
537
+ return (
538
+ isRecord(payload) &&
539
+ isRecord(payload.error) &&
540
+ (payload.error.code === "not_found" ||
541
+ payload.error.code === "channel_store_not_configured" ||
542
+ payload.error.code === "channel_delivery_not_found")
543
+ );
544
+ }
545
+
546
+ async function applyTelegramWebhook(
547
+ channel: CloudChannel,
548
+ apiFlag: string | boolean | undefined,
549
+ ): Promise<ChannelSetupResponse> {
550
+ try {
551
+ return await cloudPost<ChannelSetupResponse>(
552
+ parseCloudApiUrl(apiFlag),
553
+ `/v1/channels/${encodeURIComponent(channel.id)}/setup`,
554
+ {},
555
+ );
556
+ } catch (error) {
557
+ if (!shouldFallbackToLocalTelegramSetup(error)) {
558
+ throw error;
559
+ }
560
+ }
561
+
562
+ const botToken = process.env.TELEGRAM_BOT_TOKEN;
563
+ const secretToken = process.env.TELEGRAM_WEBHOOK_SECRET;
564
+
565
+ if (!botToken || !secretToken) {
566
+ throw new Error("TELEGRAM_BOT_TOKEN and TELEGRAM_WEBHOOK_SECRET must be set to apply Telegram setup.");
567
+ }
568
+
569
+ let response: Response;
570
+ const apiBaseUrl = telegramApiBaseUrlFromEnv();
571
+
572
+ try {
573
+ response = await fetch(new URL(`/bot${botToken}/setWebhook`, apiBaseUrl).href, {
574
+ method: "POST",
575
+ headers: { "Content-Type": "application/json" },
576
+ body: JSON.stringify({
577
+ url: channel.webhook_url,
578
+ secret_token: secretToken,
579
+ allowed_updates: ["message"],
580
+ }),
581
+ });
582
+ } catch {
583
+ throw new Error("Telegram setWebhook request failed before Telegram returned a response.");
584
+ }
585
+
586
+ const payload = await response.json().catch(() => ({}));
587
+
588
+ if (!response.ok || !isTelegramOk(payload)) {
589
+ throw new Error(readTelegramSetupError(payload, response.status));
590
+ }
591
+
592
+ const webhookInfo = await fetch(new URL(`/bot${botToken}/getWebhookInfo`, apiBaseUrl).href)
593
+ .then((telegramResponse) => telegramResponse.json())
594
+ .catch(() => null);
595
+ const webhookUrl =
596
+ webhookInfo &&
597
+ typeof webhookInfo === "object" &&
598
+ "result" in webhookInfo &&
599
+ webhookInfo.result &&
600
+ typeof webhookInfo.result === "object" &&
601
+ "url" in webhookInfo.result &&
602
+ typeof webhookInfo.result.url === "string"
603
+ ? webhookInfo.result.url
604
+ : "";
605
+
606
+ if (webhookUrl !== channel.webhook_url) {
607
+ throw new Error(`Telegram getWebhookInfo returned ${webhookUrl || "no webhook URL"} instead of ${channel.webhook_url}.`);
608
+ }
609
+
610
+ return {
611
+ channel: {
612
+ ...channel,
613
+ status: "connected",
614
+ },
615
+ setup: {
616
+ provider: "telegram",
617
+ webhook_url: channel.webhook_url,
618
+ status: "configured",
619
+ get_webhook_info: {
620
+ url: webhookUrl,
621
+ pending_update_count: 0,
622
+ },
623
+ },
624
+ };
625
+ }
626
+
627
+ function shouldFallbackToLocalTelegramSetup(error: unknown): boolean {
628
+ if (!(error instanceof Error)) {
629
+ return false;
630
+ }
631
+
632
+ return /Unknown AgentKit Cloud route|does not support channel delivery records yet|does not support channel resources yet/.test(
633
+ error.message,
634
+ );
635
+ }
636
+
637
+ function isTelegramOk(payload: unknown): payload is { ok: true } {
638
+ return Boolean(payload && typeof payload === "object" && "ok" in payload && payload.ok === true);
639
+ }
640
+
641
+ function readTelegramSetupError(payload: unknown, status: number): string {
642
+ if (payload && typeof payload === "object" && "description" in payload && typeof payload.description === "string") {
643
+ return `Telegram setWebhook failed: ${payload.description}`;
644
+ }
645
+
646
+ return `Telegram setWebhook failed with HTTP ${status}.`;
647
+ }
648
+
649
+ function telegramApiBaseUrlFromEnv(): string {
650
+ return process.env.AGENTKIT_TELEGRAM_API_BASE_URL ?? "https://api.telegram.org";
651
+ }
652
+
653
+ async function loadLocalEnvIntoProcess(cwd: string): Promise<void> {
654
+ const env = await loadCapsuleEnv(cwd, process.env);
655
+
656
+ for (const [key, value] of Object.entries(env)) {
657
+ if (value !== undefined && process.env[key] === undefined) {
658
+ process.env[key] = value;
659
+ }
660
+ }
661
+ }
662
+
663
+ function printTelegramHumanNextStep(channelName: string, botUsername: string | undefined): void {
664
+ console.log("");
665
+ console.log("Next human step:");
666
+ console.log(` Open ${botUsername ? `@${botUsername}` : "your Telegram bot"} in Telegram`);
667
+ console.log(" Send /start");
668
+ console.log(' Send "hello"');
669
+ console.log(` Then run: npm run agentkit -- channels deliveries list ${channelName}`);
670
+ }
671
+
672
+ function printDiscordHumanNextStep(channel: CloudChannel): void {
673
+ console.log("");
674
+ console.log("Next human step:");
675
+
676
+ if (channel.mode === "bot") {
677
+ console.log(" Open the Discord Developer Portal for your application");
678
+ console.log(" Bot page: enable Message Content Intent");
679
+ console.log(" Installation page: install the app into your server with bot permissions");
680
+ console.log(" Grant the bot View Channel, Read Message History, and Send Messages in the channels it should answer");
681
+ console.log(' Send "hello" in an allowed server channel');
682
+ console.log(` Then run: npm run agentkit -- channels deliveries list ${channel.name}`);
683
+ return;
684
+ }
685
+
686
+ console.log(" Open the Discord Developer Portal for your application");
687
+ console.log(` Paste ${channel.webhook_url} into Interactions Endpoint URL`);
688
+ console.log(" Register a slash command with a string option named message, text, prompt, question, query, or input");
689
+ console.log(` Then run: npm run agentkit -- channels deliveries list ${channel.name}`);
690
+ }
691
+
692
+ function printSlackHumanNextStep(channel: CloudChannel): void {
693
+ console.log("");
694
+ console.log("Next human step:");
695
+ console.log(" Open the Slack app configuration");
696
+ console.log(" Event Subscriptions: enable events");
697
+ console.log(` Request URL: paste ${channel.webhook_url}`);
698
+ console.log(" Subscribe to bot events: app_mention and message.im");
699
+ console.log(" OAuth scopes: app_mentions:read, im:history, chat:write");
700
+ console.log(" Install or reinstall the app to the workspace");
701
+ console.log(' Mention the app with "hello" or send it a direct message');
702
+ console.log(` Then run: npm run agentkit -- channels deliveries list ${channel.name}`);
703
+ }
704
+
705
+ function isRecord(value: unknown): value is Record<string, unknown> {
706
+ return Boolean(value && typeof value === "object" && !Array.isArray(value));
707
+ }
708
+
709
+ function formatCommandError(error: unknown): string {
710
+ return error instanceof Error ? error.message : String(error);
711
+ }
712
+
713
+ async function readDeployStateIfExists(cwd: string): Promise<DeployState | null> {
714
+ return readLocalDeployStateIfExists(cwd);
715
+ }
716
+
717
+ function isCliChannelType(value: string): value is "website" | "telegram" | "whatsapp" | "discord" | "slack" {
718
+ return value === "website" || value === "telegram" || value === "whatsapp" || value === "discord" || value === "slack";
719
+ }
720
+
721
+ async function readDeployStateRequired(cwd: string): Promise<DeployState> {
722
+ const state = await readDeployStateIfExists(cwd);
723
+
724
+ if (!state?.deploy_id) {
725
+ throw new Error("No .agentkit/deploy.json found. Run agentkit deploy before creating hosted channels.");
726
+ }
727
+
728
+ return state;
729
+ }
730
+
731
+ function printChannelsTable(channels: CloudChannel[]): void {
732
+ if (channels.length === 0) {
733
+ console.log("No channels found.");
734
+ return;
735
+ }
736
+
737
+ console.log("NAME\tTYPE\tPROVIDER\tSTATUS\tLAST_EVENT");
738
+ for (const channel of channels) {
739
+ console.log([channel.name, channel.type, channel.provider, channel.status, ""].join("\t"));
740
+ }
741
+ }
742
+
743
+ function printChannelStatus(channel: CloudChannel): void {
744
+ console.log(`Channel: ${channel.name}`);
745
+ if (channel.id) {
746
+ console.log(`ID: ${channel.id}`);
747
+ }
748
+ console.log(`Type: ${channel.type}`);
749
+ console.log(`Provider: ${channel.provider}`);
750
+ if (channel.type === "discord") {
751
+ console.log(`Mode: ${channel.mode ?? "interactions"}`);
752
+ }
753
+ console.log(`Status: ${channel.status}`);
754
+ if (channel.webhook_url) {
755
+ console.log(`Webhook URL: ${channel.webhook_url}`);
756
+ }
757
+ console.log("Required secrets:");
758
+ if (
759
+ channel.required_secrets.length === 0 &&
760
+ defaultChannelSecretsForCli(channel.type, channel.provider, channel.mode).length > 0
761
+ ) {
762
+ console.log(
763
+ ` control-plane channel metadata bug: required_secrets is empty for ${channel.type}/${channel.provider}`,
764
+ );
765
+ } else {
766
+ for (const secret of channel.required_secrets) {
767
+ console.log(` ${secret.name}: ${secret.status}`);
768
+ }
769
+ }
770
+ if (channel.buffer) {
771
+ if (channel.buffer.mode === "off") {
772
+ console.log("Buffer: off");
773
+ } else {
774
+ console.log(
775
+ `Buffer: debounce quiet=${channel.buffer.quietWindowMs}ms max_wait=${channel.buffer.maxWaitMs}ms max_messages=${channel.buffer.maxMessages} max_chars=${channel.buffer.maxChars}`,
776
+ );
777
+ }
778
+ }
779
+ }
780
+
781
+ function printSecretRepairHint(channel: CloudChannel): void {
782
+ if (
783
+ channel.required_secrets.length > 0 ||
784
+ defaultChannelSecretsForCli(channel.type, channel.provider, channel.mode).length === 0
785
+ ) {
786
+ return;
787
+ }
788
+
789
+ console.log(
790
+ "Hint: control-plane channel metadata bug. Rerun add/connect against the updated control-plane to repair required_secrets.",
791
+ );
792
+ }
793
+
794
+ function hasMissingRequiredSecrets(channel: CloudChannel): boolean {
795
+ const expected = defaultChannelSecretsForCli(channel.type, channel.provider, channel.mode);
796
+
797
+ if (channel.required_secrets.length === 0 && expected.length > 0) {
798
+ return true;
799
+ }
800
+
801
+ return channel.required_secrets.some((secret) => secret.status !== "set");
802
+ }
803
+
804
+ async function printChannelDeliverySummary(
805
+ channel: CloudChannel,
806
+ apiFlag: string | boolean | undefined,
807
+ ): Promise<void> {
808
+ const response = await cloudGet<{ deliveries: CloudDelivery[] }>(
809
+ parseCloudApiUrl(apiFlag),
810
+ `/v1/channels/${encodeURIComponent(channel.id)}/deliveries`,
811
+ );
812
+ const inbound = [...response.deliveries].reverse().find((delivery) => delivery.direction === "inbound");
813
+ const outbound = [...response.deliveries].reverse().find((delivery) => delivery.direction === "outbound");
814
+
815
+ console.log("Recent delivery:");
816
+ console.log(` Last inbound: ${inbound ? `${inbound.status} ${inbound.received_at}` : "none"}`);
817
+ console.log(` Last outbound: ${outbound ? `${outbound.status} ${outbound.received_at}` : "none"}`);
818
+ console.log("Provider health: not_checked");
819
+ }
820
+
821
+ function printChannelDoctor(report: ChannelDoctorReport): void {
822
+ console.log(`Channel doctor: ${report.channel.name}`);
823
+ console.log("");
824
+ console.log("Checks:");
825
+ for (const check of report.checks) {
826
+ console.log(` ${formatCheckStatus(check.status)} ${check.name}: ${check.message}`);
827
+ if (check.fix) {
828
+ console.log(` Fix: ${check.fix}`);
829
+ }
830
+ }
831
+
832
+ console.log("");
833
+ console.log("Last delivery:");
834
+ console.log(
835
+ ` Inbound: ${report.last_delivery.inbound ? `${report.last_delivery.inbound.status} ${report.last_delivery.inbound.updated_at ?? report.last_delivery.inbound.received_at}` : "none"}`,
836
+ );
837
+ console.log(
838
+ ` Outbound: ${report.last_delivery.outbound ? `${report.last_delivery.outbound.status} ${report.last_delivery.outbound.updated_at ?? report.last_delivery.outbound.received_at}` : "none"}`,
839
+ );
840
+
841
+ if (report.buffers.length > 0) {
842
+ console.log("");
843
+ console.log("Buffers:");
844
+ for (const buffer of report.buffers) {
845
+ console.log(
846
+ ` ${buffer.conversation_id}: ${buffer.status} messages=${buffer.buffered_message_count} stale=${buffer.stale ? "yes" : "no"}`,
847
+ );
848
+ }
849
+ }
850
+
851
+ if (report.probable_error) {
852
+ console.log("");
853
+ console.log(`Probable error: ${report.probable_error}`);
854
+ }
855
+ if (report.suggested_fix) {
856
+ console.log(`Suggested fix: ${report.suggested_fix}`);
857
+ }
858
+ }
859
+
860
+ function formatCheckStatus(status: ChannelDoctorReport["checks"][number]["status"]): string {
861
+ if (status === "ok") {
862
+ return "OK";
863
+ }
864
+
865
+ if (status === "warn") {
866
+ return "WARN";
867
+ }
868
+
869
+ if (status === "fail") {
870
+ return "FAIL";
871
+ }
872
+
873
+ return "UNKNOWN";
874
+ }
875
+
876
+ function printChannelBuffers(buffers: CloudChannelBuffer[]): void {
877
+ if (buffers.length === 0) {
878
+ console.log("No channel buffers found.");
879
+ return;
880
+ }
881
+
882
+ console.log("CONVERSATION\tSTATUS\tMESSAGES\tCHARS\tSTALE\tAVAILABLE_AT\tJOB");
883
+ for (const buffer of buffers) {
884
+ console.log(
885
+ [
886
+ buffer.conversation_id,
887
+ buffer.status,
888
+ String(buffer.buffered_message_count),
889
+ String(buffer.buffered_char_count),
890
+ buffer.stale ? "yes" : "no",
891
+ buffer.available_at ?? "",
892
+ buffer.job_id ?? "",
893
+ ].join("\t"),
894
+ );
895
+ }
896
+ }
897
+
898
+ function printChannelBufferAction(action: string, response: ChannelBufferActionResponse): void {
899
+ const conversationId = response.buffer?.conversation_id ?? response.job?.conversation_id ?? "unknown";
900
+
901
+ if (action === "flush") {
902
+ console.log(
903
+ response.job
904
+ ? `Flushed buffer ${conversationId}; queued job ${response.job.id}.`
905
+ : `No queued buffer was flushed for ${conversationId}.`,
906
+ );
907
+ return;
908
+ }
909
+
910
+ if (action === "clear") {
911
+ console.log(
912
+ response.cleared
913
+ ? `Cleared buffer ${conversationId}.`
914
+ : `No queued buffer was cleared for ${conversationId}.`,
915
+ );
916
+ return;
917
+ }
918
+
919
+ console.log(
920
+ response.retried && response.job
921
+ ? `Retried channel job ${response.job.id} for ${conversationId}.`
922
+ : `No failed or dead-lettered job was retried for ${conversationId}.`,
923
+ );
924
+ }
925
+
926
+ async function resolveCloudChannelByName(name: string, apiFlag: string | boolean | undefined): Promise<CloudChannel> {
927
+ const channels = await listCloudChannelsForCurrentDeploy(apiFlag);
928
+ const channel = channels.find((candidate) => candidate.name === name);
929
+
930
+ if (!channel) {
931
+ const deployState = await readDeployStateRequired(process.cwd());
932
+ throw new Error(`Channel "${name}" was not found for deploy ${deployState.deploy_id}.`);
933
+ }
934
+
935
+ return channel;
936
+ }
937
+
938
+ async function listCloudChannelsForCurrentDeploy(apiFlag: string | boolean | undefined): Promise<CloudChannel[]> {
939
+ const deployState = await readDeployStateRequired(process.cwd());
940
+ const response = await cloudGet<{ channels: CloudChannel[] }>(
941
+ parseCloudApiUrl(apiFlag),
942
+ `/v1/deploys/${encodeURIComponent(deployState.deploy_id)}/channels`,
943
+ );
944
+
945
+ return response.channels;
946
+ }
947
+
948
+ async function resolveChannelBufferTarget(
949
+ first: string,
950
+ second: string | undefined,
951
+ apiFlag: string | boolean | undefined,
952
+ ): Promise<{ channel: CloudChannel; conversationId: string }> {
953
+ if (second) {
954
+ return {
955
+ channel: await resolveCloudChannelByName(first, apiFlag),
956
+ conversationId: second,
957
+ };
958
+ }
959
+
960
+ const conversationId = first;
961
+ const channels = await listCloudChannelsForCurrentDeploy(apiFlag);
962
+ const matches: CloudChannel[] = [];
963
+
964
+ for (const channel of channels) {
965
+ const response = await cloudGet<{ buffers: CloudChannelBuffer[] }>(
966
+ parseCloudApiUrl(apiFlag),
967
+ `/v1/channels/${encodeURIComponent(channel.id)}/buffers`,
968
+ );
969
+
970
+ if (response.buffers.some((buffer) => buffer.conversation_id === conversationId)) {
971
+ matches.push(channel);
972
+ }
973
+ }
974
+
975
+ if (matches.length === 1) {
976
+ return {
977
+ channel: matches[0],
978
+ conversationId,
979
+ };
980
+ }
981
+
982
+ if (matches.length > 1) {
983
+ throw new Error(
984
+ `Conversation ${conversationId} has buffers in multiple channels. Use: agentkit channels buffers show <name> ${conversationId}`,
985
+ );
986
+ }
987
+
988
+ throw new Error(`No channel buffer found for conversation ${conversationId}.`);
989
+ }
990
+
991
+ function channelProviderForCli(type: string, providerFlag: string | boolean | undefined): string {
992
+ if (type === "website") {
993
+ return "agentkit";
994
+ }
995
+
996
+ if (type === "telegram") {
997
+ return "telegram";
998
+ }
999
+
1000
+ if (type === "discord") {
1001
+ return "discord";
1002
+ }
1003
+
1004
+ if (type === "slack") {
1005
+ if (providerFlag !== undefined) {
1006
+ throw new Error("--provider is not supported for Slack channels.");
1007
+ }
1008
+
1009
+ return "slack";
1010
+ }
1011
+
1012
+ const provider = providerFlag ?? "zapster";
1013
+
1014
+ if (provider !== "zapster" && provider !== "meta") {
1015
+ throw new Error('WhatsApp provider must be "zapster" or "meta".');
1016
+ }
1017
+
1018
+ return provider;
1019
+ }
1020
+
1021
+ function discordModeForCli(
1022
+ type: string,
1023
+ modeFlag: string | boolean | undefined,
1024
+ localChannel: AgentChannel | null,
1025
+ ): "interactions" | "bot" | undefined {
1026
+ if (type !== "discord") {
1027
+ if (modeFlag !== undefined) {
1028
+ throw new Error("--mode is only supported for Discord channels.");
1029
+ }
1030
+
1031
+ return undefined;
1032
+ }
1033
+
1034
+ if (modeFlag !== undefined) {
1035
+ if (modeFlag !== "interactions" && modeFlag !== "bot") {
1036
+ throw new Error('Discord mode must be "interactions" or "bot".');
1037
+ }
1038
+
1039
+ return modeFlag;
1040
+ }
1041
+
1042
+ return localChannel?.type === "discord" ? localChannel.mode : "interactions";
1043
+ }
1044
+
1045
+ function defaultChannelSecretsForCli(type: string, provider: string, mode?: "interactions" | "bot"): string[] {
1046
+ if (type === "website") {
1047
+ return ["AGENTKIT_WEBSITE_CHANNEL_TOKEN"];
1048
+ }
1049
+
1050
+ if (type === "telegram") {
1051
+ return ["TELEGRAM_BOT_TOKEN", "TELEGRAM_WEBHOOK_SECRET"];
1052
+ }
1053
+
1054
+ if (type === "whatsapp" && provider === "zapster") {
1055
+ return ["ZAPSTER_API_KEY", "ZAPSTER_INSTANCE_ID", "ZAPSTER_WEBHOOK_ID"];
1056
+ }
1057
+
1058
+ if (type === "whatsapp" && provider === "meta") {
1059
+ return ["META_WHATSAPP_ACCESS_TOKEN", "META_WHATSAPP_APP_SECRET", "META_WHATSAPP_VERIFY_TOKEN"];
1060
+ }
1061
+
1062
+ if (type === "discord") {
1063
+ return mode === "bot" ? ["DISCORD_BOT_TOKEN"] : ["DISCORD_PUBLIC_KEY"];
1064
+ }
1065
+
1066
+ if (type === "slack") {
1067
+ return ["SLACK_BOT_TOKEN", "SLACK_SIGNING_SECRET"];
1068
+ }
1069
+
1070
+ return [];
1071
+ }
1072
+
1073
+ async function readLocalChannelConfig(
1074
+ cwd: string,
1075
+ type: string,
1076
+ provider: string,
1077
+ name: string,
1078
+ ): Promise<AgentChannel | null> {
1079
+ const capsule = await loadAgentCapsule(cwd);
1080
+ return (
1081
+ (capsule.config.channels ?? []).find(
1082
+ (channel) => channel.name === name && channel.type === type && channel.provider === provider,
1083
+ ) ?? null
1084
+ );
1085
+ }
1086
+
1087
+ function channelLimitsForApi(limits: NonNullable<AgentChannel["limits"]>): {
1088
+ messages_per_day?: number;
1089
+ usd_per_day?: number;
1090
+ } {
1091
+ return {
1092
+ ...(limits.messagesPerDay ? { messages_per_day: limits.messagesPerDay } : {}),
1093
+ ...(limits.usdPerDay ? { usd_per_day: limits.usdPerDay } : {}),
1094
+ };
1095
+ }
1096
+
1097
+ async function channelTestBody(
1098
+ channel: CloudChannel,
1099
+ messageFlag: string | boolean | undefined,
1100
+ fixtureFlag: string | boolean | undefined,
1101
+ ): Promise<string> {
1102
+ if (typeof fixtureFlag === "string") {
1103
+ return readFile(resolve(process.cwd(), fixtureFlag), "utf8");
1104
+ }
1105
+
1106
+ const message = typeof messageFlag === "string" ? messageFlag : "hello";
1107
+
1108
+ if (channel.type === "telegram") {
1109
+ return JSON.stringify({
1110
+ update_id: Date.now(),
1111
+ message: {
1112
+ message_id: 1,
1113
+ chat: { id: 456, type: "private" },
1114
+ from: { id: 123, first_name: "AgentKit" },
1115
+ text: message,
1116
+ },
1117
+ });
1118
+ }
1119
+
1120
+ if (channel.type === "whatsapp") {
1121
+ return JSON.stringify({
1122
+ event_id: `evt_${Date.now()}`,
1123
+ instance_id: "local",
1124
+ message: {
1125
+ id: `msg_${Date.now()}`,
1126
+ from: "5511999999999",
1127
+ type: "text",
1128
+ text: { body: message },
1129
+ },
1130
+ });
1131
+ }
1132
+
1133
+ if (channel.type === "discord") {
1134
+ if (channel.mode === "bot") {
1135
+ return JSON.stringify({
1136
+ id: `discord_message_${Date.now()}`,
1137
+ guild_id: "agentkit_guild",
1138
+ channel_id: "agentkit_channel",
1139
+ author: {
1140
+ id: "agentkit_user",
1141
+ username: "agentkit",
1142
+ },
1143
+ member: {
1144
+ nick: "AgentKit",
1145
+ },
1146
+ content: message,
1147
+ mentions: [],
1148
+ timestamp: new Date().toISOString(),
1149
+ });
1150
+ }
1151
+
1152
+ return JSON.stringify({
1153
+ id: `discord_interaction_${Date.now()}`,
1154
+ application_id: "agentkit_discord_app",
1155
+ type: 2,
1156
+ data: {
1157
+ id: "agentkit_command",
1158
+ name: "ask",
1159
+ type: 1,
1160
+ options: [
1161
+ {
1162
+ name: "message",
1163
+ type: 3,
1164
+ value: message,
1165
+ },
1166
+ ],
1167
+ },
1168
+ guild_id: "agentkit_guild",
1169
+ channel_id: "agentkit_channel",
1170
+ member: {
1171
+ user: {
1172
+ id: "agentkit_user",
1173
+ username: "agentkit",
1174
+ },
1175
+ },
1176
+ token: `agentkit_interaction_token_${Date.now()}`,
1177
+ version: 1,
1178
+ });
1179
+ }
1180
+
1181
+ if (channel.type === "slack") {
1182
+ const now = Date.now();
1183
+
1184
+ return JSON.stringify({
1185
+ token: "deprecated-verification-token",
1186
+ team_id: "T_AGENTKIT",
1187
+ api_app_id: "A_AGENTKIT",
1188
+ type: "event_callback",
1189
+ event_id: `Ev${now}`,
1190
+ event_time: Math.floor(now / 1000),
1191
+ event: {
1192
+ type: "app_mention",
1193
+ user: "UAGENTKITUSER",
1194
+ text: `<@UAGENTKITBOT> ${message}`,
1195
+ ts: `${Math.floor(now / 1000)}.000100`,
1196
+ channel: "C_AGENTKIT",
1197
+ event_ts: `${Math.floor(now / 1000)}.000100`,
1198
+ },
1199
+ authorizations: [],
1200
+ is_ext_shared_channel: false,
1201
+ });
1202
+ }
1203
+
1204
+ return JSON.stringify({
1205
+ event_id: `web_${Date.now()}`,
1206
+ visitor_id: "visitor_test",
1207
+ message: {
1208
+ id: `msg_${Date.now()}`,
1209
+ text: message,
1210
+ },
1211
+ });
1212
+ }
1213
+
1214
+ async function channelAudioTestBody(
1215
+ channel: CloudChannel,
1216
+ fixtureFlag: string | boolean | undefined,
1217
+ ): Promise<string> {
1218
+ if (typeof fixtureFlag === "string" && isExplicitFixturePath(fixtureFlag)) {
1219
+ return readFile(resolve(process.cwd(), fixtureFlag), "utf8");
1220
+ }
1221
+
1222
+ const fixture = typeof fixtureFlag === "string" ? fixtureFlag : "voice-note";
1223
+
1224
+ if (channel.type === "telegram") {
1225
+ if (fixture !== "voice-note" && fixture !== "telegram-voice-note" && fixture !== "telegram-audio-message") {
1226
+ throw new Error('Telegram audio smoke supports --fixture voice-note or --fixture telegram-voice-note.');
1227
+ }
1228
+
1229
+ return JSON.stringify({
1230
+ update_id: Date.now(),
1231
+ message: {
1232
+ message_id: 1,
1233
+ date: Math.floor(Date.now() / 1000),
1234
+ chat: { id: 456, type: "private" },
1235
+ from: { id: 123, first_name: "AgentKit" },
1236
+ voice: {
1237
+ file_id: `agentkit_voice_${Date.now()}`,
1238
+ file_unique_id: `agentkit_voice_unique_${Date.now()}`,
1239
+ duration: 12,
1240
+ mime_type: "audio/ogg",
1241
+ file_size: 42,
1242
+ },
1243
+ },
1244
+ });
1245
+ }
1246
+
1247
+ if (channel.type === "whatsapp" && channel.provider === "zapster") {
1248
+ if (fixture !== "voice-note" && fixture !== "audio-file" && fixture !== "zapster-audio" && fixture !== "zapster-audio-message") {
1249
+ throw new Error('Zapster audio smoke supports --fixture voice-note, --fixture audio-file, or --fixture zapster-audio.');
1250
+ }
1251
+
1252
+ return JSON.stringify({
1253
+ event_id: `zap_evt_audio_${Date.now()}`,
1254
+ instance_id: "local",
1255
+ message: {
1256
+ id: `wamid.audio.${Date.now()}`,
1257
+ from: "5511999999999",
1258
+ type: "audio",
1259
+ audio: {
1260
+ id: `media_audio_${Date.now()}`,
1261
+ url: `https://api.zapsterapi.com/media/audio/media_audio_${Date.now()}.ogg`,
1262
+ mime_type: "audio/ogg",
1263
+ duration: 12,
1264
+ file_size: 42,
1265
+ ...(fixture === "voice-note" ? { ptt: "true" } : {}),
1266
+ },
1267
+ },
1268
+ });
1269
+ }
1270
+
1271
+ throw new Error(`Audio smoke is not supported for ${channel.type}/${channel.provider}.`);
1272
+ }
1273
+
1274
+ function isExplicitFixturePath(value: string): boolean {
1275
+ return value.includes("/") || value.startsWith(".") || value.endsWith(".json");
1276
+ }
1277
+
1278
+ function channelTestHeaders(channel: CloudChannel, rawBody: string): Headers {
1279
+ const headers = new Headers({ "Content-Type": "application/json" });
1280
+
1281
+ if (channel.type === "website") {
1282
+ const token = process.env.AGENTKIT_WEBSITE_CHANNEL_TOKEN;
1283
+
1284
+ if (token) {
1285
+ headers.set("Authorization", `Bearer ${token}`);
1286
+ }
1287
+ }
1288
+
1289
+ if (channel.type === "telegram") {
1290
+ headers.set("X-Telegram-Bot-Api-Secret-Token", process.env.TELEGRAM_WEBHOOK_SECRET ?? "");
1291
+ }
1292
+
1293
+ if (channel.type === "whatsapp" && channel.provider === "zapster") {
1294
+ headers.set("X-Instance-ID", process.env.ZAPSTER_INSTANCE_ID ?? "");
1295
+ headers.set("X-Webhook-ID", process.env.ZAPSTER_WEBHOOK_ID ?? "");
1296
+ headers.set("X-Message-ID", `cli_msg_${Date.now()}`);
1297
+ headers.set("X-Attempt-Count", "1");
1298
+ headers.set("User-Agent", "Zapsterapi/agentkit-cli");
1299
+ }
1300
+
1301
+ if (channel.type === "slack") {
1302
+ const signingSecret = process.env.SLACK_SIGNING_SECRET;
1303
+
1304
+ if (signingSecret) {
1305
+ const timestamp = String(Math.floor(Date.now() / 1000));
1306
+ const signature = createHmac("sha256", signingSecret)
1307
+ .update(`v0:${timestamp}:${rawBody}`)
1308
+ .digest("hex");
1309
+ headers.set("X-Slack-Request-Timestamp", timestamp);
1310
+ headers.set("X-Slack-Signature", `v0=${signature}`);
1311
+ }
1312
+ }
1313
+
1314
+ return headers;
1315
+ }