@andreprado/agentkit 0.1.0-alpha.22 → 0.1.0-alpha.24

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.
@@ -6,7 +6,7 @@ Add a hosted messaging channel to an Agent Capsule, create the hosted channel re
6
6
 
7
7
  ## When To Use It
8
8
 
9
- Use this when the agent should receive messages from website chat, Telegram, WhatsApp, Discord, Slack, or a generic webhook through AgentKit-owned channel infrastructure.
9
+ Use this when the agent should receive messages from website chat, Telegram, WhatsApp, Discord, Slack, or a generic webhook through AgentKit-owned channel infrastructure, or when an inbound channel should reply through another configured channel.
10
10
 
11
11
  ## Commands
12
12
 
@@ -34,16 +34,17 @@ Use `--api <url>` only when the owner gives you a non-default AgentKit Cloud API
34
34
 
35
35
  ## Files Created Or Edited
36
36
 
37
- - `agentkit.config.ts`: declare channel config with `websiteChannel`, `telegramChannel`, `whatsappChannel`, `discordChannel`, `slackChannel`, or `webhookChannel`.
37
+ - `agentkit.config.ts`: declare channel config with `websiteChannel`, `telegramChannel`, `whatsappChannel`, `discordChannel`, `slackChannel`, `webhookChannel`, or `webhookOutputChannel`.
38
38
  - `.agentkit/deploy.json`: written by `agentkit deploy`; used by `agentkit channels ...`.
39
39
  - No user Turso tables: AgentKit owns channel resources, dedupe, identities, queue state, and delivery logs.
40
40
  - Website channel clients must send `AGENTKIT_WEBSITE_CHANNEL_TOKEN` as `Authorization: Bearer <token>` or `X-AgentKit-Channel-Token`.
41
- - Generic webhook clients must send `AGENTKIT_WEBHOOK_SECRET` as `Authorization: Bearer <token>`, `X-AgentKit-Webhook-Secret`, or `X-AgentKit-Webhook-Signature: sha256=<hmac>`.
41
+ - Generic inbound webhook clients must send `AGENTKIT_WEBHOOK_SECRET` as `Authorization: Bearer <token>`, `X-AgentKit-Webhook-Secret`, or `X-AgentKit-Webhook-Signature: sha256=<hmac>`.
42
+ - Generic outbound webhook channels send replies to an HTTPS URL stored in a managed secret such as `CRM_CALLBACK_URL`.
42
43
 
43
44
  ## Minimal Working Example
44
45
 
45
46
  ```ts
46
- import { defineAgent, discordChannel, slackChannel, telegramChannel, webhookChannel, whatsappChannel, websiteChannel } from "@andreprado/agentkit";
47
+ import { defineAgent, discordChannel, slackChannel, telegramChannel, webhookChannel, webhookOutputChannel, whatsappChannel, websiteChannel } from "@andreprado/agentkit";
47
48
 
48
49
  export default defineAgent({
49
50
  name: "support-agent",
@@ -62,6 +63,7 @@ export default defineAgent({
62
63
  discordChannel({ name: "server-discord", mode: "bot" }),
63
64
  slackChannel({ name: "support-slack" }),
64
65
  webhookChannel({ name: "n8n-webhook" }),
66
+ webhookOutputChannel({ name: "crm-callback", urlSecret: "CRM_CALLBACK_URL" }),
65
67
  ],
66
68
  access: { mode: "public" },
67
69
  storage: { driver: "agentkit" },
@@ -151,7 +153,92 @@ For HMAC auth, compute `HMAC-SHA256(raw JSON body, AGENTKIT_WEBHOOK_SECRET)` and
151
153
  X-AgentKit-Webhook-Signature: sha256=<hex digest>
152
154
  ```
153
155
 
