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

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 (49) hide show
  1. package/README.md +1 -0
  2. package/docs/guides/add-channel.md +75 -4
  3. package/docs/guides/add-managed-composio.md +3 -1
  4. package/docs/guides/add-tool.md +6 -3
  5. package/docs/guides/channel-security.md +32 -1
  6. package/docs/guides/connect-whatsapp-evolution.md +121 -0
  7. package/docs/guides/connect-whatsapp-uazapi.md +126 -0
  8. package/docs/guides/create-agent.md +6 -4
  9. package/docs/guides/debug-channel.md +3 -3
  10. package/docs/guides/prepare-deploy.md +1 -1
  11. package/docs/guides/run-evals.md +5 -1
  12. package/docs/guides/use-provider.md +26 -2
  13. package/docs/llms-full.txt +82 -9
  14. package/docs/llms.txt +9 -2
  15. package/package.json +1 -1
  16. package/src/cli/commands/channels.ts +285 -14
  17. package/src/cli/deploy-chat-ui.ts +86 -15
  18. package/src/cli/help.ts +2 -2
  19. package/src/cli/index.ts +107 -8
  20. package/src/index.ts +42 -5
  21. package/src/providers/pi.ts +12 -2
  22. package/src/runtime/channel-test-harness.ts +14 -1
  23. package/src/runtime/channels/generic-webhook.ts +225 -0
  24. package/src/runtime/channels/whatsapp-evolution.ts +1357 -0
  25. package/src/runtime/channels/whatsapp-uazapi.ts +1327 -0
  26. package/src/runtime/channels.ts +1 -1
  27. package/src/runtime/chat.ts +7 -1
  28. package/src/runtime/config.ts +40 -5
  29. package/src/runtime/core/targets.ts +5 -5
  30. package/src/runtime/dev-server.ts +21 -5
  31. package/src/runtime/evals.ts +39 -24
  32. package/src/runtime/targets/cloudflare/build.ts +23 -3
  33. package/src/templates/blank.ts +29 -6
  34. package/src/templates/dentista.ts +22 -4
  35. package/src/templates/skills/agentkit-build-agent/SKILL.md +30 -2
  36. package/src/templates/skills/agentkit-capsule/SKILL.md +22 -1
  37. package/src/templates/skills/agentkit-capsule/references/docs-router.md +1 -1
  38. package/src/templates/skills/agentkit-channels/SKILL.md +25 -2
  39. package/src/templates/skills/agentkit-channels/references/channel-debugging.md +1 -1
  40. package/src/templates/skills/agentkit-channels/references/whatsapp-evolution.md +57 -0
  41. package/src/templates/skills/agentkit-channels/references/whatsapp-uazapi.md +61 -0
  42. package/src/templates/skills/agentkit-database/SKILL.md +11 -0
  43. package/src/templates/skills/agentkit-deploy/SKILL.md +2 -0
  44. package/src/templates/skills/agentkit-evals/SKILL.md +15 -0
  45. package/src/templates/skills/agentkit-improve/SKILL.md +14 -4
  46. package/src/templates/skills/agentkit-integrations/SKILL.md +22 -0
  47. package/src/templates/skills/agentkit-provider/SKILL.md +20 -2
  48. package/src/templates/skills/agentkit-tools/SKILL.md +2 -0
  49. package/src/templates/support.ts +30 -7
package/src/cli/index.ts CHANGED
@@ -109,6 +109,8 @@ async function main() {
109
109
  throw new Error("Usage: agentkit help [commands]");
110
110
  }
111
111
 
