@andreprado/agentkit 0.1.0-alpha.8 → 0.1.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.
Files changed (117) hide show
  1. package/README.md +18 -1
  2. package/docs/guides/add-channel.md +251 -7
  3. package/docs/guides/add-knowledge.md +10 -0
  4. package/docs/guides/add-managed-composio.md +165 -0
  5. package/docs/guides/add-tool.md +10 -3
  6. package/docs/guides/channel-security.md +162 -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 +61 -1
  10. package/docs/guides/connect-whatsapp-evolution.md +121 -0
  11. package/docs/guides/connect-whatsapp-uazapi.md +139 -0
  12. package/docs/guides/connect-whatsapp-zapster.md +119 -16
  13. package/docs/guides/create-agent.md +31 -4
  14. package/docs/guides/debug-channel.md +159 -0
  15. package/docs/guides/improve-from-production.md +151 -0
  16. package/docs/guides/prepare-deploy.md +32 -14
  17. package/docs/guides/replay-production-traces.md +72 -0
  18. package/docs/guides/run-evals.md +95 -25
  19. package/docs/guides/security-rules.md +9 -5
  20. package/docs/guides/send-feedback.md +135 -0
  21. package/docs/guides/use-jev.md +67 -0
  22. package/docs/guides/use-provider.md +70 -3
  23. package/docs/llms-full.txt +295 -25
  24. package/docs/llms.txt +54 -7
  25. package/package.json +3 -7
  26. package/src/cli/args.ts +23 -2
  27. package/src/cli/cloud-client.ts +121 -9
  28. package/src/cli/commands/channels.ts +1185 -50
  29. package/src/cli/commands/feedback.ts +438 -0
  30. package/src/cli/commands/provider.ts +47 -0
  31. package/src/cli/commands/transcribe.ts +171 -0
  32. package/src/cli/deploy-chat-ui.ts +232 -18
  33. package/src/cli/deploy-readiness.ts +227 -14
  34. package/src/cli/help.ts +68 -8
  35. package/src/cli/index.ts +740 -35
  36. package/src/cli/new-command.ts +41 -0
  37. package/src/cloud/client.ts +4 -3
  38. package/src/cloud/contracts.ts +1 -1
  39. package/src/create-project.ts +18 -35
  40. package/src/index.ts +565 -11
  41. package/src/providers/codex-auth.ts +111 -0
  42. package/src/providers/pi.ts +88 -19
  43. package/src/providers/test.ts +36 -0
  44. package/src/providers/types.ts +8 -0
  45. package/src/runtime/channel-test-harness.ts +21 -1
  46. package/src/runtime/channels/discord.ts +896 -0
  47. package/src/runtime/channels/generic-webhook.ts +974 -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-evolution.ts +1357 -0
  51. package/src/runtime/channels/whatsapp-meta.ts +9 -0
  52. package/src/runtime/channels/whatsapp-uazapi.ts +1323 -0
  53. package/src/runtime/channels/whatsapp-zapster.ts +674 -40
  54. package/src/runtime/channels.ts +89 -4
  55. package/src/runtime/chat.ts +70 -44
  56. package/src/runtime/config.ts +489 -20
  57. package/src/runtime/core/manifest.ts +75 -5
  58. package/src/runtime/core/targets.ts +5 -5
  59. package/src/runtime/deploy-readiness.ts +34 -4
  60. package/src/runtime/dev-server.ts +639 -39
  61. package/src/runtime/env.ts +8 -3
  62. package/src/runtime/evals.ts +445 -74
  63. package/src/runtime/improve.ts +868 -0
  64. package/src/runtime/inspect.ts +173 -4
  65. package/src/runtime/integrations/composio.ts +425 -0
  66. package/src/runtime/knowledge/retrieve.ts +25 -5
  67. package/src/runtime/knowledge/schema.ts +45 -1
  68. package/src/runtime/prompt-context.ts +141 -0
  69. package/src/runtime/runtime-contract.ts +71 -7
  70. package/src/runtime/skills.ts +95 -0
  71. package/src/runtime/targets/cloudflare/build.ts +1010 -208
  72. package/src/runtime/targets/container/server.ts +1 -1
  73. package/src/runtime/targets/vps/deploy.ts +26 -9
  74. package/src/runtime/tool-runner.ts +9 -1
  75. package/src/runtime/tools.ts +26 -2
  76. package/src/runtime/transcription.ts +483 -0
  77. package/src/storage/sqlite.ts +7 -2
  78. package/src/templates/blank.ts +37 -9
  79. package/src/templates/dentista.ts +40 -14
  80. package/src/templates/skills/agentkit-build-agent/SKILL.md +34 -5
  81. package/src/templates/skills/agentkit-build-agent/templates/appointment-intake.instructions.md +2 -1
  82. package/src/templates/skills/agentkit-capsule/SKILL.md +32 -3
  83. package/src/templates/skills/agentkit-capsule/references/docs-router.md +2 -2
  84. package/src/templates/skills/agentkit-channels/SKILL.md +71 -6
  85. package/src/templates/skills/agentkit-channels/references/channel-buffering.md +9 -2
  86. package/src/templates/skills/agentkit-channels/references/channel-debugging.md +34 -5
  87. package/src/templates/skills/agentkit-channels/references/discord.md +93 -0
  88. package/src/templates/skills/agentkit-channels/references/slack.md +56 -0
  89. package/src/templates/skills/agentkit-channels/references/telegram.md +39 -4
  90. package/src/templates/skills/agentkit-channels/references/whatsapp-evolution.md +57 -0
  91. package/src/templates/skills/agentkit-channels/references/whatsapp-uazapi.md +54 -0
  92. package/src/templates/skills/agentkit-channels/references/whatsapp-zapster.md +42 -8
  93. package/src/templates/skills/agentkit-database/SKILL.md +11 -0
  94. package/src/templates/skills/agentkit-deploy/SKILL.md +9 -1
  95. package/src/templates/skills/agentkit-evals/SKILL.md +77 -13
  96. package/src/templates/skills/agentkit-evals/templates/multi-turn.eval.md +13 -6
  97. package/src/templates/skills/agentkit-evals/templates/no-leak.eval.md +8 -4
  98. package/src/templates/skills/agentkit-evals/templates/smoke.eval.md +8 -4
  99. package/src/templates/skills/agentkit-evals/templates/tool-call.eval.md +16 -7
  100. package/src/templates/skills/agentkit-improve/SKILL.md +96 -0
  101. package/src/templates/skills/agentkit-improve/references/replay-side-effects.md +18 -0
  102. package/src/templates/skills/agentkit-improve/references/trace-packets.md +22 -0
  103. package/src/templates/skills/agentkit-improve/templates/regression.eval.md +18 -0
  104. package/src/templates/skills/agentkit-integrations/SKILL.md +98 -0
  105. package/src/templates/skills/agentkit-knowledge/SKILL.md +4 -1
  106. package/src/templates/skills/agentkit-prompts/SKILL.md +3 -1
  107. package/src/templates/skills/agentkit-provider/SKILL.md +29 -4
  108. package/src/templates/skills/agentkit-security/SKILL.md +5 -2
  109. package/src/templates/skills/agentkit-tools/SKILL.md +8 -1
  110. package/src/templates/skills/agentkit-tools/examples/eval-safe-external-action.tool.md +8 -8
  111. package/src/templates/skills/agentkit-tools/examples/jev-service-fit.tool.md +110 -0
  112. package/src/templates/skills/agentkit-troubleshooting/SKILL.md +25 -1
  113. package/src/templates/support.ts +42 -12
  114. package/docs/guides/agentkit-skills-architecture.md +0 -471
  115. package/docs/guides/channels-implementation-map.md +0 -243
  116. package/docs/guides/channels-production-handoff.md +0 -101
  117. package/docs/portable-deploy-release-checklist.md +0 -41
@@ -1,11 +1,13 @@
1
+ import { createHmac } from "node:crypto";
1
2
  import { readFile } from "node:fs/promises";
2
3
  import { resolve } from "node:path";
3
4
 
4
5
  import type { ParsedArgs } from "../args";
5
- import { cloudFetch, cloudGet, cloudPost } from "../cloud-client";
6
+ import { cloudFetch, cloudGet, cloudPost, readCloudError } from "../cloud-client";
6
7
  import { parseCloudApiUrl } from "../flags";
7
8
  import { loadAgentCapsule } from "../../runtime/config";
8
9
  import { readLocalDeployStateIfExists, type LocalDeployState } from "../../runtime/core/deploy-state";
10
+ import { loadCapsuleEnv } from "../../runtime/env";
9
11
  import type { AgentChannel } from "../../index";
10
12
 
11
13
  type DeployState = LocalDeployState;