154
- Generic webhooks are inbound-only in V1. The agent run is queued and delivery logs show the result; AgentKit does not call back into n8n unless the agent has a separate tool that does so.
156
+ Without `replyTo`, generic webhooks are inbound-only: the agent run is queued and delivery logs show the result, but AgentKit does not call back into the source system.
157
+
158
+ ## Reply Through Another Channel
159
+
160
+ Use `replyTo` when an inbound channel should receive on one transport and answer on another. The source channel still owns webhook validation, dedupe, and queueing; the target channel owns the outbound provider call.
161
+
162
+ Receive from a generic webhook and answer on WhatsApp:
163
+
164
+ ```ts
165
+ webhookChannel({
166
+ name: "lead-webhook",
167
+ replyTo: {
168
+ channel: "main-whatsapp",
169
+ recipientFrom: "phone",
170
+ },
171
+ })
172
+
173
+ whatsappChannel({
174
+ name: "main-whatsapp",
175
+ provider: "evolution",
176
+ })
177
+ ```
178
+
179
+ The inbound webhook payload must include the configured recipient field:
180
+
181
+ ```json
182
+ {
183
+ "event_id": "evt_124",
184
+ "external_id": "lead_123",
185
+ "phone": "+15551234567",
186
+ "message": "please follow up"
187
+ }
188
+ ```
189
+
190
+ `recipientFrom` is a dotted JSON path such as `phone`, `customer.phone`, or `payload.customer.phone`. AgentKit reads it from the raw inbound payload, requires a non-empty scalar value, and formats it for the target channel. Do not use `recipientFrom` for Discord or Slack replies; those require provider-native source identities from their own inbound events.
191
+
192
+ You can declare multiple webhook inputs by giving each `webhookChannel` a unique name:
193
+
194
+ ```ts
195
+ webhookChannel({ name: "n8n-webhook" })
196
+ webhookChannel({ name: "make-webhook" })
197
+ webhookChannel({ name: "crm-webhook", replyTo: { channel: "main-whatsapp", recipientFrom: "phone" } })
198
+ ```
199
+
200
+ ## Generic Output Webhooks
201
+
202
+ Use `webhookOutputChannel` when the agent should send the final answer to a generic callback URL, such as an internal CRM, n8n callback, Make webhook, or a custom API endpoint.
203
+
204
+ ```ts
205
+ webhookOutputChannel({
206
+ name: "crm-callback",
207
+ urlSecret: "CRM_CALLBACK_URL",
208
+ auth: {
209
+ type: "bearer",
210
+ tokenSecret: "CRM_CALLBACK_TOKEN",
211
+ },
212
+ })
213
+
214
+ webhookChannel({
215
+ name: "n8n-webhook",
216
+ replyTo: {
217
+ channel: "crm-callback",
218
+ recipientFrom: "crm_id",
219
+ },
220
+ })
221
+ ```
222
+
223
+ Set `CRM_CALLBACK_URL` as a managed secret. It must be a public HTTPS URL; AgentKit rejects localhost, private network, link-local, and metadata-service hosts before sending. Supported output auth modes are `none`, `bearer`, `header`, and `hmac`, and all auth values come from managed secrets.
224
+
225
+ Outbound generic webhook requests contain the agent's final text answer:
226
+
227
+ ```json
228
+ {
229
+ "event_id": "webhook:generic:crm-callback:<conversation-id>",
230
+ "conversation_id": "<conversation-id>",
231
+ "channel_id": "crm-callback",
232
+ "external_identity": "webhook:generic:recipient:acct_123",
233
+ "message": {
234
+ "role": "assistant",
235
+ "content_type": "text",
236
+ "content": "Thanks, I can help with that."
237
+ }
238
+ }
239
+ ```
240
+
241
+ Generic output channels do not have public inbound webhook URLs. `agentkit channels test <output-name>` is unsupported because there is no inbound endpoint to smoke.
155
242
 
156
243
  ## Auto Transcribe Audio
157
244
 
@@ -2,11 +2,11 @@
2
2
 
3
3
  ## Goal
4
4
 
5
- Configure website, Telegram, WhatsApp, Discord, Slack, and generic webhook channels without leaking secrets, storing raw provider payloads, or treating public webhook URLs as authorization.
5
+ Configure website, Telegram, WhatsApp, Discord, Slack, generic inbound webhook channels, and generic outbound webhook channels without leaking secrets, storing raw provider payloads, or treating public webhook URLs as authorization.
6
6
 
7
7
  ## When To Use It
8
8
 
9
- Use this when a coding agent adds or changes channels in an Agent Capsule, enables audio transcription, inspects deliveries, or prepares a hosted deploy that receives provider webhooks.
9
+ Use this when a coding agent adds or changes channels in an Agent Capsule, enables audio transcription, inspects deliveries, prepares a hosted deploy that receives provider webhooks, or routes replies through another channel.
10
10
 