112
+ warnIfUnsupportedNode();
113
+
112
114
  if (args.command === "new") {
113
115
  const [name] = args.positional;
114
116
  const template = String(args.flags.template ?? "blank");
@@ -136,8 +138,8 @@ async function main() {
136
138
  console.log(" npm install");
137
139
  }
138
140
  console.log(" open this folder in Codex or Claude Code");
139
- console.log(" coding agents start at skills/agentkit-capsule/SKILL.md");
140
- console.log(' ask: "Develop an appointment and intake agent for an ophthalmology office"');
141
+ console.log(' ask Codex: "Develop an appointment and intake agent for an ophthalmology office"');
142
+ console.log(" Codex starts at skills/agentkit-capsule/SKILL.md and treats your request as the brief");
141
143
  console.log(' npm run chat -- --message "hello"');
142
144
  console.log(" npm run dev");
143
145
  console.log(" npm run agentkit -- deploy");
@@ -187,16 +189,25 @@ async function main() {
187
189
  throw new Error("Usage: agentkit chat-ui --deploy [--port <number>] [--token-file <path>]");
188
190
  }
189
191
 
190
- const port = parsePort(args.flags.port, "agentkit chat-ui --deploy --port <number>") ?? 4124;
192
+ const requestedPort = parsePort(args.flags.port, "agentkit chat-ui --deploy --port <number>");
193
+ const port = requestedPort ?? 4124;
191
194
  const tokenFile = parseOptionalStringFlag(
192
195
  args.flags["token-file"],
193
196
  "token-file",
194
197
  "agentkit chat-ui --deploy --token-file <path>",
195
198
  );
196
- const server = await startDeployChatUiServer(process.cwd(), { port, tokenFile });
199
+ const server = await startDeployChatUiServer(process.cwd(), {
200
+ port,
201
+ tokenFile,
202
+ autoPort: requestedPort === undefined,
203
+ });
197
204
 
198
205
  console.log("AgentKit hosted chat UI running");
199
206
  console.log("");
207
+ if (server.portConflict) {
208
+ console.log(`Port ${server.portConflict.port} is already used. Started hosted chat UI at ${server.url}.`);
209
+ console.log("");
210
+ }
200
211
  console.log(`Chat: ${server.url}`);
201
212
  console.log(`Production: ${server.deployUrl}`);
202
213
  console.log(`Token: ${server.tokenSource}`);
@@ -936,14 +947,16 @@ async function main() {
936
947
  return;
937
948
  }
938
949
 
950
+ let readinessReport: Awaited<ReturnType<typeof checkDeployReadiness>> | null = null;
951
+
939
952
  if (!args.flags["dry-run"] && !args.flags["local-wrangler"]) {
940
- const report = await checkDeployReadiness({
953
+ readinessReport = await checkDeployReadiness({
941
954
  apiUrl: await resolveCloudApiUrl(args.flags.api),
942
955
  requireLogin: !anonymous,
943
956
  });
944
957
 
945
- if (!report.ok) {
946
- printDeployReadinessReport(report);
958
+ if (!readinessReport.ok) {
959
+ printDeployReadinessReport(readinessReport);
947
960
  throw new AgentKitError(
948
961
  "deploy_readiness_failed",
949
962
  "Fix deploy readiness failures before running agentkit deploy.",
@@ -1016,6 +1029,14 @@ async function main() {
1016
1029
  : undefined,
1017
1030
  });
1018
1031
  }
1032
+ printDeployProductionHandoff({
1033
+ deployUrl: deploy.deploy.url,
1034
+ statePath,
1035
+ chatAccess,
1036
+ databaseSchemaPath: result.tursoSchemaPath,
1037
+ readinessReport,
1038
+ smokeMessage,
1039
+ });
1019
1040
  return;
1020
1041
  }
1021
1042
 
@@ -1362,7 +1383,7 @@ Rules:
1362
1383
  - Edit the agent contract in agentkit.config.ts.
1363
1384
  - Add TypeScript tools under tools/.
1364
1385
  - Keep the first useful version runnable with test/fake unless the owner has already chosen a real provider.
1365
- - Before testing real conversation behavior, ask the owner to choose OpenRouter, OpenAI, Anthropic, or another supported provider. Do not choose for them.
1386
+ - Before testing real conversation behavior, ask the owner to choose OpenRouter, OpenAI, Anthropic, OpenCode Zen, OpenCode Go, or another supported provider. Do not choose for them.
1366
1387
  - Keep local secrets in .env only.
1367
1388
  - Do not commit or print secret values.
1368
1389
  - Do not edit .agentkit/ directly.
@@ -1402,6 +1423,84 @@ function isRecord(value: unknown): value is Record<string, unknown> {
1402
1423
  return typeof value === "object" && value !== null && !Array.isArray(value);
1403
1424
  }
1404
1425
 
1426
+ function warnIfUnsupportedNode(): void {
1427
+ const major = majorNodeVersion(process.versions.node);
1428
+
1429
+ if (major !== null && major < 24) {
1430
+ console.warn(
1431
+ `Warning: @andreprado/agentkit requires Node >=24; current Node is ${process.versions.node}. Use Node 24+ before relying on local runtime or deploy commands.`,
1432
+ );
1433
+ }
1434
+ }
1435
+
1436
+ function majorNodeVersion(version: string): number | null {
1437
+ const [major] = version.split(".");
1438
+ const parsed = Number(major);
1439
+ return Number.isInteger(parsed) ? parsed : null;
1440
+ }
1441
+
1442
+ function printDeployProductionHandoff(input: {
1443
+ deployUrl: string;
1444
+ statePath: string;
1445
+ chatAccess: Awaited<ReturnType<typeof ensureDeployChatAccessToken>>;
1446
+ databaseSchemaPath: string | null;
1447
+ readinessReport: Awaited<ReturnType<typeof checkDeployReadiness>> | null;
1448
+ smokeMessage?: string;
1449
+ }): void {
1450
+ const context = input.readinessReport?.context;
1451
+ const userSecrets = context?.userManagedSecrets ?? [];
1452
+ const managedSecrets = context?.agentKitManagedSecrets ?? [];
1453
+ const composio = context?.integrations.managedComposio;
1454
+
1455
+ console.log("");
1456
+ console.log("Production handoff");
1457
+ console.log(`URL: ${input.deployUrl}`);
1458
+ console.log(`UI: npm run agentkit -- chat-ui --deploy`);
1459
+ console.log(`State: ${input.statePath}`);
1460
+ console.log(
1461
+ `Secrets: ${
1462
+ userSecrets.length > 0
1463
+ ? `user-managed set (${userSecrets.join(", ")})`
1464
+ : "no user-managed hosted secrets required"
1465
+ }${managedSecrets.length > 0 ? `; AgentKit-managed: ${managedSecrets.join(", ")}` : ""}`,
1466
+ );
1467
+ console.log(
1468
+ `Database: ${
1469
+ input.databaseSchemaPath
1470
+ ? `AgentKit-managed; schema artifact ${input.databaseSchemaPath}`
1471
+ : "AgentKit-managed; no application schema artifact"
1472
+ }`,
1473
+ );
1474
+
1475
+ if (composio?.enabled) {
1476
+ console.log(`Integrations: managed Composio configured for ${composio.toolkits.join(", ")}`);
1477
+ for (const toolkit of composio.toolkits) {
1478
+ console.log(`Connect: npm run agentkit -- integrations connect composio --toolkit ${toolkit}`);
1479
+ }
1480
+ } else {
1481
+ console.log("Integrations: none configured");
1482
+ }
1483
+
1484
+ if (input.smokeMessage) {
1485
+ console.log(`Smoke: verified with ${JSON.stringify(input.smokeMessage)}`);
1486
+ } else {
1487
+ console.log('Smoke: not run; next check: npm run agentkit -- deploy smoke --message "hello"');
1488
+ }
1489
+
1490
+ if (input.chatAccess.status === "ready") {
1491
+ console.log(`Chat token: ${relativePath(process.cwd(), input.chatAccess.path)}`);
1492
+ } else if (input.chatAccess.status === "skipped") {
1493
+ console.log("Chat token: not created for anonymous deploy");
1494
+ } else {
1495
+ console.log(`Chat token: failed; run npm run agentkit -- access token create ${defaultDeployChatAccessTokenName} --out ${defaultDeployChatAccessTokenPath}`);
1496
+ }
1497
+
1498
+ const nextCommand = composio?.enabled && composio.toolkits.length > 0
1499
+ ? `npm run agentkit -- integrations connect composio --toolkit ${composio.toolkits[0]}`
1500
+ : "npm run agentkit -- chat-ui --deploy";
1501
+ console.log(`Next: ${nextCommand}`);
1502
+ }
1503
+
1405
1504
  async function ensureDeployChatAccessToken(options: {
1406
1505
  apiUrl: string;
1407
1506
  root: string;
package/src/index.ts CHANGED
@@ -1,10 +1,10 @@
1
1
  export type AgentRuntime = "local" | "edge";
2
- export type AgentProviderName = "test" | "openai" | "anthropic" | "openrouter" | "custom";
2
+ export type AgentProviderName = "test" | "openai" | "anthropic" | "openrouter" | "opencode" | "opencode-go" | "custom";
3
3
  export type AccessMode = "private" | "token" | "public";
4
4
  export type ToolVisibility = "user" | "internal";
5
- export type ChannelType = "website" | "telegram" | "whatsapp" | "discord" | "slack";
6
- export type ChannelProvider = "agentkit" | "telegram" | "zapster" | "meta" | "discord" | "slack";
7
- export type WhatsappChannelProvider = "zapster" | "meta";
5
+ export type ChannelType = "website" | "telegram" | "whatsapp" | "discord" | "slack" | "webhook";
6
+ export type ChannelProvider = "agentkit" | "telegram" | "zapster" | "meta" | "uazapi" | "evolution" | "discord" | "slack" | "generic";
7
+ export type WhatsappChannelProvider = "zapster" | "meta" | "uazapi" | "evolution";
8
8
  export type DiscordChannelMode = "interactions" | "bot";
9
9
  export type TelegramAllowedUpdate = "message";
10
10
  export type ChannelAudioMode = "off" | "reject" | "transcribe";
@@ -180,12 +180,30 @@ export type SlackChannelConfig = {
180
180
  audio?: never;
181
181
  };
182
182
 
183
+ export type GenericWebhookChannelInput = {
184
+ name: string;
185
+ secrets?: string[];
186
+ limits?: ChannelLimits;
187
+ buffer?: ChannelBufferInput;
188
+ };
189
+
190
+ export type GenericWebhookChannelConfig = {
191
+ type: "webhook";
192
+ provider: "generic";
193
+ name: string;
194
+ secrets: string[];
195
+ limits?: ChannelLimits;
196
+ buffer?: ChannelBufferConfig;
197
+ audio?: never;
198
+ };
199
+
183
200
  export type AgentChannel =
184
201
  | WebsiteChannelConfig
185
202
  | TelegramChannelConfig
186
203
  | WhatsappChannelConfig
187
204
  | DiscordChannelConfig
188
- | SlackChannelConfig;
205
+ | SlackChannelConfig
206
+ | GenericWebhookChannelConfig;
189
207
 
190
208
  export type IntegrationTagsConfig = {
191
209
  enable?: string[];
@@ -495,6 +513,17 @@ export function slackChannel(input: SlackChannelInput): SlackChannelConfig {
495
513
  };
496
514
  }
497
515
 
516
+ export function webhookChannel(input: GenericWebhookChannelInput): GenericWebhookChannelConfig {
517
+ return {
518
+ type: "webhook",
519
+ provider: "generic",
520
+ name: input.name,
521
+ secrets: normalizeChannelSecrets(input.secrets ?? ["AGENTKIT_WEBHOOK_SECRET"], "generic webhook channel"),
522
+ ...(input.limits ? { limits: input.limits } : {}),
523
+ ...(input.buffer ? { buffer: normalizeChannelBuffer(input.buffer, "generic webhook channel") } : {}),
524
+ };
525
+ }
526
+
498
527
  function defaultDiscordChannelSecrets(mode: DiscordChannelMode): string[] {
499
528
  return mode === "bot" ? ["DISCORD_BOT_TOKEN"] : ["DISCORD_PUBLIC_KEY"];
500
529
  }
@@ -504,6 +533,14 @@ function defaultWhatsappChannelSecrets(provider: WhatsappChannelProvider): strin
504
533
  return ["ZAPSTER_API_KEY", "ZAPSTER_INSTANCE_ID", "ZAPSTER_WEBHOOK_ID"];
505
534
  }
506
535
 
536
+ if (provider === "uazapi") {
537
+ return ["UAZAPI_BASE_URL", "UAZAPI_TOKEN"];
538
+ }
539
+
540
+ if (provider === "evolution") {
541
+ return ["EVOLUTION_API_BASE_URL", "EVOLUTION_API_KEY", "EVOLUTION_INSTANCE_NAME", "EVOLUTION_WEBHOOK_TOKEN"];
542
+ }
543
+
507
544
  return ["META_WHATSAPP_ACCESS_TOKEN", "META_WHATSAPP_APP_SECRET", "META_WHATSAPP_VERIFY_TOKEN"];
508
545
  }
509
546
 
@@ -17,7 +17,7 @@ import type { AgentProviderName, AgentTool } from "../index";
17
17
  import { AgentKitError } from "../runtime/errors";
18
18
  import type { ProviderAdapter, ProviderRunInput, ProviderRunResult } from "./types";
19
19
 
20
- const PI_PROVIDER_NAMES = ["openai", "anthropic", "openrouter"] as const satisfies AgentProviderName[];
20
+ const PI_PROVIDER_NAMES = ["openai", "anthropic", "openrouter", "opencode", "opencode-go"] as const satisfies AgentProviderName[];
21
21
  const MAX_TOOL_TURNS = 8;
22
22
  const OPENROUTER_BASE_URL = "https://openrouter.ai/api/v1";
23
23
  const UNKNOWN_OPENROUTER_CONTEXT_WINDOW = 128000;
@@ -159,7 +159,13 @@ async function runPiTurn(
159
159
  }
160
160
 
161
161
  function toPiProvider(provider: string): Provider {
162
- if (provider === "openai" || provider === "anthropic" || provider === "openrouter") {
162
+ if (
163
+ provider === "openai" ||
164
+ provider === "anthropic" ||
165
+ provider === "openrouter" ||
166
+ provider === "opencode" ||
167
+ provider === "opencode-go"
168
+ ) {
163
169
  return provider;
164
170
  }
165
171
 
@@ -181,6 +187,10 @@ function requireProviderApiKey(provider: Provider, env: Record<string, string |
181
187
  }
182
188
 
183
189
  function defaultProviderSecretNames(provider: Provider): string[] {
190
+ if (provider === "opencode" || provider === "opencode-go") {
191
+ return ["OPENCODE_API_KEY"];
192
+ }
193
+
184
194
  return [`${provider.toUpperCase().replace(/[^A-Z0-9]+/g, "_")}_API_KEY`];
185
195
  }
186
196
 
@@ -15,11 +15,24 @@ export type ChannelFixtureName =
15
15
  | "zapster-audio-message"
16
16
  | "zapster-duplicate-message"
17
17
  | "zapster-unsupported-media"
18
+ | "uazapi-message"
19
+ | "uazapi-audio-message"
20
+ | "uazapi-duplicate-message"
21
+ | "uazapi-unsupported-media"
22
+ | "uazapi-was-sent-by-api"
23
+ | "evolution-message"
24
+ | "evolution-audio-message"
25
+ | "evolution-captioned-media"
26
+ | "evolution-duplicate-message"
27
+ | "evolution-from-me"
28
+ | "evolution-unsupported-media"
18
29
  | "discord-command"
19
30
  | "discord-message-create"
20
31
  | "discord-ping"
21
32
  | "slack-app-mention"
22
- | "slack-direct-message";
33
+ | "slack-direct-message"
34
+ | "generic-webhook-message"
35
+ | "generic-webhook-duplicate";
23
36
 
24
37
  export type ChannelFixture = {
25
38
  name: ChannelFixtureName;
@@ -0,0 +1,225 @@
1
+ import { createHash, createHmac, timingSafeEqual } from "node:crypto";
2
+
3
+ import type { ChannelAdapter, ChannelSendInput, RawWebhookEvent } from "../channels";
4
+ import { AgentKitError } from "../errors";
5
+
6
+ const DEFAULT_SECRET_NAME = "AGENTKIT_WEBHOOK_SECRET";
7
+
8
+ export const genericWebhookChannelAdapter: ChannelAdapter = {
9
+ type: "webhook",
10
+ provider: "generic",
11
+ requiredSecrets: [DEFAULT_SECRET_NAME],
12
+ verifyWebhook(input) {
13
+ const expected = input.secrets[DEFAULT_SECRET_NAME];
14
+
15
+ if (!expected) {
16
+ return {
17
+ ok: false,
18
+ code: "channel_secret_missing",
19
+ message: `Secret ${DEFAULT_SECRET_NAME} is not set for this channel.`,
20
+ };
21
+ }
22
+
23
+ const signature = input.headers.get("x-agentkit-webhook-signature") ?? input.headers.get("x-agentkit-signature");
24
+
25
+ if (signature) {
26
+ const expectedSignature = `sha256=${createHmac("sha256", expected).update(input.rawBody).digest("hex")}`;
27
+
28
+ if (constantTimeEqual(signature.trim(), expectedSignature)) {
29
+ return { ok: true };
30
+ }
31
+
32
+ return {
33
+ ok: false,
34
+ code: "channel_signature_invalid",
35
+ message: "Generic webhook HMAC signature did not match.",
36
+ };
37
+ }
38
+
39
+ const bearer = readBearerToken(input.headers.get("authorization"));
40
+ const headerToken =
41
+ input.headers.get("x-agentkit-webhook-secret") ??
42
+ input.headers.get("x-agentkit-channel-token") ??
43
+ input.headers.get("x-agentkit-webhook-token");
44
+
45
+ if (constantTimeEqual(bearer, expected) || constantTimeEqual(headerToken ?? "", expected)) {
46
+ return { ok: true };
47
+ }
48
+
49
+ return {
50
+ ok: false,
51
+ code: "channel_signature_invalid",
52
+ message: "Generic webhook token did not match.",
53
+ };
54
+ },
55
+ normalizeInbound(input) {
56
+ const body = parseJsonObject(input);
57
+ const eventId = readFirstScalar(body, ["event_id", "eventId", "id"]) ?? readNestedMessageId(body);
58
+
59
+ if (!eventId) {
60
+ throw new AgentKitError("channel_payload_invalid", "Generic webhook payload is missing event_id.");
61
+ }
62
+
63
+ const text = readMessageText(body);
64
+ const identitySource =
65
+ readFirstScalar(body, [
66
+ "external_id",
67
+ "externalId",
68
+ "user_id",
69
+ "userId",
70
+ "contact_id",
71
+ "contactId",
72
+ "conversation_id",
73
+ "conversationId",
74
+ "client_id",
75
+ "clientId",
76
+ "visitor_id",
77
+ "visitorId",
78
+ "source_id",
79
+ "sourceId",
80
+ ]) ?? `event:${eventId}`;
81
+ const identityKey = `webhook:generic:identity:${sha256(identitySource).slice(0, 32)}`;
82
+ const base = {
83
+ type: "webhook" as const,
84
+ provider: "generic" as const,
85
+ providerEventId: eventId,
86
+ dedupeKey: `webhook:generic:${input.channelId}:${eventId}`,
87
+ externalIdentity: {
88
+ key: identityKey,
89
+ redactedDisplay: identityKey,
90
+ },
91
+ receivedAt: input.receivedAt,
92
+ };
93
+
94
+ if (!text) {
95
+ return [
96
+ {
97
+ ...base,
98
+ kind: "unsupported" as const,
99
+ unsupportedReason: "unsupported_message_type:missing_text",
100
+ },
101
+ ];
102
+ }
103
+
104
+ return [
105
+ {
106
+ ...base,
107
+ kind: "message" as const,
108
+ message: {
109
+ role: "user" as const,
110
+ contentType: "text" as const,
111
+ content: text,
112
+ providerMessageId: readNestedMessageId(body) ?? eventId,
113
+ },
114
+ },
115
+ ];
116
+ },
117
+ async sendMessage(input) {
118
+ return {
119
+ ok: true,
120
+ status: "stubbed",
121
+ providerRequestId: buildGenericWebhookProviderRequestId(input),
122
+ };
123
+ },
124
+ getStatus() {
125
+ return {
126
+ status: "connected",
127
+ checkedAt: new Date().toISOString(),
128
+ providerMetadata: {
129
+ outbound: "stubbed",
130
+ },
131
+ };
132
+ },
133
+ };
134
+
135
+ function buildGenericWebhookProviderRequestId(input: ChannelSendInput): string {
136
+ return `webhook:generic:${input.channelId}:${input.conversationId}`;
137
+ }
138
+
139
+ function parseJsonObject(input: RawWebhookEvent): Record<string, unknown> {
140
+ const contentType = input.headers.get("content-type") ?? "";
141
+
142
+ if (contentType && !contentType.toLowerCase().includes("application/json")) {
143
+ throw new AgentKitError("channel_payload_invalid", "Generic webhook payload must use content-type application/json.");
144
+ }
145
+
146
+ try {
147
+ const parsed = JSON.parse(input.rawBody) as unknown;
148
+
149
+ if (isRecord(parsed)) {
150
+ return parsed;
151
+ }
152
+ } catch (error) {
153
+ throw new AgentKitError("channel_payload_invalid", "Generic webhook payload must be valid JSON.", {
154
+ cause: error,
155
+ });
156
+ }
157
+
158
+ throw new AgentKitError("channel_payload_invalid", "Generic webhook payload must be a JSON object.");
159
+ }
160
+
161
+ function readMessageText(body: Record<string, unknown>): string | undefined {
162
+ const message = body.message;
163
+
164
+ if (typeof message === "string") {
165
+ return normalizeNonEmpty(message);
166
+ }
167
+
168
+ if (isRecord(message)) {
169
+ return readFirstScalar(message, ["text", "content", "body"]);
170
+ }
171
+
172
+ return readFirstScalar(body, ["text", "content", "prompt", "input"]);
173
+ }
174
+
175
+ function readNestedMessageId(body: Record<string, unknown>): string | undefined {
176
+ const message = body.message;
177
+ return isRecord(message) ? readFirstScalar(message, ["id", "message_id", "messageId"]) : undefined;
178
+ }
179
+
180
+ function readFirstScalar(value: Record<string, unknown>, keys: string[]): string | undefined {
181
+ for (const key of keys) {
182
+ const field = value[key];
183
+
184
+ if (typeof field === "string") {
185
+ const normalized = normalizeNonEmpty(field);
186
+
187
+ if (normalized) {
188
+ return normalized;
189
+ }
190
+ }
191
+
192
+ if (typeof field === "number" && Number.isFinite(field)) {
193
+ return String(field);
194
+ }
195
+ }
196
+
197
+ return undefined;
198
+ }
199
+
200
+ function normalizeNonEmpty(value: string): string | undefined {
201
+ const trimmed = value.trim();
202
+ return trimmed.length > 0 ? trimmed : undefined;
203
+ }
204
+
205
+ function readBearerToken(value: string | null): string {
206
+ if (!value?.startsWith("Bearer ")) {
207
+ return "";
208
+ }
209
+
210
+ return value.slice("Bearer ".length).trim();
211
+ }
212
+
213
+ function constantTimeEqual(left: string, right: string): boolean {
214
+ const leftDigest = createHash("sha256").update(left).digest();
215
+ const rightDigest = createHash("sha256").update(right).digest();
216
+ return timingSafeEqual(leftDigest, rightDigest) && left === right;
217
+ }
218
+
219
+ function sha256(value: string): string {
220
+ return createHash("sha256").update(value).digest("hex");
221
+ }
222
+
223
+ function isRecord(value: unknown): value is Record<string, unknown> {
224
+ return typeof value === "object" && value !== null && !Array.isArray(value);
225
+ }