@@ -13,12 +15,18 @@ type DeployState = LocalDeployState;
13
15
  type CloudChannel = {
14
16
  id: string;
15
17
  name: string;
16
- type: "website" | "telegram" | "whatsapp";
17
- provider: "agentkit" | "telegram" | "zapster" | "meta";
18
+ type: "website" | "telegram" | "whatsapp" | "discord" | "slack" | "webhook";
19
+ provider: "agentkit" | "telegram" | "zapster" | "meta" | "uazapi" | "evolution" | "discord" | "slack" | "generic";
20
+ mode?: "interactions" | "bot";
21
+ direction?: "inbound" | "outbound";
18
22
  status: string;
19
23
  webhook_url: string;
20
24
  required_secrets: Array<{ name: string; status: string }>;
21
25
  buffer?: AgentChannel["buffer"];
26
+ audio?: AgentChannel["audio"];
27
+ reply_to?: Extract<AgentChannel, { replyTo?: unknown }>["replyTo"];
28
+ url_secret?: string;
29
+ auth?: Extract<AgentChannel, { type: "webhook"; direction: "outbound" }>["auth"];
22
30
  };
23
31
 
24
32
  type CloudDelivery = {
@@ -30,10 +38,84 @@ type CloudDelivery = {
30
38
  event_id?: string;
31
39
  };
32
40
  received_at: string;
41
+ updated_at?: string;
42
+ error?: {
43
+ code: string;
44
+ message: string;
45
+ };
46
+ events?: Array<{
47
+ state: string;
48
+ message: string;
49
+ created_at: string;
50
+ }>;
51
+ };
52
+
53
+ type CloudChannelBuffer = {
54
+ channel_id: string;
55
+ deploy_id: string;
56
+ conversation_id: string;
57
+ job_id?: string;
58
+ delivery_id?: string;
59
+ buffered_delivery_ids: string[];
60
+ buffered_message_count: number;
61
+ buffered_char_count: number;
62
+ status: string;
63
+ first_received_at: string;
64
+ last_received_at: string;
65
+ available_at?: string;
66
+ age_ms: number;
67
+ stale: boolean;
68
+ message_preview: string;
69
+ };
70
+
71
+ type ChannelBufferActionResponse = {
72
+ buffer: CloudChannelBuffer | null;
73
+ job: {
74
+ id: string;
75
+ status: string;
76
+ conversation_id: string;
77
+ } | null;
78
+ cleared?: boolean;
79
+ retried?: boolean;
80
+ };
81
+
82
+ type ChannelSetupResponse = {
83
+ channel: CloudChannel;
84
+ setup?: {
85
+ provider: "telegram" | "uazapi" | "evolution";
86
+ webhook_url: string;
87
+ status: "configured";
88
+ bot_username?: string;
89
+ webhook_count?: number;
90
+ matched_webhook_id?: string;
91
+ get_webhook_info?: {
92
+ url: string;
93
+ pending_update_count: number;
94
+ };
95
+ };
96
+ };
97
+
98
+ type ChannelDoctorReport = {
99
+ channel: CloudChannel;
100
+ checks: Array<{
101
+ name: string;
102
+ status: "ok" | "warn" | "fail" | "unknown";
103
+ message: string;
104
+ fix?: string;
105
+ }>;
106
+ buffers: CloudChannelBuffer[];
107
+ last_delivery: {
108
+ inbound: CloudDelivery | null;
109
+ outbound: CloudDelivery | null;
110
+ };
111
+ probable_error: string | null;
112
+ suggested_fix: string | null;
33
113
  };
34
114
 