11
11
  ## Commands
12
12
 
@@ -28,12 +28,15 @@ agentkit channels deliveries show <delivery-id>
28
28
  ## Safety Rules
29
29
 
30
30
  - Public webhook URLs are not permission grants.
31
- - Keep provider tokens, webhook secrets, bot tokens, transcription keys, and bearer tokens out of source files.
31
+ - Keep provider tokens, webhook secrets, output webhook URLs, bot tokens, transcription keys, and bearer tokens out of source files.
32
32
  - Put secret names in `agentkit.config.ts`; put local values in ignored `.env`; upload production values with `agentkit secret set` or `agentkit secret sync`.
33
33
  - Do not store raw webhook bodies, raw audio, or provider secret values in tools, prompts, evals, fixtures, or delivery notes.
34
34
  - Do not expose Discord interaction tokens or bot tokens. Interaction tokens are reply tokens, not public identifiers.
35
35
  - Do not expose Slack bot tokens or signing secrets. Slack signing secrets authenticate webhook origin; Slack bot tokens authorize outbound replies.
36
36
  - Do not expose `AGENTKIT_WEBHOOK_SECRET`. Generic webhook clients must authenticate with bearer/header token auth or an HMAC signature over the exact raw body.
37
+ - Do not put generic output webhook URLs or auth values in payloads, prompts, tools, fixtures, or `agentkit.config.ts`. Use `urlSecret` and output auth secret names only.
38
+ - Keep generic output webhook URLs on public HTTPS origins. AgentKit rejects localhost, private network, link-local, and metadata-service hosts before sending.
39
+ - Do not route replies to a channel name from the inbound payload. `replyTo.channel` must be static config, and `recipientFrom` may only select the recipient identifier.
37
40
  - Do not remove `EVOLUTION_WEBHOOK_TOKEN` from Evolution WhatsApp webhooks. Evolution provider docs expose webhook delivery setup but no webhook signing-secret contract, so AgentKit requires a secret token query parameter and rejects requests without it.
38
41
  - Keep `EVOLUTION_API_BASE_URL` on a public HTTPS origin. AgentKit rejects localhost, private network, link-local, and metadata-service hosts for Evolution outbound and media-download requests.
39
42
  - Inspect delivery IDs, hashes, redacted metadata, and statuses instead of raw provider payloads.
@@ -126,6 +129,35 @@ webhookChannel({
126
129
 
127
130
  AgentKit validates `Authorization: Bearer <secret>`, `X-AgentKit-Webhook-Secret`, or `X-AgentKit-Webhook-Signature` against the raw body before parsing business fields. Payloads must include `event_id` for replay protection. `external_id` is hashed before storage.
128
131
 
132
+ Generic output webhooks use managed URL and auth secrets:
133
+
134
+ ```ts
135
+ webhookOutputChannel({
136
+ name: "crm-callback",
137
+ urlSecret: "CRM_CALLBACK_URL",
138
+ auth: {
139
+ type: "hmac",
140
+ secret: "CRM_CALLBACK_SIGNING_SECRET",
141
+ },
142
+ })
143
+ ```
144
+
145
+ The output URL must be a public HTTPS URL stored in `CRM_CALLBACK_URL`. Supported auth modes are `none`, `bearer`, `header`, and `hmac`. For HMAC auth, AgentKit signs the exact JSON callback body and sends `sha256=<digest>` in `X-AgentKit-Webhook-Signature` unless a different `headerName` is configured.
146
+
147
+ Cross-channel replies use static routing:
148
+
149
+ ```ts
150
+ webhookChannel({
151
+ name: "lead-webhook",
152
+ replyTo: {
153
+ channel: "crm-callback",
154
+ recipientFrom: "crm_id",
155
+ },
156
+ })
157
+ ```
158
+
159
+ `replyTo.channel` must name another configured channel. `recipientFrom` is a dotted JSON path into the inbound payload and must resolve to a non-empty string, number, or boolean. It is for recipient identity only; never use inbound data to choose a callback URL, auth secret, or channel name.
160
+
129
161
  Evolution WhatsApp uses a self-hosted API key and an AgentKit webhook token:
130
162
 
131
163
  ```ts
@@ -2,11 +2,11 @@
2
2
 
3
3
  ## Goal
4
4
 
5
- Diagnose website, Telegram, WhatsApp, Discord, Slack, or generic webhook channel setup, webhook validation, dedupe, buffering, outbound sends, and delivery failures from the Agent Capsule CLI.
5
+ Diagnose website, Telegram, WhatsApp, Discord, Slack, generic inbound webhook, generic outbound webhook, cross-channel reply routing, webhook validation, dedupe, buffering, outbound sends, and delivery failures from the Agent Capsule CLI.
6
6
 
7
7
  ## When To Use It
8
8
 
9
- Use this when a hosted channel is not responding, a provider is retrying messages, audio transcription is failing, or delivery status does not match the user's expectation.
9
+ Use this when a hosted channel is not responding, a provider is retrying messages, audio transcription is failing, cross-channel replies are going to the wrong target, or delivery status does not match the user's expectation.
10
10
 
11
11
  ## Commands
12
12
 
@@ -20,6 +20,7 @@ agentkit transcribe smoke --provider groq
20
20
  agentkit channels test <name> --fixture ./fixtures/provider-event.json
21
21
  agentkit channels deliveries list <name> --since 24h
22
22
  agentkit channels deliveries show <delivery-id>
23
+ agentkit channels deliveries list <reply-target-name> --since 24h
23
24
  agentkit channels buffers list <name>
24
25
  agentkit channels buffers show <conversation-id>
25
26
  agentkit channels buffers flush <conversation-id>
@@ -70,6 +71,8 @@ Expected `show` output includes signature status, dedupe key, queue/run state, o
70
71
  - Inspect delivery IDs, hashes, statuses, and redacted metadata rather than raw provider payloads.
71
72
  - Never paste provider secrets into fixtures or prompts.
72
73
  - Treat `adapter_stubbed` as "no outbound provider call happened", not proof that a client received a message. It can mean explicit dry-run mode or an inbound-only channel such as a generic webhook.
74
+ - For `replyTo` routes, inspect the source channel delivery for validation, dedupe, buffer, queue, and agent-run state, then inspect the target channel delivery for the outbound provider result.
75
+ - Generic output webhook channels do not have inbound endpoints. Use `agentkit channels doctor <name>` to inspect secret readiness and inspect target-channel deliveries after a source event triggers the route.
73
76
  - Before replaying or flushing buffered conversations, confirm the channel name and conversation ID.
74
77
  - Keep local `.env` values ignored and upload hosted production values through AgentKit secret commands.
75
78
 
@@ -117,10 +120,19 @@ The provider retried an already-processed event. No second agent run should be c
117
120
  The message is accepted and waiting inside a per-conversation channel buffer. It should move to `queued` after the quiet window, max wait, max message count, or max character count.
118
121
 
119
122
  `adapter_stubbed`:
120
- The adapter completed without calling an outbound provider. This can happen in explicit dry-run mode or for an inbound-only channel such as a generic webhook.
123
+ The adapter completed without calling an outbound provider. This can happen in explicit dry-run mode or for an inbound-only channel such as a generic webhook without `replyTo`.
121
124
 
122
125
  `provider_sent`:
123
- The provider API accepted the outbound request and returned a provider message ID.
126
+ The provider API accepted the outbound request and returned a provider message ID. For cross-channel replies, this status appears on the reply target channel, such as WhatsApp or a generic output webhook.
127
+
128
+ `channel_reply_target_missing`:
129
+ The source channel has `replyTo.channel`, but the hosted channel resource for that target does not exist in the same deploy. Create or reconnect the target channel, then retry the source event.
130
+
131
+ `channel_reply_recipient_missing`:
132
+ The route needs a destination identity but `recipientFrom` was missing, resolved to an empty value, or the source identity cannot be reused by the target channel. Check the inbound payload and the configured dotted JSON path.
133
+
134
+ `channel_reply_target_invalid`:
135
+ The route points to an invalid target, such as an inbound-only generic webhook, or tries to use `recipientFrom` for a provider that requires native source identities such as Discord or Slack.
124
136
 
125
137
  `channel_limit_exceeded`:
126
138
  Backpressure skipped the message before queueing.
@@ -372,7 +372,7 @@ Channels are hosted inbound/outbound conversation transports. They are separate
372
372
  Use these helpers in `agentkit.config.ts`:
373
373
 
374
374
  ```ts
375
- import { defineAgent, discordChannel, slackChannel, telegramChannel, webhookChannel, whatsappChannel, websiteChannel } from "@andreprado/agentkit";
375
+ import { defineAgent, discordChannel, slackChannel, telegramChannel, webhookChannel, webhookOutputChannel, whatsappChannel, websiteChannel } from "@andreprado/agentkit";
376
376
 
377
377
  export default defineAgent({
378
378
  name: "support-agent",
@@ -390,6 +390,7 @@ export default defineAgent({
390
390
  discordChannel({ name: "server-discord", mode: "bot" }),
391
391
  slackChannel({ name: "support-slack" }),
392
392
  webhookChannel({ name: "n8n-webhook" }),
393
+ webhookOutputChannel({ name: "crm-callback", urlSecret: "CRM_CALLBACK_URL" }),
393
394
  ],
394
395
  access: { mode: "public" },
395
396
  storage: { driver: "agentkit" },
@@ -405,6 +406,8 @@ Rules:
405
406
  - Do not store channel plumbing in the user's Turso database.
406
407
  - Use `buffer.mode: "debounce"` when a channel should coalesce rapid client messages into one agent run.
407
408
  - Buffered deliveries show `buffered`, then flush to one `queued` run after `quietWindowMs`, `maxWaitMs`, `maxMessages`, or `maxChars`.
409
+ - Use `replyTo` when an inbound channel should receive on one transport and answer on another configured channel.
410
+ - Use `webhookOutputChannel` when the reply target is a generic HTTPS callback URL stored in a managed secret.
408
411
 
409
412
  Channel buffer example:
410
413
 
@@ -495,6 +498,56 @@ Generic webhook payloads should be canonical JSON:
495
498
 
496
499
  Use `Authorization: Bearer <AGENTKIT_WEBHOOK_SECRET>`, `X-AgentKit-Webhook-Secret`, or `X-AgentKit-Webhook-Signature: sha256=<hmac>` where the HMAC is SHA-256 over the exact raw JSON body. The hosted URL is `/channels/<name>/webhook`.
497
500
 
501
+ Without `replyTo`, generic webhooks are inbound-only and do not call back into the source system. To receive on one channel and answer on another, add a static `replyTo` route:
502
+
503
+ ```ts
504
+ webhookChannel({
505
+ name: "lead-webhook",
506
+ replyTo: {
507
+ channel: "main-whatsapp",
508
+ recipientFrom: "phone",
509
+ },
510
+ })
511
+
512
+ whatsappChannel({
513
+ name: "main-whatsapp",
514
+ provider: "evolution",
515
+ })
516
+ ```
517
+
518
+ `recipientFrom` is a dotted JSON path such as `phone`, `customer.phone`, or `payload.customer.phone`. It must resolve to a non-empty scalar in the inbound payload. Do not use `recipientFrom` for Discord or Slack replies because those transports need provider-native source identities.
519
+
520
+ Multiple webhooks are multiple named channels:
521
+
522
+ ```ts
523
+ webhookChannel({ name: "n8n-webhook" })
524
+ webhookChannel({ name: "make-webhook" })
525
+ webhookChannel({ name: "crm-webhook", replyTo: { channel: "main-whatsapp", recipientFrom: "phone" } })
526
+ ```
527
+
528
+ Generic output webhook channels send the final agent answer to an HTTPS callback URL:
529
+
530
+ ```ts
531
+ webhookOutputChannel({
532
+ name: "crm-callback",
533
+ urlSecret: "CRM_CALLBACK_URL",
534
+ auth: {
535
+ type: "bearer",
536
+ tokenSecret: "CRM_CALLBACK_TOKEN",
537
+ },
538
+ })
539
+
540
+ webhookChannel({
541
+ name: "n8n-webhook",
542
+ replyTo: {
543
+ channel: "crm-callback",
544
+ recipientFrom: "crm_id",
545
+ },
546
+ })
547
+ ```
548
+
549
+ `CRM_CALLBACK_URL` and output auth values are managed secrets only. The URL must be public HTTPS; AgentKit rejects localhost, private network, link-local, and metadata-service hosts. Supported output auth modes are `none`, `bearer`, `header`, and `hmac`. Generic output channels do not have public inbound webhook URLs, so `agentkit channels test <output-name>` is unsupported.
550
+
498
551
  Common channel verification:
499
552
 
500
553
  ```sh
package/docs/llms.txt CHANGED
@@ -19,6 +19,7 @@ Task guides:
19
19
  - Build container artifact: `agentkit build --target container`
20
20
  - Generate VPS handoff: `agentkit deploy --target vps --host agent.example.com --dry-run`
21
21
  - Add hosted channels: `docs/guides/add-channel.md`
22
+ - Route replies across channels or add generic output webhooks: `docs/guides/add-channel.md#reply-through-another-channel`
22
23
  - Add AgentKit-managed Composio: `docs/guides/add-managed-composio.md`
23
24
  - Buffer rapid channel messages: `docs/guides/add-channel.md#buffer-bursty-messages`
24
25
  - Connect Discord: `docs/guides/connect-discord.md`
@@ -28,6 +29,7 @@ Task guides:
28
29
  - Connect WhatsApp through UAZAPI: `docs/guides/connect-whatsapp-uazapi.md`
29
30
  - Connect WhatsApp through Zapster: `docs/guides/connect-whatsapp-zapster.md`
30
31
  - Follow channel webhook and delivery-log safety rules: `docs/guides/channel-security.md`
32
+ - Debug channel delivery and cross-channel reply routing: `docs/guides/debug-channel.md`
31
33
  - Follow secret, access, and tool safety rules: `docs/guides/security-rules.md`
32
34
 
33
35
  Current local commands:
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@andreprado/agentkit",
3
- "version": "0.1.0-alpha.22",
3
+ "version": "0.1.0-alpha.24",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "repository": {
@@ -18,11 +18,15 @@ type CloudChannel = {
18
18
  type: "website" | "telegram" | "whatsapp" | "discord" | "slack" | "webhook";
19
19
  provider: "agentkit" | "telegram" | "zapster" | "meta" | "uazapi" | "evolution" | "discord" | "slack" | "generic";
20
20
  mode?: "interactions" | "bot";
21
+ direction?: "inbound" | "outbound";
21
22
  status: string;
22
23
  webhook_url: string;
23
24
  required_secrets: Array<{ name: string; status: string }>;
24
25
  buffer?: AgentChannel["buffer"];
25
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"];
26
30
  };
27
31
 
28
32
  type CloudDelivery = {
@@ -247,6 +251,12 @@ export async function handleChannelsCommand(args: ParsedArgs): Promise<void> {
247
251
  }
248
252
 
249
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
+
250
260
  const smoke = await runConnectSmoke(() => runChannelSmoke(channel, args.flags.api, args.flags.message));
251
261
  printGenericWebhookHumanNextStep(channel);
252
262
  if (!smoke) {
@@ -318,7 +328,11 @@ export async function handleChannelsCommand(args: ParsedArgs): Promise<void> {
318
328
  } else if (channel.type === "slack") {
319
329
  printSlackHumanNextStep(channel);
320
330
  } else if (channel.type === "webhook") {
321
- printGenericWebhookHumanNextStep(channel);
331
+ if (channel.direction === "outbound") {
332
+ console.log("Outbound webhook endpoint: configured from managed secrets.");
333
+ } else {
334
+ printGenericWebhookHumanNextStep(channel);
335
+ }
322
336
  } else {
323
337
  console.log(`Website endpoint: ${channel.webhook_url}`);
324
338
  console.log("Use this endpoint from the AgentKit website client or a signed server-to-server test.");
@@ -361,6 +375,9 @@ export async function handleChannelsCommand(args: ParsedArgs): Promise<void> {
361
375
  }
362
376
 
363
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
+ }
364
381
  const apiUrl = parseCloudApiUrl(args.flags.api);
365
382
  const signed = channel.type === "whatsapp" && channel.provider === "evolution" && !args.flags.fixture
366
383
  ? await runServerGeneratedChannelTest(channel, apiUrl, args.flags.message)
@@ -511,6 +528,7 @@ async function createHostedChannel(
511
528
  ...(localChannel?.limits ? { limits: channelLimitsForApi(localChannel.limits) } : {}),
512
529
  ...(localChannel?.buffer ? { buffer: localChannel.buffer } : {}),
513
530
  ...(localChannel?.audio ? { audio: localChannel.audio } : {}),
531
+ ...channelExtraConfigForApi(localChannel),
514
532
  };
515
533
 
516
534
  return cloudPost<{ channel: CloudChannel }>(
@@ -890,6 +908,9 @@ function printChannelStatus(channel: CloudChannel): void {
890
908
  if (channel.type === "discord") {
891
909
  console.log(`Mode: ${channel.mode ?? "interactions"}`);
892
910
  }
911
+ if (channel.type === "webhook" && channel.direction) {
912
+ console.log(`Direction: ${channel.direction}`);
913
+ }
893
914
  console.log(`Status: ${channel.status}`);
894
915
  if (channel.webhook_url) {
895
916
  console.log(`Webhook URL: ${channel.webhook_url}`);
@@ -1230,6 +1251,19 @@ function defaultChannelSecretsForCli(type: string, provider: string, mode?: "int
1230
1251
  return [];
1231
1252
  }
1232
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
+
1233
1267
  async function readLocalChannelConfig(
1234
1268
  cwd: string,
1235
1269
  type: string,
@@ -29,6 +29,16 @@ type DeployReadinessReport = {
29
29
  apiUrl: string;
30
30
  context: DeployReadinessContext;
31
31
  checks: DeployReadinessCheck[];
32
+ cloudAuth: {
33
+ source?: string;
34
+ accountEmail?: string;
35
+ };
36
+ onlineCapacity: {
37
+ accountUsed?: number;
38
+ accountLimit?: number;
39
+ globalUsed?: number;
40
+ globalLimit?: number;
41
+ };
32
42
  };
33
43
 
34
44
  export async function checkDeployReadiness(options: {
@@ -39,6 +49,12 @@ export async function checkDeployReadiness(options: {
39
49
  const auth = options.requireLogin ? await readCloudAuthForApiUrl(options.apiUrl) : null;
40
50
  const checks: DeployReadinessCheck[] = [];
41
51
  let cloudAuthUsable = false;
52
+ const cloudAuth: DeployReadinessReport["cloudAuth"] = {};
53
+ const onlineCapacity: DeployReadinessReport["onlineCapacity"] = {};
54
+
55
+ if (auth?.token) {
56
+ cloudAuth.source = formatCloudAuthSource(auth);
57
+ }
42
58
 
43
59
  if (context.runtime !== "edge") {
44
60
  checks.push({
@@ -84,6 +100,9 @@ export async function checkDeployReadiness(options: {
84
100
  try {
85
101
  const me = (await cloudApiRequest(options.apiUrl, "/v1/me", { method: "GET" })) as CloudMeResponse;
86
102
  const email = me.account?.email;
103
+ if (email) {
104
+ cloudAuth.accountEmail = email;
105
+ }
87
106
  checks.push({
88
107
  status: "pass",
89
108
  title: "Login",
@@ -164,6 +183,22 @@ export async function checkDeployReadiness(options: {
164
183
  const globalLimit = normalizeOptionalNumber(global?.limit);
165
184
  const globalUsed = normalizeOptionalNumber(global?.used);
166
185
 
186
+ if (accountLimit !== undefined) {
187
+ onlineCapacity.accountLimit = accountLimit;
188
+ }
189
+
190
+ if (accountUsed !== undefined) {
191
+ onlineCapacity.accountUsed = accountUsed;
192
+ }
193
+
194
+ if (globalLimit !== undefined) {
195
+ onlineCapacity.globalLimit = globalLimit;
196
+ }
197
+
198
+ if (globalUsed !== undefined) {
199
+ onlineCapacity.globalUsed = globalUsed;
200
+ }
201
+
167
202
  if (accountLimit !== undefined && accountUsed !== undefined && accountUsed >= accountLimit) {
168
203
  checks.push({
169
204
  status: "fail",
@@ -374,6 +409,8 @@ export async function checkDeployReadiness(options: {
374
409
  apiUrl: options.apiUrl,
375
410
  context,
376
411
  checks,
412
+ cloudAuth,
413
+ onlineCapacity,
377
414
  };
378
415
  }
379
416
 
@@ -383,6 +420,19 @@ export function printDeployReadinessReport(report: DeployReadinessReport): void
383
420
  console.log(`Project ID: ${report.context.projectId}`);
384
421
  console.log(`Target: ${report.context.target}`);
385
422
  console.log(`API: ${report.apiUrl}`);
423
+ const authSummary = formatDeployCloudAuthSummary(report);
424
+
425
+ if (authSummary) {
426
+ console.log(`Account: ${authSummary.account}`);
427
+ console.log(`Auth source: ${authSummary.source}`);
428
+ }
429
+
430
+ const capacitySummary = formatDeployOnlineCapacity(report, "current");
431
+
432
+ if (capacitySummary) {
433
+ console.log(`Capacity: ${capacitySummary}`);
434
+ }
435
+
386
436
  console.log("");
387
437
 
388
438
  for (const check of report.checks) {
@@ -394,6 +444,36 @@ export function printDeployReadinessReport(report: DeployReadinessReport): void
394
444
  }
395
445
  }
396
446
 
447
+ export function formatDeployCloudAuthSummary(
448
+ report: DeployReadinessReport,
449
+ ): { account: string; source: string } | null {
450
+ if (!report.cloudAuth.accountEmail || !report.cloudAuth.source) {
451
+ return null;
452
+ }
453
+
454
+ return {
455
+ account: report.cloudAuth.accountEmail,
456
+ source: report.cloudAuth.source,
457
+ };
458
+ }
459
+
460
+ export function formatDeployOnlineCapacity(
461
+ report: DeployReadinessReport,
462
+ timing: "current" | "before deploy",
463
+ ): string | null {
464
+ const suffix = timing === "before deploy" ? " before deploy" : "";
465
+
466
+ if (report.onlineCapacity.accountUsed !== undefined && report.onlineCapacity.accountLimit !== undefined) {
467
+ return `${report.onlineCapacity.accountUsed}/${report.onlineCapacity.accountLimit} account online deploy slots in use${suffix}`;
468
+ }
469
+
470
+ if (report.onlineCapacity.globalUsed !== undefined && report.onlineCapacity.globalLimit !== undefined) {
471
+ return `${report.onlineCapacity.globalUsed}/${report.onlineCapacity.globalLimit} global online deploy slots in use${suffix}`;
472
+ }
473
+
474
+ return null;
475
+ }
476
+
397
477
  export async function syncHostedSecretsFromLocal(options: { apiUrl: string; projectId: string }): Promise<void> {
398
478
  const context = await loadDeployReadinessContext(process.cwd(), process.env);
399
479
  const localEnv = await loadCapsuleEnv(context.root, {});
package/src/cli/index.ts CHANGED
@@ -57,6 +57,8 @@ import {
57
57
  } from "./cloud-client";
58
58
  import {
59
59
  checkDeployReadiness,
60
+ formatDeployCloudAuthSummary,
61
+ formatDeployOnlineCapacity,
60
62
  formatReadinessError,
61
63
  printDeployReadinessReport,
62
64
  resolveProjectIdForSecretCommand,
@@ -1002,6 +1004,7 @@ async function main() {
1002
1004
  console.log(`Project: ${deploy.project.id}`);
1003
1005
  console.log(`Deploy: ${deploy.deploy.id}`);
1004
1006
  console.log(`State: ${statePath}`);
1007
+ printDeployCloudSummary(readinessReport);
1005
1008
 
1006
1009
  if (chatAccess?.status === "ready") {
1007
1010
  console.log(`Chat auth: ${relativePath(process.cwd(), chatAccess.path)}`);
@@ -1501,6 +1504,25 @@ function printDeployProductionHandoff(input: {
1501
1504
  console.log(`Next: ${nextCommand}`);
1502
1505
  }
1503
1506
 
1507
+ function printDeployCloudSummary(report: Awaited<ReturnType<typeof checkDeployReadiness>> | null): void {
1508
+ if (!report) {
1509
+ return;
1510
+ }
1511
+
1512
+ const authSummary = formatDeployCloudAuthSummary(report);
1513
+
1514
+ if (authSummary) {
1515
+ console.log(`Account: ${authSummary.account}`);
1516
+ console.log(`Auth: ${authSummary.source}`);
1517
+ }
1518
+
1519
+ const capacitySummary = formatDeployOnlineCapacity(report, "before deploy");
1520
+
1521
+ if (capacitySummary) {
1522
+ console.log(`Capacity: ${capacitySummary}`);
1523
+ }
1524
+ }
1525
+
1504
1526
  async function ensureDeployChatAccessToken(options: {
1505
1527
  apiUrl: string;
1506
1528
  root: string;