35
115
  export async function handleChannelsCommand(args: ParsedArgs): Promise<void> {
36
- const [subcommand, first, second] = args.positional;
116
+ await loadLocalEnvIntoProcess(process.cwd());
117
+
118
+ const [subcommand, first, second, third] = args.positional;
37
119
 
38
120
  if (subcommand === "list") {
39
121
  const deployState = await readDeployStateIfExists(process.cwd());
@@ -54,6 +136,7 @@ export async function handleChannelsCommand(args: ParsedArgs): Promise<void> {
54
136
  name: channel.name,
55
137
  type: channel.type,
56
138
  provider: channel.provider,
139
+ ...(channel.type === "discord" ? { mode: channel.mode } : {}),
57
140
  status: "local_config",
58
141
  webhook_url: "",
59
142
  required_secrets: channel.secrets.map((name) => ({ name, status: "missing" })),
@@ -66,27 +149,138 @@ export async function handleChannelsCommand(args: ParsedArgs): Promise<void> {
66
149
  const type = first;
67
150
  const name = second;
68
151
 
69
- if (!type || !name || (type !== "website" && type !== "telegram" && type !== "whatsapp")) {
70
- throw new Error("Usage: agentkit channels add <website|telegram|whatsapp> <name> [--provider zapster|meta]");
152
+ if (!type || !name || !isCliChannelType(type)) {
153
+ throw new Error("Usage: agentkit channels add <website|telegram|whatsapp|discord|slack|webhook> <name> [--provider zapster|meta|uazapi|evolution] [--mode interactions|bot]");
71
154
  }
72
155
 
73
- const deployState = await readDeployStateRequired(process.cwd());
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
- );
156
+ const response = await createHostedChannel(type, name, args.flags.provider, args.flags.mode, args.flags.api);
89
157
  printChannelStatus(response.channel);
158
+ printSecretRepairHint(response.channel);
159
+ return;
160
+ }
161
+
162
+ if (subcommand === "connect") {
163
+ const type = first;
164
+ const name = second;
165
+
166
+ if (!type || !name || !isCliChannelType(type)) {
167
+ throw new Error("Usage: agentkit channels connect <website|telegram|whatsapp|discord|slack|webhook> <name> [--provider zapster|meta|uazapi|evolution] [--mode interactions|bot]");
168
+ }
169
+
170
+ const created = await createHostedChannel(type, name, args.flags.provider, args.flags.mode, args.flags.api);
171
+ let channel = created.channel;
172
+ printChannelStatus(channel);
173
+ printSecretRepairHint(channel);
174
+ console.log("Channel resource: created_or_updated");
175
+
176
+ if (hasMissingRequiredSecrets(channel)) {
177
+ console.log("Secrets: missing");
178
+ console.log("");
179
+ console.log("Connect blocked: set the missing managed secrets, then rerun this same command.");
180
+ console.log("Run: npm run agentkit -- secret sync --from-local");
181
+ process.exitCode = 1;
182
+ return;
183
+ }
184
+
185
+ console.log("Secrets: validated");
186
+
187
+ if (channel.type === "telegram") {
188
+ const setup = await applyTelegramWebhook(channel, args.flags.api);
189
+ channel = setup.channel ?? channel;
190
+ console.log("Telegram setWebhook: OK");
191
+ console.log("Telegram webhook confirmed.");
192
+ if (setup.setup?.get_webhook_info) {
193
+ console.log(`Telegram pending updates: ${setup.setup.get_webhook_info.pending_update_count}`);
194
+ }
195
+
196
+ const smoke = await runConnectSmoke(() => runChannelSmoke(channel, args.flags.api, args.flags.message));
197
+ printTelegramHumanNextStep(channel.name, setup.setup?.bot_username);
198
+ if (!smoke) {
199
+ process.exitCode = 1;
200
+ }
201
+ return;
202
+ }
203
+
204
+ if (channel.type === "whatsapp" && channel.provider === "uazapi") {
205
+ const setup = await applyProviderWebhook(channel, args.flags.api);
206
+ channel = setup.channel ?? channel;
207
+ console.log("UAZAPI webhook setup: OK");
208
+ console.log("UAZAPI webhook confirmed.");
209
+ if (setup.setup?.webhook_count !== undefined) {
210
+ console.log(`UAZAPI webhooks: ${setup.setup.webhook_count}`);
211
+ }
212
+
213
+ const smoke = await runConnectSmoke(() => runChannelSmoke(channel, args.flags.api, args.flags.message));
214
+ printUazapiHumanNextStep(channel);
215
+ if (!smoke) {
216
+ process.exitCode = 1;
217
+ }
218
+ return;
219
+ }
220
+
221
+ if (channel.type === "whatsapp" && channel.provider === "evolution") {
222
+ const setup = await applyProviderWebhook(channel, args.flags.api);
223
+ channel = setup.channel ?? channel;
224
+ console.log("Evolution webhook setup: OK");
225
+ console.log("Evolution webhook confirmed.");
226
+
227
+ const smoke = await runConnectSmoke(() => runChannelSmoke(channel, args.flags.api, args.flags.message));
228
+ printEvolutionHumanNextStep(channel);
229
+ if (!smoke) {
230
+ process.exitCode = 1;
231
+ }
232
+ return;
233
+ }
234
+
235
+ if (channel.type === "discord") {
236
+ const smoke = await runConnectSmoke(() => runChannelSmoke(channel, args.flags.api, args.flags.message));
237
+ printDiscordHumanNextStep(channel);
238
+ if (!smoke) {
239
+ process.exitCode = 1;
240
+ }
241
+ return;
242
+ }
243
+
244
+ if (channel.type === "slack") {
245
+ const smoke = await runConnectSmoke(() => runChannelSmoke(channel, args.flags.api, args.flags.message));
246
+ printSlackHumanNextStep(channel);
247
+ if (!smoke) {
248
+ process.exitCode = 1;
249
+ }
250
+ return;
251
+ }
252
+
253
+ if (channel.type === "webhook") {
254
+ if (channel.direction === "outbound") {
255
+ console.log("Outbound webhook: configured");
256
+ console.log(`Next human step: make sure ${channel.url_secret ?? "the webhook URL secret"} is set as a managed secret.`);
257
+ return;
258
+ }
259
+
260
+ const smoke = await runConnectSmoke(() => runChannelSmoke(channel, args.flags.api, args.flags.message));
261
+ printGenericWebhookHumanNextStep(channel);
262
+ if (!smoke) {
263
+ process.exitCode = 1;
264
+ }
265
+ return;
266
+ }
267
+
268
+ if (channel.type === "whatsapp" && channel.provider === "zapster") {
269
+ const smoke = await runConnectSmoke(() => runChannelSmoke(channel, args.flags.api, args.flags.message));
270
+ console.log(`Next human step: paste ${channel.webhook_url} into ${channel.provider}'s webhook settings.`);
271
+ if (!smoke) {
272
+ process.exitCode = 1;
273
+ }
274
+ } else if (channel.type === "whatsapp") {
275
+ console.log("Smoke: skipped until the provider webhook is configured.");
276
+ console.log(`Next human step: paste ${channel.webhook_url} into ${channel.provider}'s webhook settings.`);
277
+ } else {
278
+ const smoke = await runConnectSmoke(() => runChannelSmoke(channel, args.flags.api, args.flags.message));
279
+ console.log(`Next human step: use ${channel.webhook_url} from your website client.`);
280
+ if (!smoke) {
281
+ process.exitCode = 1;
282
+ }
283
+ }
90
284
  return;
91
285
  }
92
286
 
@@ -99,14 +293,46 @@ export async function handleChannelsCommand(args: ParsedArgs): Promise<void> {
99
293
  printChannelStatus(channel);
100
294
  if (channel.type === "telegram") {
101
295
  if (args.flags.apply) {
102
- await applyTelegramWebhook(channel, args.flags.api);
103
- console.log("Telegram webhook updated.");
296
+ const setup = await applyTelegramWebhook(channel, args.flags.api);
297
+ console.log("Telegram webhook confirmed.");
298
+ printChannelStatus(setup.channel ?? channel);
299
+ printTelegramHumanNextStep(channel.name, setup.setup?.bot_username);
104
300
  } else {
105
301
  console.log(`Setup: call Telegram setWebhook with ${channel.webhook_url}`);
106
302
  console.log("Use --apply to mutate Telegram webhook settings.");
107
303
  }
304
+ } else if (channel.type === "whatsapp" && channel.provider === "uazapi") {
305
+ if (args.flags.apply) {
306
+ const setup = await applyProviderWebhook(channel, args.flags.api);
307
+ console.log("UAZAPI webhook confirmed.");
308
+ printChannelStatus(setup.channel ?? channel);
309
+ printUazapiHumanNextStep(setup.channel ?? channel);
310
+ } else {
311
+ console.log(`Setup: configure UAZAPI /webhook with ${channel.webhook_url}`);
312
+ console.log("Use --apply to mutate UAZAPI webhook settings.");
313
+ }
314
+ } else if (channel.type === "whatsapp" && channel.provider === "evolution") {
315
+ if (args.flags.apply) {
316
+ const setup = await applyProviderWebhook(channel, args.flags.api);
317
+ console.log("Evolution webhook confirmed.");
318
+ printChannelStatus(setup.channel ?? channel);
319
+ printEvolutionHumanNextStep(setup.channel ?? channel);
320
+ } else {
321
+ console.log(`Setup: configure Evolution /webhook/set/{instance} with ${channel.webhook_url}`);
322
+ console.log("Use --apply to mutate Evolution webhook settings.");
323
+ }
108
324
  } else if (channel.type === "whatsapp") {
109
325
  console.log(`Next step: paste this URL into ${channel.provider}'s webhook settings.`);
326
+ } else if (channel.type === "discord") {
327
+ printDiscordHumanNextStep(channel);
328
+ } else if (channel.type === "slack") {
329
+ printSlackHumanNextStep(channel);
330
+ } else if (channel.type === "webhook") {
331
+ if (channel.direction === "outbound") {
332
+ console.log("Outbound webhook endpoint: configured from managed secrets.");
333
+ } else {
334
+ printGenericWebhookHumanNextStep(channel);
335
+ }
110
336
  } else {
111
337
  console.log(`Website endpoint: ${channel.webhook_url}`);
112
338
  console.log("Use this endpoint from the AgentKit website client or a signed server-to-server test.");
@@ -125,22 +351,58 @@ export async function handleChannelsCommand(args: ParsedArgs): Promise<void> {
125
351
  return;
126
352
  }
127
353
 
354
+ if (subcommand === "doctor") {
355
+ if (!first) {
356
+ throw new Error("Usage: agentkit channels doctor <name>");
357
+ }
358
+
359
+ const channel = await resolveCloudChannelByName(first, args.flags.api);
360
+ const report = await cloudGet<{ doctor: ChannelDoctorReport }>(
361
+ parseCloudApiUrl(args.flags.api),
362
+ `/v1/channels/${encodeURIComponent(channel.id)}/doctor`,
363
+ );
364
+ printChannelDoctor(report.doctor);
365
+
366
+ if (report.doctor.checks.some((check) => check.status === "fail")) {
367
+ process.exitCode = 1;
368
+ }
369
+ return;
370
+ }
371
+
128
372
  if (subcommand === "test") {
129
373
  if (!first) {
130
374
  throw new Error('Usage: agentkit channels test <name> [--message "hello"] [--fixture <path>]');
131
375
  }
132
376
 
133
377
  const channel = await resolveCloudChannelByName(first, args.flags.api);
378
+ if (channel.type === "webhook" && channel.direction === "outbound") {
379
+ throw new Error("Outbound webhook channels do not have an inbound test endpoint. Test them through a source channel that routes replies to this channel.");
380
+ }
134
381
  const apiUrl = parseCloudApiUrl(args.flags.api);
135
- const body = await channelTestBody(channel, args.flags.message, args.flags.fixture);
136
- const path = new URL(channel.webhook_url).pathname;
137
- const headers = channelTestHeaders(channel);
138
- const response = await cloudFetch(apiUrl, path, {
139
- method: channel.type === "whatsapp" && channel.provider === "meta" ? "POST" : "POST",
140
- headers,
141
- body,
142
- });
143
- const payload = await response.json();
382
+ const signed = channel.type === "whatsapp" && channel.provider === "evolution" && !args.flags.fixture
383
+ ? await runServerGeneratedChannelTest(channel, apiUrl, args.flags.message)
384
+ : await runServerSignedChannelTest(channel, apiUrl, await channelTestBody(channel, args.flags.message, args.flags.fixture));
385
+ const response = signed.response;
386
+ const payload = signed.payload;
387
+
388
+ console.log(JSON.stringify(payload, null, 2));
389
+ if (!response.ok) {
390
+ process.exitCode = 1;
391
+ }
392
+ return;
393
+ }
394
+
395
+ if (subcommand === "test-audio") {
396
+ if (!first) {
397
+ throw new Error("Usage: agentkit channels test-audio <name> [--fixture voice-note|audio-file|<path>]");
398
+ }
399
+
400
+ const channel = await resolveCloudChannelByName(first, args.flags.api);
401
+ const apiUrl = parseCloudApiUrl(args.flags.api);
402
+ const body = await channelAudioTestBody(channel, args.flags.fixture);
403
+ const signed = await runServerSignedChannelTest(channel, apiUrl, body);
404
+ const response = signed.response;
405
+ const payload = signed.payload;
144
406
 
145
407
  console.log(JSON.stringify(payload, null, 2));
146
408
  if (!response.ok) {
@@ -196,22 +458,223 @@ export async function handleChannelsCommand(args: ParsedArgs): Promise<void> {
196
458
  }
197
459
  }
198
460
 
461
+ if (subcommand === "buffers") {
462
+ if (first === "list") {
463
+ if (!second) {
464
+ throw new Error("Usage: agentkit channels buffers list <name>");
465
+ }
466
+
467
+ const channel = await resolveCloudChannelByName(second, args.flags.api);
468
+ const response = await cloudGet<{ buffers: CloudChannelBuffer[] }>(
469
+ parseCloudApiUrl(args.flags.api),
470
+ `/v1/channels/${encodeURIComponent(channel.id)}/buffers`,
471
+ );
472
+ printChannelBuffers(response.buffers);
473
+ return;
474
+ }
475
+
476
+ if (first === "show") {
477
+ if (!second) {
478
+ throw new Error("Usage: agentkit channels buffers show [<name>] <conversation-id>");
479
+ }
480
+
481
+ const target = await resolveChannelBufferTarget(second, third, args.flags.api);
482
+ const response = await cloudGet<{ buffer: CloudChannelBuffer }>(
483
+ parseCloudApiUrl(args.flags.api),
484
+ `/v1/channels/${encodeURIComponent(target.channel.id)}/buffers/${encodeURIComponent(target.conversationId)}`,
485
+ );
486
+ console.log(JSON.stringify(response.buffer, null, 2));
487
+ return;
488
+ }
489
+
490
+ if (first === "flush" || first === "clear" || first === "retry") {
491
+ if (!second) {
492
+ throw new Error(`Usage: agentkit channels buffers ${first} [<name>] <conversation-id>`);
493
+ }
494
+
495
+ const target = await resolveChannelBufferTarget(second, third, args.flags.api);
496
+ const response = await cloudPost<ChannelBufferActionResponse>(
497
+ parseCloudApiUrl(args.flags.api),
498
+ `/v1/channels/${encodeURIComponent(target.channel.id)}/buffers/${encodeURIComponent(target.conversationId)}/${first}`,
499
+ {},
500
+ );
501
+ printChannelBufferAction(first, response);
502
+ return;
503
+ }
504
+ }
505
+
199
506
  throw new Error(
200
- "Usage: agentkit channels list | add | setup | status | test | deliveries list | deliveries show",
507
+ "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",
508
+ );
509
+ }
510
+
511
+ async function createHostedChannel(
512
+ type: "website" | "telegram" | "whatsapp" | "discord" | "slack" | "webhook",
513
+ name: string,
514
+ providerFlag: string | boolean | undefined,
515
+ modeFlag: string | boolean | undefined,
516
+ apiFlag: string | boolean | undefined,
517
+ ): Promise<{ channel: CloudChannel }> {
518
+ const deployState = await readDeployStateRequired(process.cwd());
519
+ const provider = channelProviderForCli(type, providerFlag);
520
+ const localChannel = await readLocalChannelConfig(process.cwd(), type, provider, name);
521
+ const mode = discordModeForCli(type, modeFlag, localChannel);
522
+ const payload = {
523
+ name,
524
+ type,
525
+ provider,
526
+ ...(mode ? { mode } : {}),
527
+ secrets: localChannel?.secrets ?? defaultChannelSecretsForCli(type, provider, mode),
528
+ ...(localChannel?.limits ? { limits: channelLimitsForApi(localChannel.limits) } : {}),
529
+ ...(localChannel?.buffer ? { buffer: localChannel.buffer } : {}),
530
+ ...(localChannel?.audio ? { audio: localChannel.audio } : {}),
531
+ ...channelExtraConfigForApi(localChannel),
532
+ };
533
+
534
+ return cloudPost<{ channel: CloudChannel }>(
535
+ parseCloudApiUrl(apiFlag),
536
+ `/v1/deploys/${encodeURIComponent(deployState.deploy_id)}/channels`,
537
+ payload,
538
+ );
539
+ }
540
+
541
+ async function runChannelSmoke(
542
+ channel: CloudChannel,
543
+ apiFlag: string | boolean | undefined,
544
+ messageFlag: string | boolean | undefined,
545
+ ): Promise<{ status: string; delivery_id?: string }> {
546
+ const apiUrl = parseCloudApiUrl(apiFlag);
547
+ const { response, payload } = channel.type === "whatsapp" && channel.provider === "evolution"
548
+ ? await runServerGeneratedChannelTest(channel, apiUrl, messageFlag)
549
+ : await runServerSignedChannelTest(channel, apiUrl, await channelTestBody(channel, messageFlag, undefined));
550
+
551
+ if (!response.ok) {
552
+ throw new Error(readCloudError(payload, response.status));
553
+ }
554
+
555
+ if (isRecord(payload) && typeof payload.status === "string") {
556
+ return {
557
+ status: payload.status,
558
+ ...(typeof payload.delivery_id === "string" ? { delivery_id: payload.delivery_id } : {}),
559
+ };
560
+ }
561
+
562
+ return { status: "unknown" };
563
+ }
564
+
565
+ async function runServerGeneratedChannelTest(
566
+ channel: CloudChannel,
567
+ apiUrl: string,
568
+ messageFlag: string | boolean | undefined,
569
+ ): Promise<{ response: Response; payload: unknown }> {
570
+ const body: Record<string, unknown> = {};
571
+
572
+ if (typeof messageFlag === "string") {
573
+ body.message = messageFlag;
574
+ }
575
+
576
+ const response = await cloudFetch(apiUrl, `/v1/channels/${encodeURIComponent(channel.id)}/test`, {
577
+ method: "POST",
578
+ headers: { "Content-Type": "application/json" },
579
+ body: JSON.stringify(body),
580
+ });
581
+ const payload = await response.json().catch(() => null);
582
+
583
+ if (!response.ok && isUnsupportedChannelTestRoute(payload)) {
584
+ return runLocallySignedChannelTest(
585
+ channel,
586
+ apiUrl,
587
+ await channelTestBody(channel, messageFlag, undefined),
588
+ );
589
+ }
590
+
591
+ return { response, payload };
592
+ }
593
+
594
+ async function runConnectSmoke(
595
+ run: () => Promise<{ status: string; delivery_id?: string }>,
596
+ ): Promise<{ status: string; delivery_id?: string } | null> {
597
+ try {
598
+ const smoke = await run();
599
+ console.log(`Synthetic smoke: ${smoke.status}${smoke.delivery_id ? ` (${smoke.delivery_id})` : ""}`);
600
+ return smoke;
601
+ } catch (error) {
602
+ console.log("Synthetic smoke: failed");
603
+ console.log(` ${formatCommandError(error)}`);
604
+ return null;
605
+ }
606
+ }
607
+
608
+ async function runServerSignedChannelTest(
609
+ channel: CloudChannel,
610
+ apiUrl: string,
611
+ rawBody: string,
612
+ ): Promise<{ response: Response; payload: unknown }> {
613
+ const response = await cloudFetch(apiUrl, `/v1/channels/${encodeURIComponent(channel.id)}/test`, {
614
+ method: "POST",
615
+ headers: { "Content-Type": "application/json" },
616
+ body: JSON.stringify({ raw_body: rawBody }),
617
+ });
618
+ const payload = await response.json().catch(() => null);
619
+
620
+ if (!response.ok && isUnsupportedChannelTestRoute(payload)) {
621
+ return runLocallySignedChannelTest(channel, apiUrl, rawBody);
622
+ }
623
+
624
+ return { response, payload };
625
+ }
626
+
627
+ async function runLocallySignedChannelTest(
628
+ channel: CloudChannel,
629
+ apiUrl: string,
630
+ rawBody: string,
631
+ ): Promise<{ response: Response; payload: unknown }> {
632
+ const webhookUrl = new URL(channel.webhook_url);
633
+
634
+ if (channel.required_secrets.some((secret) => secret.name === "ZAPSTER_WEBHOOK_TOKEN")) {
635
+ webhookUrl.searchParams.set("token", process.env.ZAPSTER_WEBHOOK_TOKEN ?? "");
636
+ }
637
+
638
+ if (channel.required_secrets.some((secret) => secret.name === "UAZAPI_WEBHOOK_TOKEN")) {
639
+ webhookUrl.searchParams.set("token", process.env.UAZAPI_WEBHOOK_TOKEN ?? "");
640
+ }
641
+
642
+ if (channel.required_secrets.some((secret) => secret.name === "EVOLUTION_WEBHOOK_TOKEN")) {
643
+ webhookUrl.searchParams.set("token", process.env.EVOLUTION_WEBHOOK_TOKEN ?? "");
644
+ }
645
+
646
+ const path = `${webhookUrl.pathname}${webhookUrl.search}`;
647
+ const headers = channelTestHeaders(channel, rawBody);
648
+ const response = await cloudFetch(apiUrl, path, {
649
+ method: "POST",
650
+ headers,
651
+ body: rawBody,
652
+ });
653
+ const payload = await response.json().catch(() => null);
654
+
655
+ return { response, payload };
656
+ }
657
+
658
+ function isUnsupportedChannelTestRoute(payload: unknown): boolean {
659
+ return (
660
+ isRecord(payload) &&
661
+ isRecord(payload.error) &&
662
+ (payload.error.code === "not_found" ||
663
+ payload.error.code === "channel_store_not_configured" ||
664
+ payload.error.code === "channel_delivery_not_found")
201
665
  );
202
666
  }
203
667
 
204
668
  async function applyTelegramWebhook(
205
669
  channel: CloudChannel,
206
670
  apiFlag: string | boolean | undefined,
207
- ): Promise<void> {
671
+ ): Promise<ChannelSetupResponse> {
208
672
  try {
209
- await cloudPost(
673
+ return await cloudPost<ChannelSetupResponse>(
210
674
  parseCloudApiUrl(apiFlag),
211
675
  `/v1/channels/${encodeURIComponent(channel.id)}/setup`,
212
676
  {},
213
677
  );
214
- return;
215
678
  } catch (error) {
216
679
  if (!shouldFallbackToLocalTelegramSetup(error)) {
217
680
  throw error;
@@ -226,9 +689,10 @@ async function applyTelegramWebhook(
226
689
  }
227
690
 
228
691
  let response: Response;
692
+ const apiBaseUrl = telegramApiBaseUrlFromEnv();
229
693
 
230
694
  try {
231
- response = await fetch(`https://api.telegram.org/bot${botToken}/setWebhook`, {
695
+ response = await fetch(new URL(`/bot${botToken}/setWebhook`, apiBaseUrl).href, {
232
696
  method: "POST",
233
697
  headers: { "Content-Type": "application/json" },
234
698
  body: JSON.stringify({
@@ -246,6 +710,51 @@ async function applyTelegramWebhook(
246
710
  if (!response.ok || !isTelegramOk(payload)) {
247
711
  throw new Error(readTelegramSetupError(payload, response.status));
248
712
  }
713
+
714
+ const webhookInfo = await fetch(new URL(`/bot${botToken}/getWebhookInfo`, apiBaseUrl).href)
715
+ .then((telegramResponse) => telegramResponse.json())
716
+ .catch(() => null);
717
+ const webhookUrl =
718
+ webhookInfo &&
719
+ typeof webhookInfo === "object" &&
720
+ "result" in webhookInfo &&
721
+ webhookInfo.result &&
722
+ typeof webhookInfo.result === "object" &&
723
+ "url" in webhookInfo.result &&
724
+ typeof webhookInfo.result.url === "string"
725
+ ? webhookInfo.result.url
726
+ : "";
727
+
728
+ if (webhookUrl !== channel.webhook_url) {
729
+ throw new Error(`Telegram getWebhookInfo returned ${webhookUrl || "no webhook URL"} instead of ${channel.webhook_url}.`);
730
+ }
731
+
732
+ return {
733
+ channel: {
734
+ ...channel,
735
+ status: "connected",
736
+ },
737
+ setup: {
738
+ provider: "telegram",
739
+ webhook_url: channel.webhook_url,
740
+ status: "configured",
741
+ get_webhook_info: {
742
+ url: webhookUrl,
743
+ pending_update_count: 0,
744
+ },
745
+ },
746
+ };
747
+ }
748
+
749
+ async function applyProviderWebhook(
750
+ channel: CloudChannel,
751
+ apiFlag: string | boolean | undefined,
752
+ ): Promise<ChannelSetupResponse> {
753
+ return cloudPost<ChannelSetupResponse>(
754
+ parseCloudApiUrl(apiFlag),
755
+ `/v1/channels/${encodeURIComponent(channel.id)}/setup`,
756
+ {},
757
+ );
249
758
  }
250
759
 
251
760
  function shouldFallbackToLocalTelegramSetup(error: unknown): boolean {
@@ -270,10 +779,103 @@ function readTelegramSetupError(payload: unknown, status: number): string {
270
779
  return `Telegram setWebhook failed with HTTP ${status}.`;
271
780
  }
272
781
 
782
+ function telegramApiBaseUrlFromEnv(): string {
783
+ return process.env.AGENTKIT_TELEGRAM_API_BASE_URL ?? "https://api.telegram.org";
784
+ }
785
+
786
+ async function loadLocalEnvIntoProcess(cwd: string): Promise<void> {
787
+ const env = await loadCapsuleEnv(cwd, process.env);
788
+
789
+ for (const [key, value] of Object.entries(env)) {
790
+ if (value !== undefined && process.env[key] === undefined) {
791
+ process.env[key] = value;
792
+ }
793
+ }
794
+ }
795
+
796
+ function printTelegramHumanNextStep(channelName: string, botUsername: string | undefined): void {
797
+ console.log("");
798
+ console.log("Next human step:");
799
+ console.log(` Open ${botUsername ? `@${botUsername}` : "your Telegram bot"} in Telegram`);
800
+ console.log(" Send /start");
801
+ console.log(' Send "hello"');
802
+ console.log(` Then run: npm run agentkit -- channels deliveries list ${channelName}`);
803
+ }
804
+
805
+ function printUazapiHumanNextStep(channel: CloudChannel): void {
806
+ console.log("");
807
+ console.log("Next human step:");
808
+ console.log(" Confirm the UAZAPI instance is connected to WhatsApp");
809
+ console.log(' Send "hello" from a real WhatsApp contact');
810
+ console.log(` Then run: npm run agentkit -- channels deliveries list ${channel.name}`);
811
+ }
812
+
813
+ function printEvolutionHumanNextStep(channel: CloudChannel): void {
814
+ console.log("");
815
+ console.log("Next human step:");
816
+ console.log(" Confirm the Evolution API instance is connected to WhatsApp");
817
+ console.log(' Send "hello" from a real WhatsApp contact');
818
+ console.log(` Then run: npm run agentkit -- channels deliveries list ${channel.name}`);
819
+ }
820
+
821
+ function printDiscordHumanNextStep(channel: CloudChannel): void {
822
+ console.log("");
823
+ console.log("Next human step:");
824
+
825
+ if (channel.mode === "bot") {
826
+ console.log(" Open the Discord Developer Portal for your application");
827
+ console.log(" Bot page: enable Message Content Intent");
828
+ console.log(" Installation page: install the app into your server with bot permissions");
829
+ console.log(" Grant the bot View Channel, Read Message History, and Send Messages in the channels it should answer");
830
+ console.log(' Send "hello" in an allowed server channel');
831
+ console.log(` Then run: npm run agentkit -- channels deliveries list ${channel.name}`);
832
+ return;
833
+ }
834
+
835
+ console.log(" Open the Discord Developer Portal for your application");
836
+ console.log(` Paste ${channel.webhook_url} into Interactions Endpoint URL`);
837
+ console.log(" Register a slash command with a string option named message, text, prompt, question, query, or input");
838
+ console.log(` Then run: npm run agentkit -- channels deliveries list ${channel.name}`);
839
+ }
840
+
841
+ function printSlackHumanNextStep(channel: CloudChannel): void {
842
+ console.log("");
843
+ console.log("Next human step:");
844
+ console.log(" Open the Slack app configuration");
845
+ console.log(" Event Subscriptions: enable events");
846
+ console.log(` Request URL: paste ${channel.webhook_url}`);
847
+ console.log(" Subscribe to bot events: app_mention and message.im");
848
+ console.log(" OAuth scopes: app_mentions:read, im:history, chat:write");
849
+ console.log(" Install or reinstall the app to the workspace");
850
+ console.log(' Mention the app with "hello" or send it a direct message');
851
+ console.log(` Then run: npm run agentkit -- channels deliveries list ${channel.name}`);
852
+ }
853
+
854
+ function printGenericWebhookHumanNextStep(channel: CloudChannel): void {
855
+ console.log("");
856
+ console.log("Next human step:");
857
+ console.log(` Point n8n, Make, Zapier, or your server to ${channel.webhook_url}`);
858
+ console.log(" Send JSON with event_id, external_id, and message");
859
+ console.log(" Authenticate with Authorization: Bearer <AGENTKIT_WEBHOOK_SECRET>");
860
+ console.log(` Then run: npm run agentkit -- channels deliveries list ${channel.name}`);
861
+ }
862
+
863
+ function isRecord(value: unknown): value is Record<string, unknown> {
864
+ return Boolean(value && typeof value === "object" && !Array.isArray(value));
865
+ }
866
+
867
+ function formatCommandError(error: unknown): string {
868
+ return error instanceof Error ? error.message : String(error);
869
+ }
870
+
273
871
  async function readDeployStateIfExists(cwd: string): Promise<DeployState | null> {
274
872
  return readLocalDeployStateIfExists(cwd);
275
873
  }
276
874
 
875
+ function isCliChannelType(value: string): value is "website" | "telegram" | "whatsapp" | "discord" | "slack" | "webhook" {
876
+ return value === "website" || value === "telegram" || value === "whatsapp" || value === "discord" || value === "slack" || value === "webhook";
877
+ }
878
+
277
879
  async function readDeployStateRequired(cwd: string): Promise<DeployState> {
278
880
  const state = await readDeployStateIfExists(cwd);
279
881
 
@@ -303,13 +905,28 @@ function printChannelStatus(channel: CloudChannel): void {
303
905
  }
304
906
  console.log(`Type: ${channel.type}`);
305
907
  console.log(`Provider: ${channel.provider}`);
908
+ if (channel.type === "discord") {
909
+ console.log(`Mode: ${channel.mode ?? "interactions"}`);
910
+ }
911
+ if (channel.type === "webhook" && channel.direction) {
912
+ console.log(`Direction: ${channel.direction}`);
913
+ }
306
914
  console.log(`Status: ${channel.status}`);
307
915
  if (channel.webhook_url) {
308
916
  console.log(`Webhook URL: ${channel.webhook_url}`);
309
917
  }
310
918
  console.log("Required secrets:");
311
- for (const secret of channel.required_secrets) {
312
- console.log(` ${secret.name}: ${secret.status}`);
919
+ if (
920
+ channel.required_secrets.length === 0 &&
921
+ defaultChannelSecretsForCli(channel.type, channel.provider, channel.mode).length > 0
922
+ ) {
923
+ console.log(
924
+ ` control-plane channel metadata bug: required_secrets is empty for ${channel.type}/${channel.provider}`,
925
+ );
926
+ } else {
927
+ for (const secret of channel.required_secrets) {
928
+ console.log(` ${secret.name}: ${secret.status}`);
929
+ }
313
930
  }
314
931
  if (channel.buffer) {
315
932
  if (channel.buffer.mode === "off") {
@@ -322,6 +939,29 @@ function printChannelStatus(channel: CloudChannel): void {
322
939
  }
323
940
  }
324
941
 
942
+ function printSecretRepairHint(channel: CloudChannel): void {
943
+ if (
944
+ channel.required_secrets.length > 0 ||
945
+ defaultChannelSecretsForCli(channel.type, channel.provider, channel.mode).length === 0
946
+ ) {
947
+ return;
948
+ }
949
+
950
+ console.log(
951
+ "Hint: control-plane channel metadata bug. Rerun add/connect against the updated control-plane to repair required_secrets.",
952
+ );
953
+ }
954
+
955
+ function hasMissingRequiredSecrets(channel: CloudChannel): boolean {
956
+ const expected = defaultChannelSecretsForCli(channel.type, channel.provider, channel.mode);
957
+
958
+ if (channel.required_secrets.length === 0 && expected.length > 0) {
959
+ return true;
960
+ }
961
+
962
+ return channel.required_secrets.some((secret) => secret.status !== "set");
963
+ }
964
+
325
965
  async function printChannelDeliverySummary(
326
966
  channel: CloudChannel,
327
967
  apiFlag: string | boolean | undefined,
@@ -339,19 +979,174 @@ async function printChannelDeliverySummary(
339
979
  console.log("Provider health: not_checked");
340
980
  }
341
981
 
982
+ function printChannelDoctor(report: ChannelDoctorReport): void {
983
+ console.log(`Channel doctor: ${report.channel.name}`);
984
+ console.log("");
985
+ console.log("Checks:");
986
+ for (const check of report.checks) {
987
+ console.log(` ${formatCheckStatus(check.status)} ${check.name}: ${check.message}`);
988
+ if (check.fix) {
989
+ console.log(` Fix: ${check.fix}`);
990
+ }
991
+ }
992
+
993
+ console.log("");
994
+ console.log("Last delivery:");
995
+ console.log(
996
+ ` Inbound: ${report.last_delivery.inbound ? `${report.last_delivery.inbound.status} ${report.last_delivery.inbound.updated_at ?? report.last_delivery.inbound.received_at}` : "none"}`,
997
+ );
998
+ console.log(
999
+ ` Outbound: ${report.last_delivery.outbound ? `${report.last_delivery.outbound.status} ${report.last_delivery.outbound.updated_at ?? report.last_delivery.outbound.received_at}` : "none"}`,
1000
+ );
1001
+
1002
+ if (report.buffers.length > 0) {
1003
+ console.log("");
1004
+ console.log("Buffers:");
1005
+ for (const buffer of report.buffers) {
1006
+ console.log(
1007
+ ` ${buffer.conversation_id}: ${buffer.status} messages=${buffer.buffered_message_count} stale=${buffer.stale ? "yes" : "no"}`,
1008
+ );
1009
+ }
1010
+ }
1011
+
1012
+ if (report.probable_error) {
1013
+ console.log("");
1014
+ console.log(`Probable error: ${report.probable_error}`);
1015
+ }
1016
+ if (report.suggested_fix) {
1017
+ console.log(`Suggested fix: ${report.suggested_fix}`);
1018
+ }
1019
+ }
1020
+
1021
+ function formatCheckStatus(status: ChannelDoctorReport["checks"][number]["status"]): string {
1022
+ if (status === "ok") {
1023
+ return "OK";
1024
+ }
1025
+
1026
+ if (status === "warn") {
1027
+ return "WARN";
1028
+ }
1029
+
1030
+ if (status === "fail") {
1031
+ return "FAIL";
1032
+ }
1033
+
1034
+ return "UNKNOWN";
1035
+ }
1036
+
1037
+ function printChannelBuffers(buffers: CloudChannelBuffer[]): void {
1038
+ if (buffers.length === 0) {
1039
+ console.log("No channel buffers found.");
1040
+ return;
1041
+ }
1042
+
1043
+ console.log("CONVERSATION\tSTATUS\tMESSAGES\tCHARS\tSTALE\tAVAILABLE_AT\tJOB");
1044
+ for (const buffer of buffers) {
1045
+ console.log(
1046
+ [
1047
+ buffer.conversation_id,
1048
+ buffer.status,
1049
+ String(buffer.buffered_message_count),
1050
+ String(buffer.buffered_char_count),
1051
+ buffer.stale ? "yes" : "no",
1052
+ buffer.available_at ?? "",
1053
+ buffer.job_id ?? "",
1054
+ ].join("\t"),
1055
+ );
1056
+ }
1057
+ }
1058
+
1059
+ function printChannelBufferAction(action: string, response: ChannelBufferActionResponse): void {
1060
+ const conversationId = response.buffer?.conversation_id ?? response.job?.conversation_id ?? "unknown";
1061
+
1062
+ if (action === "flush") {
1063
+ console.log(
1064
+ response.job
1065
+ ? `Flushed buffer ${conversationId}; queued job ${response.job.id}.`
1066
+ : `No queued buffer was flushed for ${conversationId}.`,
1067
+ );
1068
+ return;
1069
+ }
1070
+
1071
+ if (action === "clear") {
1072
+ console.log(
1073
+ response.cleared
1074
+ ? `Cleared buffer ${conversationId}.`
1075
+ : `No queued buffer was cleared for ${conversationId}.`,
1076
+ );
1077
+ return;
1078
+ }
1079
+
1080
+ console.log(
1081
+ response.retried && response.job
1082
+ ? `Retried channel job ${response.job.id} for ${conversationId}.`
1083
+ : `No failed or dead-lettered job was retried for ${conversationId}.`,
1084
+ );
1085
+ }
1086
+
342
1087
  async function resolveCloudChannelByName(name: string, apiFlag: string | boolean | undefined): Promise<CloudChannel> {
1088
+ const channels = await listCloudChannelsForCurrentDeploy(apiFlag);
1089
+ const channel = channels.find((candidate) => candidate.name === name);
1090
+
1091
+ if (!channel) {
1092
+ const deployState = await readDeployStateRequired(process.cwd());
1093
+ throw new Error(`Channel "${name}" was not found for deploy ${deployState.deploy_id}.`);
1094
+ }
1095
+
1096
+ return channel;
1097
+ }
1098
+
1099
+ async function listCloudChannelsForCurrentDeploy(apiFlag: string | boolean | undefined): Promise<CloudChannel[]> {
343
1100
  const deployState = await readDeployStateRequired(process.cwd());
344
1101
  const response = await cloudGet<{ channels: CloudChannel[] }>(
345
1102
  parseCloudApiUrl(apiFlag),
346
1103
  `/v1/deploys/${encodeURIComponent(deployState.deploy_id)}/channels`,
347
1104
  );
348
- const channel = response.channels.find((candidate) => candidate.name === name);
349
1105
 
350
- if (!channel) {
351
- throw new Error(`Channel "${name}" was not found for deploy ${deployState.deploy_id}.`);
1106
+ return response.channels;
1107
+ }
1108
+
1109
+ async function resolveChannelBufferTarget(
1110
+ first: string,
1111
+ second: string | undefined,
1112
+ apiFlag: string | boolean | undefined,
1113
+ ): Promise<{ channel: CloudChannel; conversationId: string }> {
1114
+ if (second) {
1115
+ return {
1116
+ channel: await resolveCloudChannelByName(first, apiFlag),
1117
+ conversationId: second,
1118
+ };
352
1119
  }
353
1120
 
354
- return channel;
1121
+ const conversationId = first;
1122
+ const channels = await listCloudChannelsForCurrentDeploy(apiFlag);
1123
+ const matches: CloudChannel[] = [];
1124
+
1125
+ for (const channel of channels) {
1126
+ const response = await cloudGet<{ buffers: CloudChannelBuffer[] }>(
1127
+ parseCloudApiUrl(apiFlag),
1128
+ `/v1/channels/${encodeURIComponent(channel.id)}/buffers`,
1129
+ );
1130
+
1131
+ if (response.buffers.some((buffer) => buffer.conversation_id === conversationId)) {
1132
+ matches.push(channel);
1133
+ }
1134
+ }
1135
+
1136
+ if (matches.length === 1) {
1137
+ return {
1138
+ channel: matches[0],
1139
+ conversationId,
1140
+ };
1141
+ }
1142
+
1143
+ if (matches.length > 1) {
1144
+ throw new Error(
1145
+ `Conversation ${conversationId} has buffers in multiple channels. Use: agentkit channels buffers show <name> ${conversationId}`,
1146
+ );
1147
+ }
1148
+
1149
+ throw new Error(`No channel buffer found for conversation ${conversationId}.`);
355
1150
  }
356
1151
 
357
1152
  function channelProviderForCli(type: string, providerFlag: string | boolean | undefined): string {
@@ -363,16 +1158,60 @@ function channelProviderForCli(type: string, providerFlag: string | boolean | un
363
1158
  return "telegram";
364
1159
  }
365
1160
 
1161
+ if (type === "discord") {
1162
+ return "discord";
1163
+ }
1164
+
1165
+ if (type === "slack") {
1166
+ if (providerFlag !== undefined) {
1167
+ throw new Error("--provider is not supported for Slack channels.");
1168
+ }
1169
+
1170
+ return "slack";
1171
+ }
1172
+
1173
+ if (type === "webhook") {
1174
+ if (providerFlag !== undefined) {
1175
+ throw new Error("--provider is not supported for generic webhook channels.");
1176
+ }
1177
+
1178
+ return "generic";
1179
+ }
1180
+
366
1181
  const provider = providerFlag ?? "zapster";
367
1182
 
368
- if (provider !== "zapster" && provider !== "meta") {
369
- throw new Error('WhatsApp provider must be "zapster" or "meta".');
1183
+ if (provider !== "zapster" && provider !== "meta" && provider !== "uazapi" && provider !== "evolution") {
1184
+ throw new Error('WhatsApp provider must be "zapster", "meta", "uazapi", or "evolution".');
370
1185
  }
371
1186
 
372
1187
  return provider;
373
1188
  }
374
1189
 
375
- function defaultChannelSecretsForCli(type: string, provider: string): string[] {
1190
+ function discordModeForCli(
1191
+ type: string,
1192
+ modeFlag: string | boolean | undefined,
1193
+ localChannel: AgentChannel | null,
1194
+ ): "interactions" | "bot" | undefined {
1195
+ if (type !== "discord") {
1196
+ if (modeFlag !== undefined) {
1197
+ throw new Error("--mode is only supported for Discord channels.");
1198
+ }
1199
+
1200
+ return undefined;
1201
+ }
1202
+
1203
+ if (modeFlag !== undefined) {
1204
+ if (modeFlag !== "interactions" && modeFlag !== "bot") {
1205
+ throw new Error('Discord mode must be "interactions" or "bot".');
1206
+ }
1207
+
1208
+ return modeFlag;
1209
+ }
1210
+
1211
+ return localChannel?.type === "discord" ? localChannel.mode : "interactions";
1212
+ }
1213
+
1214
+ function defaultChannelSecretsForCli(type: string, provider: string, mode?: "interactions" | "bot"): string[] {
376
1215
  if (type === "website") {
377
1216
  return ["AGENTKIT_WEBSITE_CHANNEL_TOKEN"];
378
1217
  }
@@ -382,16 +1221,49 @@ function defaultChannelSecretsForCli(type: string, provider: string): string[] {
382
1221
  }
383
1222
 
384
1223
  if (type === "whatsapp" && provider === "zapster") {
385
- return ["ZAPSTER_API_KEY", "ZAPSTER_WEBHOOK_SECRET"];
1224
+ return ["ZAPSTER_API_KEY", "ZAPSTER_INSTANCE_ID", "ZAPSTER_WEBHOOK_ID", "ZAPSTER_WEBHOOK_TOKEN"];
386
1225
  }
387
1226
 
388
1227
  if (type === "whatsapp" && provider === "meta") {
389
1228
  return ["META_WHATSAPP_ACCESS_TOKEN", "META_WHATSAPP_APP_SECRET", "META_WHATSAPP_VERIFY_TOKEN"];
390
1229
  }
391
1230
 
1231
+ if (type === "whatsapp" && provider === "uazapi") {
1232
+ return ["UAZAPI_BASE_URL", "UAZAPI_TOKEN", "UAZAPI_WEBHOOK_TOKEN"];
1233
+ }
1234
+
1235
+ if (type === "whatsapp" && provider === "evolution") {
1236
+ return ["EVOLUTION_API_BASE_URL", "EVOLUTION_API_KEY", "EVOLUTION_INSTANCE_NAME", "EVOLUTION_WEBHOOK_TOKEN"];
1237
+ }
1238
+
1239
+ if (type === "discord") {
1240
+ return mode === "bot" ? ["DISCORD_BOT_TOKEN"] : ["DISCORD_PUBLIC_KEY"];
1241
+ }
1242
+
1243
+ if (type === "slack") {
1244
+ return ["SLACK_BOT_TOKEN", "SLACK_SIGNING_SECRET"];
1245
+ }
1246
+
1247
+ if (type === "webhook") {
1248
+ return ["AGENTKIT_WEBHOOK_SECRET"];
1249
+ }
1250
+
392
1251
  return [];
393
1252
  }
394
1253
 
1254
+ function channelExtraConfigForApi(channel: AgentChannel | null): Record<string, unknown> {
1255
+ if (!channel) {
1256
+ return {};
1257
+ }
1258
+
1259
+ return {
1260
+ ...("direction" in channel && channel.direction ? { direction: channel.direction } : {}),
1261
+ ...(channel.replyTo ? { reply_to: channel.replyTo } : {}),
1262
+ ...("urlSecret" in channel && channel.urlSecret ? { url_secret: channel.urlSecret } : {}),
1263
+ ...("auth" in channel && channel.auth ? { auth: channel.auth } : {}),
1264
+ };
1265
+ }
1266
+
395
1267
  async function readLocalChannelConfig(
396
1268
  cwd: string,
397
1269
  type: string,
@@ -439,6 +1311,45 @@ async function channelTestBody(
439
1311
  });
440
1312
  }
441
1313
 
1314
+ if (channel.type === "whatsapp" && channel.provider === "uazapi") {
1315
+ return JSON.stringify({
1316
+ EventType: "messages",
1317
+ event: "messages",
1318
+ message: {
1319
+ id: `uaz_msg_${Date.now()}`,
1320
+ messageid: `uaz_wamid_${Date.now()}`,
1321
+ chatid: "5511999999999@s.whatsapp.net",
1322
+ sender: "5511999999999@s.whatsapp.net",
1323
+ senderName: "AgentKit",
1324
+ fromMe: false,
1325
+ wasSentByApi: false,
1326
+ messageType: "text",
1327
+ text: message,
1328
+ messageTimestamp: Date.now(),
1329
+ },
1330
+ });
1331
+ }
1332
+
1333
+ if (channel.type === "whatsapp" && channel.provider === "evolution") {
1334
+ return JSON.stringify({
1335
+ event: "MESSAGES_UPSERT",
1336
+ instance: process.env.EVOLUTION_INSTANCE_NAME ?? "local",
1337
+ data: {
1338
+ key: {
1339
+ remoteJid: "5511999999999@s.whatsapp.net",
1340
+ fromMe: false,
1341
+ id: `evo_wamid_${Date.now()}`,
1342
+ },
1343
+ pushName: "AgentKit",
1344
+ messageType: "conversation",
1345
+ message: {
1346
+ conversation: message,
1347
+ },
1348
+ messageTimestamp: Math.floor(Date.now() / 1000),
1349
+ },
1350
+ });
1351
+ }
1352
+
442
1353
  if (channel.type === "whatsapp") {
443
1354
  return JSON.stringify({
444
1355
  event_id: `evt_${Date.now()}`,
@@ -452,6 +1363,85 @@ async function channelTestBody(
452
1363
  });
453
1364
  }
454
1365
 
1366
+ if (channel.type === "discord") {
1367
+ if (channel.mode === "bot") {
1368
+ return JSON.stringify({
1369
+ id: `discord_message_${Date.now()}`,
1370
+ guild_id: "agentkit_guild",
1371
+ channel_id: "agentkit_channel",
1372
+ author: {
1373
+ id: "agentkit_user",
1374
+ username: "agentkit",
1375
+ },
1376
+ member: {
1377
+ nick: "AgentKit",
1378
+ },
1379
+ content: message,
1380
+ mentions: [],
1381
+ timestamp: new Date().toISOString(),
1382
+ });
1383
+ }
1384
+
1385
+ return JSON.stringify({
1386
+ id: `discord_interaction_${Date.now()}`,
1387
+ application_id: "agentkit_discord_app",
1388
+ type: 2,
1389
+ data: {
1390
+ id: "agentkit_command",
1391
+ name: "ask",
1392
+ type: 1,
1393
+ options: [
1394
+ {
1395
+ name: "message",
1396
+ type: 3,
1397
+ value: message,
1398
+ },
1399
+ ],
1400
+ },
1401
+ guild_id: "agentkit_guild",
1402
+ channel_id: "agentkit_channel",
1403
+ member: {
1404
+ user: {
1405
+ id: "agentkit_user",
1406
+ username: "agentkit",
1407
+ },
1408
+ },
1409
+ token: `agentkit_interaction_token_${Date.now()}`,
1410
+ version: 1,
1411
+ });
1412
+ }
1413
+
1414
+ if (channel.type === "slack") {
1415
+ const now = Date.now();
1416
+
1417
+ return JSON.stringify({
1418
+ token: "deprecated-verification-token",
1419
+ team_id: "T_AGENTKIT",
1420
+ api_app_id: "A_AGENTKIT",
1421
+ type: "event_callback",
1422
+ event_id: `Ev${now}`,
1423
+ event_time: Math.floor(now / 1000),
1424
+ event: {
1425
+ type: "app_mention",
1426
+ user: "UAGENTKITUSER",
1427
+ text: `<@UAGENTKITBOT> ${message}`,
1428
+ ts: `${Math.floor(now / 1000)}.000100`,
1429
+ channel: "C_AGENTKIT",
1430
+ event_ts: `${Math.floor(now / 1000)}.000100`,
1431
+ },
1432
+ authorizations: [],
1433
+ is_ext_shared_channel: false,
1434
+ });
1435
+ }
1436
+
1437
+ if (channel.type === "webhook") {
1438
+ return JSON.stringify({
1439
+ event_id: `evt_${Date.now()}`,
1440
+ external_id: "agentkit-webhook-test",
1441
+ message,
1442
+ });
1443
+ }
1444
+
455
1445
  return JSON.stringify({
456
1446
  event_id: `web_${Date.now()}`,
457
1447
  visitor_id: "visitor_test",
@@ -462,7 +1452,127 @@ async function channelTestBody(
462
1452
  });
463
1453
  }
464
1454
 
465
- function channelTestHeaders(channel: CloudChannel): Headers {
1455
+ async function channelAudioTestBody(
1456
+ channel: CloudChannel,
1457
+ fixtureFlag: string | boolean | undefined,
1458
+ ): Promise<string> {
1459
+ if (typeof fixtureFlag === "string" && isExplicitFixturePath(fixtureFlag)) {
1460
+ return readFile(resolve(process.cwd(), fixtureFlag), "utf8");
1461
+ }
1462
+
1463
+ const fixture = typeof fixtureFlag === "string" ? fixtureFlag : "voice-note";
1464
+
1465
+ if (channel.type === "telegram") {
1466
+ if (fixture !== "voice-note" && fixture !== "telegram-voice-note" && fixture !== "telegram-audio-message") {
1467
+ throw new Error('Telegram audio smoke supports --fixture voice-note or --fixture telegram-voice-note.');
1468
+ }
1469
+
1470
+ return JSON.stringify({
1471
+ update_id: Date.now(),
1472
+ message: {
1473
+ message_id: 1,
1474
+ date: Math.floor(Date.now() / 1000),
1475
+ chat: { id: 456, type: "private" },
1476
+ from: { id: 123, first_name: "AgentKit" },
1477
+ voice: {
1478
+ file_id: `agentkit_voice_${Date.now()}`,
1479
+ file_unique_id: `agentkit_voice_unique_${Date.now()}`,
1480
+ duration: 12,
1481
+ mime_type: "audio/ogg",
1482
+ file_size: 42,
1483
+ },
1484
+ },
1485
+ });
1486
+ }
1487
+
1488
+ if (channel.type === "whatsapp" && channel.provider === "zapster") {
1489
+ if (fixture !== "voice-note" && fixture !== "audio-file" && fixture !== "zapster-audio" && fixture !== "zapster-audio-message") {
1490
+ throw new Error('Zapster audio smoke supports --fixture voice-note, --fixture audio-file, or --fixture zapster-audio.');
1491
+ }
1492
+
1493
+ return JSON.stringify({
1494
+ event_id: `zap_evt_audio_${Date.now()}`,
1495
+ instance_id: "local",
1496
+ message: {
1497
+ id: `wamid.audio.${Date.now()}`,
1498
+ from: "5511999999999",
1499
+ type: "audio",
1500
+ audio: {
1501
+ id: `media_audio_${Date.now()}`,
1502
+ url: `https://api.zapsterapi.com/media/audio/media_audio_${Date.now()}.ogg`,
1503
+ mime_type: "audio/ogg",
1504
+ duration: 12,
1505
+ file_size: 42,
1506
+ ...(fixture === "voice-note" ? { ptt: "true" } : {}),
1507
+ },
1508
+ },
1509
+ });
1510
+ }
1511
+
1512
+ if (channel.type === "whatsapp" && channel.provider === "uazapi") {
1513
+ if (fixture !== "voice-note" && fixture !== "audio-file" && fixture !== "uazapi-audio" && fixture !== "uazapi-audio-message") {
1514
+ throw new Error('UAZAPI audio smoke supports --fixture voice-note, --fixture audio-file, or --fixture uazapi-audio.');
1515
+ }
1516
+
1517
+ return JSON.stringify({
1518
+ EventType: "messages",
1519
+ event: "messages",
1520
+ message: {
1521
+ id: `uaz_audio_${Date.now()}`,
1522
+ messageid: `uaz_wamid_audio_${Date.now()}`,
1523
+ chatid: "5511999999999@s.whatsapp.net",
1524
+ sender: "5511999999999@s.whatsapp.net",
1525
+ senderName: "AgentKit",
1526
+ fromMe: false,
1527
+ wasSentByApi: false,
1528
+ messageType: fixture === "voice-note" ? "ptt" : "audio",
1529
+ mimetype: "audio/ogg",
1530
+ duration: 12,
1531
+ file_size: 42,
1532
+ messageTimestamp: Date.now(),
1533
+ },
1534
+ });
1535
+ }
1536
+
1537
+ if (channel.type === "whatsapp" && channel.provider === "evolution") {
1538
+ if (fixture !== "voice-note" && fixture !== "audio-file" && fixture !== "evolution-audio" && fixture !== "evolution-audio-message") {
1539
+ throw new Error(
1540
+ 'Evolution audio smoke supports --fixture voice-note, --fixture audio-file, --fixture evolution-audio, or --fixture evolution-audio-message.',
1541
+ );
1542
+ }
1543
+
1544
+ return JSON.stringify({
1545
+ event: "MESSAGES_UPSERT",
1546
+ instance: process.env.EVOLUTION_INSTANCE_NAME ?? "local",
1547
+ data: {
1548
+ key: {
1549
+ remoteJid: "5511999999999@s.whatsapp.net",
1550
+ fromMe: false,
1551
+ id: `evo_wamid_audio_${Date.now()}`,
1552
+ },
1553
+ pushName: "AgentKit",
1554
+ messageType: "audioMessage",
1555
+ message: {
1556
+ audioMessage: {
1557
+ mimetype: "audio/ogg",
1558
+ seconds: 12,
1559
+ ptt: fixture === "voice-note",
1560
+ fileLength: 42,
1561
+ },
1562
+ },
1563
+ messageTimestamp: Math.floor(Date.now() / 1000),
1564
+ },
1565
+ });
1566
+ }
1567
+
1568
+ throw new Error(`Audio smoke is not supported for ${channel.type}/${channel.provider}.`);
1569
+ }
1570
+
1571
+ function isExplicitFixturePath(value: string): boolean {
1572
+ return value.includes("/") || value.startsWith(".") || value.endsWith(".json");
1573
+ }
1574
+
1575
+ function channelTestHeaders(channel: CloudChannel, rawBody: string): Headers {
466
1576
  const headers = new Headers({ "Content-Type": "application/json" });
467
1577
 
468
1578
  if (channel.type === "website") {
@@ -478,7 +1588,32 @@ function channelTestHeaders(channel: CloudChannel): Headers {
478
1588
  }
479
1589
 
480
1590
  if (channel.type === "whatsapp" && channel.provider === "zapster") {
481
- headers.set("X-Zapster-Webhook-Secret", process.env.ZAPSTER_WEBHOOK_SECRET ?? "");
1591
+ headers.set("X-Instance-ID", process.env.ZAPSTER_INSTANCE_ID ?? "");
1592
+ headers.set("X-Webhook-ID", process.env.ZAPSTER_WEBHOOK_ID ?? "");
1593
+ headers.set("X-Message-ID", `cli_msg_${Date.now()}`);
1594
+ headers.set("X-Attempt-Count", "1");
1595
+ headers.set("User-Agent", "Zapsterapi/agentkit-cli");
1596
+ }
1597
+
1598
+ if (channel.type === "slack") {
1599
+ const signingSecret = process.env.SLACK_SIGNING_SECRET;
1600
+
1601
+ if (signingSecret) {
1602
+ const timestamp = String(Math.floor(Date.now() / 1000));
1603
+ const signature = createHmac("sha256", signingSecret)
1604
+ .update(`v0:${timestamp}:${rawBody}`)
1605
+ .digest("hex");
1606
+ headers.set("X-Slack-Request-Timestamp", timestamp);
1607
+ headers.set("X-Slack-Signature", `v0=${signature}`);
1608
+ }
1609
+ }
1610
+
1611
+ if (channel.type === "webhook") {
1612
+ const secret = process.env.AGENTKIT_WEBHOOK_SECRET;
1613
+
1614
+ if (secret) {
1615
+ headers.set("Authorization", `Bearer ${secret}`);
1616
+ }
482
1617
  }
483
1618
 
484
1619
  return headers;