@andreprado/agentkit 0.1.0-alpha.4 → 0.1.0-alpha.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/cli/index.ts CHANGED
@@ -1,14 +1,12 @@
1
1
  import { spawn } from "node:child_process";
2
- import { mkdir, readFile, rm, writeFile } from "node:fs/promises";
3
- import { createServer, type IncomingMessage, type ServerResponse } from "node:http";
4
- import { homedir } from "node:os";
2
+ import { mkdir, readFile, writeFile } from "node:fs/promises";
5
3
  import { dirname, join, relative, resolve } from "node:path";
6
4
 
7
5
  import { createProject } from "../create-project";
8
6
  import { buildAgentCapsule } from "../runtime/build";
9
7
  import { findAgentCapsuleRoot, loadAgentCapsule } from "../runtime/config";
10
8
  import { readLocalDeployStateIfExists, type LocalDeployState } from "../runtime/core/deploy-state";
11
- import { listTargetsWithCapability, resolveBuildTarget, resolveDeployTarget } from "../runtime/core/targets";
9
+ import { resolveBuildTarget, resolveDeployTarget } from "../runtime/core/targets";
12
10
  import { getConversationFromCwd, listConversationsFromCwd } from "../runtime/conversations";
13
11
  import { runAgentMessageFromCwd } from "../runtime/chat";
14
12
  import { deployBuildToAgentKitCloud, writeLocalDeployState } from "../runtime/deploy";
@@ -20,214 +18,49 @@ import {
20
18
  } from "../runtime/db-commands";
21
19
  import { runEvalsFromCwd } from "../runtime/evals";
22
20
  import { findAgentKitDocs } from "../runtime/docs";
23
- import { listCapsuleEnv, loadCapsuleEnv, setCapsuleEnv, unsetCapsuleEnv } from "../runtime/env";
24
- import { loadDeployReadinessContext, stableCloudProjectId, type DeployReadinessContext } from "../runtime/deploy-readiness";
21
+ import { listCapsuleEnv, setCapsuleEnv, unsetCapsuleEnv } from "../runtime/env";
25
22
  import { runToolFromCwd } from "../runtime/tool-runner";
26
23
  import { startAgentDevServer } from "../runtime/dev-server";
27
24
  import { inspectAgentCapsule } from "../runtime/inspect";
28
25
  import { AgentKitError, isAgentKitError } from "../runtime/errors";
29
26
  import { prepareVpsDeploy } from "../runtime/targets/vps/deploy";
30
-
31
- const defaultAgentKitCloudApiUrl = "https://agentkit-cloud.aibuilders.com.br";
32
- const defaultAgentKitDevPort = 4123;
33
- const defaultDeployChatAccessTokenName = "agentkit-chat-ui";
34
- const defaultDeployChatAccessTokenPath = ".agentkit/chat-access-token.json";
35
-
36
- function renderHelp(): string {
37
- return `AgentKit
38
-
39
- Create, run, inspect, and deploy Agent Capsules.
40
-
41
- Quick start:
42
- agentkit new support-agent --template support
43
- cd support-agent
44
- npm run dev
45
- npm run chat -- --message "hello"
46
-
47
- Usage:
48
- agentkit <command> [options]
49
-
50
- Start:
51
- agentkit new <name> [--template blank|support|dentista] [--no-install]
52
- agentkit dev
53
- agentkit open
54
- agentkit chat-ui --deploy
55
- agentkit chat --message <text> [--conversation-id <id>]
56
- agentkit inspect
57
-
58
- Build:
59
- agentkit tool <name> [--input <path-or-json>]
60
- agentkit db migrate
61
- agentkit eval run
62
- agentkit conversations list
63
-
64
- Deploy:
65
- agentkit login --token <token>
66
- agentkit deploy doctor
67
- agentkit secret set <NAME> --stdin
68
- agentkit secret sync --from-local
69
- agentkit deploy --smoke "hello"
70
- agentkit deploy status
71
-
72
- More:
73
- agentkit channels ...
74
- agentkit access token ...
75
- agentkit env ...
76
- agentkit docs full
77
- agentkit help commands
78
- `;
79
- }
80
-
81
- function renderCommandReference(): string {
82
- return `AgentKit Command Reference
83
-
84
- Usage:
85
- agentkit new <name> [--template blank|support|dentista] [--no-install]
86
- agentkit dev [--port <number>]
87
- agentkit open
88
- agentkit chat-ui --deploy [--port <number>] [--token-file <path>]
89
- agentkit chat --message <text> [--conversation-id <id>]
90
- agentkit tool <name> [--input <path-or-json>]
91
- agentkit db migrate
92
- agentkit db reset --yes
93
- agentkit db shell
94
- agentkit db seed [--file <path>]
95
- agentkit eval run
96
- agentkit conversations list
97
- agentkit conversations show <conversation-id>
98
- agentkit channels list
99
- agentkit channels add <website|telegram|whatsapp> <name> [--provider zapster|meta] [--api <url>]
100
- agentkit channels setup <name> [--apply] [--api <url>]
101
- agentkit channels status <name> [--api <url>]
102
- agentkit channels test <name> [--message <text>] [--fixture <path>] [--api <url>]
103
- agentkit channels deliveries list <name> [--api <url>]
104
- agentkit channels deliveries show <delivery-id> [--api <url>]
105
- agentkit inspect
106
- agentkit build [--target cloudflare|container]
107
- agentkit login --token <token> [--api <url>]
108
- agentkit logout
109
- agentkit deploy [--target cloudflare|vps] [--host <host>] [--api <url>] [--dry-run] [--anonymous] [--local-wrangler] [--smoke <message>]
110
- agentkit deploy doctor [--api <url>] [--anonymous]
111
- agentkit deploy smoke [--message <text>] [--api <url>]
112
- agentkit deploy status [--api <url>]
113
- agentkit deploy pause [--api <url>]
114
- agentkit deploy resume [--api <url>]
115
- agentkit secret set <NAME> <VALUE> [--api <url>]
116
- agentkit secret set <NAME> --stdin [--api <url>]
117
- agentkit secret set <NAME> --from-env [ENV_NAME] [--api <url>]
118
- agentkit secret set <NAME> --from-local-env [--api <url>]
119
- agentkit secret sync --from-local [--api <url>]
120
- agentkit secret list [--api <url>]
121
- agentkit secret unset <NAME> [--api <url>]
122
- agentkit access token create <name> [--api <url>] [--out <path>]
123
- agentkit access token list [--api <url>]
124
- agentkit access token revoke <token-id> [--api <url>]
125
- agentkit env set <NAME> <VALUE>
126
- agentkit env set <NAME> --stdin
127
- agentkit env set <NAME> --from-env [ENV_NAME]
128
- agentkit env list
129
- agentkit env unset <NAME>
130
- agentkit docs path
131
- agentkit docs llms
132
- agentkit docs full
133
- agentkit handoff codex [goal]
134
- agentkit help commands
135
- agentkit --help
136
-
137
- Commands:
138
- new Create a new Agent Capsule
139
- dev Run the Agent Capsule locally
140
- open Open the local Agent Capsule UI if it is already running
141
- chat-ui Run a local chat UI, optionally pointed at a hosted deploy
142
- chat Send one message to the local Agent Capsule
143
- tool Run one registered tool directly
144
- db Manage the local SQLite database
145
- eval Run local eval files
146
- conversations List and inspect stored local conversations
147
- channels Manage hosted channel resources
148
- inspect Print machine-readable Agent Capsule state
149
- build Build a hosted deploy artifact
150
- login Store an AgentKit Cloud alpha token outside the capsule
151
- logout Remove the stored AgentKit Cloud alpha token
152
- deploy Build and send a hosted artifact to AgentKit Cloud
153
- secret Manage hosted AgentKit Cloud secrets without readback
154
- access Manage hosted deploy access tokens
155
- env Manage local .env values without opening code
156
- docs Print AgentKit docs locations
157
- handoff Print a ready-to-paste prompt for another coding agent
158
- help Print focused help or the full command reference
159
-
160
- Examples:
161
- agentkit new support-agent --template support
162
- cd support-agent
163
- agentkit dev
164
- agentkit open
165
- agentkit tool lookup_order --input '{"orderId":"A100"}'
166
- agentkit eval run
167
-
168
- agentkit new clara-dentista --template dentista
169
- `;
170
- }
171
-
172
- type ParsedArgs = {
173
- command?: string;
174
- positional: string[];
175
- flags: Record<string, string | boolean>;
176
- };
177
-
178
- function parseArgs(argv: string[]): ParsedArgs {
179
- const [command, ...rest] = argv;
180
- const positional: string[] = [];
181
- const flags: Record<string, string | boolean> = {};
182
-
183
- for (let index = 0; index < rest.length; index += 1) {
184
- const value = rest[index];
185
-
186
- if (!value.startsWith("--")) {
187
- positional.push(value);
188
- continue;
189
- }
190
-
191
- const name = value.slice(2);
192
- const next = rest[index + 1];
193
-
194
- if (next && !next.startsWith("--")) {
195
- flags[name] = next;
196
- index += 1;
197
- } else {
198
- flags[name] = true;
199
- }
200
- }
201
-
202
- return { command, positional, flags };
203
- }
204
-
205
- function isHelpRequested(args: ParsedArgs): boolean {
206
- return args.flags.help === true || args.flags.h === true || args.positional.includes("--help") || args.positional.includes("-h");
207
- }
208
-
209
- function renderCommandHelp(command: string): string {
210
- const usage: Record<string, string> = {
211
- new: "agentkit new <name> [--template blank|support|dentista] [--no-install]",
212
- dev: "agentkit dev [--port <number>]",
213
- open: "agentkit open",
214
- "chat-ui": "agentkit chat-ui --deploy [--port <number>] [--token-file <path>]",
215
- chat: 'agentkit chat --message "hello" [--conversation-id <id>]',
216
- tool: "agentkit tool <name> [--input <path-or-json>]",
217
- deploy:
218
- 'agentkit deploy [--smoke "hello"] | agentkit deploy smoke [--message "hello"] | agentkit deploy doctor | status | pause | resume',
219
- secret:
220
- "agentkit secret set <NAME> --stdin | --from-env [ENV_NAME] | --from-local-env | agentkit secret sync --from-local | list | unset <NAME>",
221
- access: "agentkit access token create <name> [--out <path>] | list | revoke <token-id>",
222
- env: "agentkit env set <NAME> --stdin | --from-env [ENV_NAME] | list | unset <NAME>",
223
- docs: "agentkit docs path | llms | full",
224
- handoff: "agentkit handoff codex [goal] | agentkit handoff claude [goal]",
225
- };
226
-
227
- return `Usage: ${usage[command] ?? "agentkit <command> [options]"}
228
-
229
- Run \`agentkit help commands\` for the full command reference.`;
230
- }
27
+ import { parseArgs, isHelpRequested, type ParsedArgs } from "./args";
28
+ import {
29
+ defaultAgentKitDevPort,
30
+ defaultDeployChatAccessTokenName,
31
+ defaultDeployChatAccessTokenPath,
32
+ } from "./constants";
33
+ import {
34
+ clearCloudAuth,
35
+ cloudApiRequest,
36
+ readCloudAuth,
37
+ readCloudError,
38
+ resolveCloudApiUrl,
39
+ verifyCloudLogin,
40
+ writeCloudAuth,
41
+ type CloudMeResponse,
42
+ type DeployAccessTokenCreateResponse,
43
+ type DeployAccessTokenListResponse,
44
+ } from "./cloud-client";
45
+ import {
46
+ checkDeployReadiness,
47
+ formatReadinessError,
48
+ printDeployReadinessReport,
49
+ resolveProjectIdForSecretCommand,
50
+ syncHostedSecretsFromLocal,
51
+ } from "./deploy-readiness";
52
+ import { readDeployAccessTokenForUi, startDeployChatUiServer } from "./deploy-chat-ui";
53
+ import {
54
+ parseCloudApiUrl,
55
+ parseOptionalStringFlag,
56
+ parsePort,
57
+ parseRequiredStringFlag,
58
+ parseTargetFlag,
59
+ resolveSecretInputValue,
60
+ } from "./flags";
61
+ import { renderCommandHelp, renderCommandReference, renderHelp } from "./help";
62
+ import { runInheritedCommand, waitForShutdown } from "./process";
63
+ import { handleChannelsCommand } from "./commands/channels";
231
64
 
232
65
  async function main() {
233
66
  const args = parseArgs(process.argv.slice(2));
@@ -956,1379 +789,231 @@ Final response:
956
789
  `;
957
790
  }
958
791
 
959
- type DeployReadinessStatus = "pass" | "fail" | "warn" | "info";
960
-
961
- type DeployReadinessCheck = {
962
- status: DeployReadinessStatus;
963
- title: string;
964
- message: string;
965
- action?: string;
966
- };
792
+ function isRecord(value: unknown): value is Record<string, unknown> {
793
+ return typeof value === "object" && value !== null && !Array.isArray(value);
794
+ }
967
795
 
968
- type DeployReadinessReport = {
969
- ok: boolean;
970
- apiUrl: string;
971
- context: DeployReadinessContext;
972
- checks: DeployReadinessCheck[];
973
- };
974
-
975
- type CloudMeResponse = {
976
- account?: {
977
- email?: string;
978
- status?: string;
979
- };
980
- };
981
-
982
- type CloudCapabilitiesResponse = {
983
- capabilities?: Array<{
984
- name?: string;
985
- status?: string;
986
- }>;
987
- };
988
-
989
- type CloudLimitsResponse = {
990
- online_agents?: {
991
- account?: {
992
- used?: number | null;
993
- limit?: number | null;
994
- };
995
- global?: {
996
- used?: number | null;
997
- limit?: number | null;
998
- };
999
- };
1000
- };
1001
-
1002
- type DeployAccessTokenCreateResponse = {
1003
- access_token?: {
1004
- id?: string;
1005
- deploy_id?: string;
1006
- name?: string;
1007
- token?: string;
1008
- };
1009
- };
1010
-
1011
- type DeployAccessTokenListResponse = {
1012
- access_tokens?: Array<{
1013
- id?: string;
1014
- deploy_id?: string;
1015
- name?: string;
1016
- }>;
1017
- };
1018
-
1019
- type CloudSecretsResponse = {
1020
- secrets?: Array<{
1021
- name?: string;
1022
- status?: string;
1023
- updated_at?: string;
1024
- }>;
1025
- };
1026
-
1027
- async function checkDeployReadiness(options: {
796
+ async function ensureDeployChatAccessToken(options: {
1028
797
  apiUrl: string;
1029
- requireLogin: boolean;
1030
- }): Promise<DeployReadinessReport> {
1031
- const context = await loadDeployReadinessContext(process.cwd(), process.env);
1032
- const auth = await readCloudAuth();
1033
- const checks: DeployReadinessCheck[] = [];
1034
- let cloudAuthUsable = false;
1035
-
1036
- if (context.runtime !== "edge") {
1037
- checks.push({
1038
- status: "fail",
1039
- title: "Target runtime",
1040
- message: 'Cloudflare deploy requires runtime: "edge" in agentkit.config.ts.',
1041
- action: "set runtime: \"edge\" before deploying to AgentKit Cloud",
1042
- });
1043
- } else {
1044
- checks.push({
1045
- status: "pass",
1046
- title: "Target runtime",
1047
- message: "Capsule is configured for edge deploy.",
1048
- });
1049
- }
1050
-
1051
- if (!auth?.token) {
1052
- checks.push({
1053
- status: options.requireLogin ? "fail" : "warn",
1054
- title: "Login",
1055
- message: options.requireLogin
1056
- ? "No AgentKit Cloud token was found for hosted production deploy."
1057
- : "No AgentKit Cloud token was found; deploy will request anonymous mode.",
1058
- action: "agentkit login --token <token>",
1059
- });
1060
- } else {
1061
- try {
1062
- const me = (await cloudApiRequest(options.apiUrl, "/v1/me", { method: "GET" })) as CloudMeResponse;
1063
- const email = me.account?.email;
1064
- checks.push({
1065
- status: "pass",
1066
- title: "Login",
1067
- message: email ? `Authenticated as ${email}.` : "AgentKit Cloud token is accepted.",
1068
- });
1069
- cloudAuthUsable = true;
1070
- } catch (error) {
1071
- checks.push({
1072
- status: "fail",
1073
- title: "Login",
1074
- message: `AgentKit Cloud rejected the token from ${formatCloudAuthSource(auth)}. ${formatReadinessError(error)}`,
1075
- action: "agentkit login --token <token>",
1076
- });
1077
- }
798
+ root: string;
799
+ deployId: string;
800
+ skip: boolean;
801
+ }): Promise<
802
+ | { status: "ready"; path: string; tokenId: string; created: boolean }
803
+ | { status: "skipped" }
804
+ | { status: "failed"; error: unknown }
805
+ > {
806
+ if (options.skip) {
807
+ return { status: "skipped" };
1078
808
  }
1079
809
 
1080
- if (cloudAuthUsable) {
1081
- try {
1082
- const payload = (await cloudApiRequest(options.apiUrl, "/v1/me/capabilities", {
1083
- method: "GET",
1084
- })) as CloudCapabilitiesResponse;
1085
- const capabilities = payload.capabilities ?? [];
1086
- const hasCloudflareAlpha = capabilities.some(
1087
- (capability) => capability.name === "cloudflare_deploy_alpha" && capability.status === "active",
1088
- );
810
+ try {
811
+ const path = join(options.root, defaultDeployChatAccessTokenPath);
812
+ const existing = await readDeployAccessTokenFile(path);
1089
813
 
1090
- checks.push({
1091
- status: hasCloudflareAlpha ? "pass" : "fail",
1092
- title: "Deploy entitlement",
1093
- message: hasCloudflareAlpha
1094
- ? "cloudflare_deploy_alpha is active."
1095
- : "This account is missing the cloudflare_deploy_alpha entitlement.",
1096
- ...(hasCloudflareAlpha ? {} : { action: "log in with an invited AgentKit Cloud alpha token" }),
1097
- });
1098
- } catch (error) {
1099
- checks.push({
1100
- status: "fail",
1101
- title: "Deploy entitlement",
1102
- message: `Could not verify AgentKit Cloud capabilities. ${formatReadinessError(error)}`,
1103
- });
1104
- }
814
+ if (existing?.deploy_id === options.deployId && existing.id) {
815
+ const listed = (await cloudApiRequest(
816
+ options.apiUrl,
817
+ `/v1/deploys/${encodeURIComponent(options.deployId)}/access-tokens`,
818
+ { method: "GET" },
819
+ )) as DeployAccessTokenListResponse;
820
+ const stillExists = (listed.access_tokens ?? []).some((token) => token.id === existing.id);
1105
821
 
1106
- try {
1107
- const payload = (await cloudApiRequest(options.apiUrl, "/v1/me/limits", {
1108
- method: "GET",
1109
- })) as CloudLimitsResponse;
1110
- const account = payload.online_agents?.account;
1111
- const global = payload.online_agents?.global;
1112
- const accountLimit = normalizeOptionalNumber(account?.limit);
1113
- const accountUsed = normalizeOptionalNumber(account?.used);
1114
- const globalLimit = normalizeOptionalNumber(global?.limit);
1115
- const globalUsed = normalizeOptionalNumber(global?.used);
1116
-
1117
- if (accountLimit !== undefined && accountUsed !== undefined && accountUsed >= accountLimit) {
1118
- checks.push({
1119
- status: "fail",
1120
- title: "Online agent capacity",
1121
- message: `This account is already using ${accountUsed}/${accountLimit} online AgentKit deploys.`,
1122
- action: "agentkit deploy pause",
1123
- });
1124
- } else if (globalLimit !== undefined && globalUsed !== undefined && globalUsed >= globalLimit) {
1125
- checks.push({
1126
- status: "fail",
1127
- title: "Online agent capacity",
1128
- message: `AgentKit Cloud is already using ${globalUsed}/${globalLimit} global online deploy slots.`,
1129
- action: "agentkit deploy status",
1130
- });
1131
- } else if (accountLimit !== undefined && accountUsed !== undefined) {
1132
- checks.push({
1133
- status: "pass",
1134
- title: "Online agent capacity",
1135
- message: `Account has capacity: ${accountUsed}/${accountLimit} online deploys are in use.`,
1136
- });
1137
- } else if (globalLimit !== undefined && globalUsed !== undefined) {
1138
- checks.push({
1139
- status: "pass",
1140
- title: "Online agent capacity",
1141
- message: `Global capacity is available: ${globalUsed}/${globalLimit} online deploys are in use.`,
1142
- });
1143
- } else {
1144
- checks.push({
1145
- status: "info",
1146
- title: "Online agent capacity",
1147
- message: "No online deploy limit is configured for this account.",
1148
- });
822
+ if (stillExists) {
823
+ return { status: "ready", path, tokenId: existing.id, created: false };
1149
824
  }
1150
- } catch (error) {
1151
- checks.push({
1152
- status: "warn",
1153
- title: "Online agent capacity",
1154
- message: `Could not verify online deploy capacity. ${formatReadinessError(error)}`,
1155
- });
1156
825
  }
1157
- }
1158
-
1159
- if (context.agentKitManagedSecrets.length > 0) {
1160
- checks.push({
1161
- status: "info",
1162
- title: "AgentKit-managed secrets",
1163
- message: `${context.agentKitManagedSecrets.join(", ")} ${
1164
- context.agentKitManagedSecrets.length === 1 ? "is" : "are"
1165
- } provisioned by AgentKit Cloud during deploy.`,
1166
- });
1167
- }
1168
826
 
1169
- if (context.accessMode === "private") {
1170
- checks.push({
1171
- status: "info",
1172
- title: "Private access",
1173
- message: "AGENTKIT_PRIVATE_TOKEN is required at runtime; AgentKit Cloud generates it and saves it in .agentkit/deploy.json.",
1174
- });
1175
- }
827
+ const created = (await cloudApiRequest(
828
+ options.apiUrl,
829
+ `/v1/deploys/${encodeURIComponent(options.deployId)}/access-tokens`,
830
+ {
831
+ method: "POST",
832
+ body: JSON.stringify({ name: defaultDeployChatAccessTokenName }),
833
+ },
834
+ )) as DeployAccessTokenCreateResponse;
835
+ const token = created.access_token;
1176
836
 
1177
- if (context.userManagedSecrets.length === 0) {
1178
- checks.push({
1179
- status: "pass",
1180
- title: "Hosted secrets",
1181
- message: "No user-managed hosted secrets are required by this capsule.",
1182
- });
1183
- } else if (!cloudAuthUsable) {
1184
- checks.push({
1185
- status: options.requireLogin ? "fail" : "warn",
1186
- title: "Hosted secrets",
1187
- message: `Could not check hosted secrets without a valid AgentKit Cloud login. Required: ${context.userManagedSecrets.join(", ")}.`,
1188
- action: "agentkit secret list",
1189
- });
1190
- } else {
1191
- try {
1192
- const payload = (await cloudApiRequest(
1193
- options.apiUrl,
1194
- `/v1/projects/${encodeURIComponent(context.projectId)}/secrets`,
1195
- { method: "GET" },
1196
- )) as CloudSecretsResponse;
1197
- const hostedSecretNames = new Set((payload.secrets ?? []).map((secret) => secret.name).filter(isString));
1198
- const missing = context.userManagedSecrets.filter((name) => !hostedSecretNames.has(name));
1199
-
1200
- if (missing.length === 0) {
1201
- checks.push({
1202
- status: "pass",
1203
- title: "Hosted secrets",
1204
- message: "All user-managed hosted secrets are set.",
1205
- });
1206
- } else {
1207
- for (const secret of missing) {
1208
- const localSource = context.localSecrets[secret];
1209
- checks.push({
1210
- status: "fail",
1211
- title: `Hosted secret ${secret}`,
1212
- message: localSource
1213
- ? `${secret} is present locally but has not been set in AgentKit Cloud.`
1214
- : `${secret} is required but missing from AgentKit Cloud.`,
1215
- action: secretSetCommand(secret, localSource),
1216
- });
1217
- }
1218
- }
1219
- } catch (error) {
1220
- checks.push({
1221
- status: "fail",
1222
- title: "Hosted secrets",
1223
- message: `Could not list hosted secrets for project ${context.projectId}. ${formatReadinessError(error)}`,
1224
- action: "agentkit secret list",
1225
- });
837
+ if (!token?.id || token.deploy_id !== options.deployId || !token.token) {
838
+ throw new AgentKitError("access_token_invalid", "AgentKit Cloud returned an invalid deploy access token payload.");
1226
839
  }
840
+
841
+ await writeSecretJson(path, created);
842
+ return { status: "ready", path, tokenId: token.id, created: true };
843
+ } catch (error) {
844
+ return { status: "failed", error };
1227
845
  }
846
+ }
1228
847
 
1229
- return {
1230
- ok: checks.every((check) => check.status !== "fail"),
1231
- apiUrl: options.apiUrl,
1232
- context,
1233
- checks,
1234
- };
848
+ async function readDeployAccessTokenFile(path: string): Promise<{ id?: string; deploy_id?: string } | null> {
849
+ try {
850
+ const parsed = JSON.parse(await readFile(path, "utf8")) as DeployAccessTokenCreateResponse;
851
+ return parsed.access_token ?? null;
852
+ } catch {
853
+ return null;
854
+ }
1235
855
  }
1236
856
 
1237
- function printDeployReadinessReport(report: DeployReadinessReport): void {
1238
- console.log("AgentKit deploy readiness");
1239
- console.log(`Project: ${report.context.projectName}`);
1240
- console.log(`Project ID: ${report.context.projectId}`);
1241
- console.log(`Target: ${report.context.target}`);
1242
- console.log(`API: ${report.apiUrl}`);
1243
- console.log("");
857
+ async function writeSecretJson(path: string, value: unknown): Promise<void> {
858
+ await mkdir(dirname(path), { recursive: true });
859
+ await writeFile(path, `${JSON.stringify(value, null, 2)}\n`, { mode: 0o600 });
860
+ }
1244
861
 
1245
- for (const check of report.checks) {
1246
- console.log(`[${check.status}] ${check.title}: ${check.message}`);
862
+ async function requireLocalDeployState(): Promise<LocalDeployState> {
863
+ const state = await readLocalDeployStateIfExists(process.cwd());
1247
864
 
1248
- if (check.action) {
1249
- console.log(` Run: ${check.action}`);
1250
- }
865
+ if (!state) {
866
+ throw new Error("No .agentkit/deploy.json found. Run agentkit deploy first.");
1251
867
  }
868
+
869
+ return state;
1252
870
  }
1253
871
 
1254
- function secretSetCommand(secret: string, source: DeployReadinessContext["localSecrets"][string] | undefined): string {
1255
- if (source?.dotenv) {
1256
- return `agentkit secret set ${secret} --from-local-env`;
872
+ function requireDeployUrl(state: Pick<LocalDeployState, "deploy_id" | "url">): string {
873
+ if (!state.url) {
874
+ throw new Error(`Deploy ${state.deploy_id} does not have a URL. Run agentkit deploy again.`);
1257
875
  }
1258
876
 
1259
- if (source?.environment) {
1260
- return `agentkit secret set ${secret} --from-env`;
877
+ return state.url;
878
+ }
879
+
880
+ async function readDeployAccessTokenForSmoke(cwd: string, state: LocalDeployState): Promise<string | undefined> {
881
+ if (state.access?.mode === "public") {
882
+ return undefined;
1261
883
  }
1262
884
 
1263
- return `agentkit secret set ${secret} --stdin`;
885
+ const root = await findAgentCapsuleRoot(cwd);
886
+ return readDeployAccessTokenForUi(join(root, defaultDeployChatAccessTokenPath));
1264
887
  }
1265
888
 
1266
- async function syncHostedSecretsFromLocal(options: { apiUrl: string; projectId: string }): Promise<void> {
1267
- const context = await loadDeployReadinessContext(process.cwd(), process.env);
1268
- const localEnv = await loadCapsuleEnv(context.root, {});
1269
- let synced = 0;
1270
- let skipped = 0;
889
+ async function runDeploySmoke(input: { deployUrl: string; message: string; token?: string }): Promise<void> {
890
+ const headers = new Headers({ "Content-Type": "application/json" });
1271
891
 
1272
- for (const secret of context.userManagedSecrets) {
1273
- const value = localEnv[secret];
892
+ if (input.token) {
893
+ headers.set("Authorization", `Bearer ${input.token}`);
894
+ }
1274
895
 
1275
- if (value === undefined) {
1276
- skipped += 1;
1277
- console.log(`${secret}\tskipped_missing_local`);
1278
- continue;
1279
- }
896
+ let response: Response;
1280
897
 
1281
- await cloudApiRequest(options.apiUrl, `/v1/projects/${encodeURIComponent(options.projectId)}/secrets/${encodeURIComponent(secret)}`, {
1282
- method: "PUT",
1283
- body: JSON.stringify({ value }),
898
+ try {
899
+ response = await fetch(new URL("/v1/chat", input.deployUrl).href, {
900
+ method: "POST",
901
+ headers,
902
+ body: JSON.stringify({ message: input.message }),
1284
903
  });
1285
- synced += 1;
1286
- console.log(`${secret}\tsynced`);
904
+ } catch (error) {
905
+ throw new AgentKitError(
906
+ "deploy_smoke_failed",
907
+ `Deploy smoke request failed before a response: ${error instanceof Error ? error.message : String(error)}`,
908
+ );
1287
909
  }
1288
910
 
1289
- if (context.userManagedSecrets.length === 0) {
1290
- console.log("No user-managed hosted secrets are declared by this capsule.");
1291
- return;
911
+ const payload = await response.json().catch(() => null);
912
+
913
+ if (!response.ok) {
914
+ throw new AgentKitError("deploy_smoke_failed", readCloudError(payload, response.status));
1292
915
  }
1293
916
 
917
+ const content =
918
+ payload && typeof payload === "object" && "message" in payload && isRecord((payload as { message?: unknown }).message)
919
+ ? (payload as { message: { content?: unknown } }).message.content
920
+ : undefined;
921
+
1294
922
  console.log("");
1295
- console.log(`${synced} synced, ${skipped} skipped`);
923
+ console.log("Deploy smoke verified");
924
+ console.log(`URL: ${input.deployUrl}`);
925
+ console.log(`Message: ${input.message}`);
926
+ if (typeof content === "string") {
927
+ console.log(`Response: ${content}`);
928
+ }
1296
929
  }
1297
930
 
1298
- function normalizeOptionalNumber(value: number | null | undefined): number | undefined {
1299
- return typeof value === "number" && Number.isFinite(value) ? value : undefined;
931
+ function printJson(value: unknown): void {
932
+ console.log(JSON.stringify(value, null, 2));
1300
933
  }
1301
934
 
1302
- function formatReadinessError(error: unknown): string {
1303
- if (isAgentKitError(error)) {
1304
- return `${error.code}: ${error.message}`;
1305
- }
935
+ async function printJsonOrWrite(value: unknown, outFlag: string | boolean | undefined, usage: string): Promise<void> {
936
+ const outPath = parseOptionalStringFlag(outFlag, "out", usage);
1306
937
 
1307
- return error instanceof Error ? error.message : String(error);
1308
- }
938
+ if (!outPath) {
939
+ printJson(value);
940
+ return;
941
+ }
1309
942
 
1310
- function isString(value: unknown): value is string {
1311
- return typeof value === "string";
943
+ const absolutePath = resolve(process.cwd(), outPath);
944
+ await writeSecretJson(absolutePath, value);
945
+ console.log(`Deploy access token written to ${absolutePath}`);
1312
946
  }
1313
947
 
1314
- function isRecord(value: unknown): value is Record<string, unknown> {
1315
- return typeof value === "object" && value !== null && !Array.isArray(value);
948
+ function relativePath(root: string, path: string): string {
949
+ const output = relative(root, path);
950
+ return output.startsWith("..") ? path : output || ".";
1316
951
  }
1317
952
 
1318
- async function handleChannelsCommand(args: ParsedArgs): Promise<void> {
1319
- const [subcommand, first, second, third] = args.positional;
1320
-
1321
- if (subcommand === "list") {
1322
- const deployState = await readDeployStateIfExists(process.cwd());
953
+ function printBuildResult(result: Awaited<ReturnType<typeof buildAgentCapsule>>): void {
954
+ console.log("AgentKit build complete");
955
+ console.log("");
956
+ console.log(`Target: ${result.target}`);
957
+ console.log(`Build dir: ${result.buildDir}`);
958
+ console.log(`Manifest: ${result.manifestPath}`);
1323
959
 
1324
- if (deployState) {
1325
- const channels = await cloudGet<{ channels: CloudChannel[] }>(
1326
- parseCloudApiUrl(args.flags.api),
1327
- `/v1/deploys/${encodeURIComponent(deployState.deploy_id)}/channels`,
1328
- );
1329
- printChannelsTable(channels.channels);
1330
- return;
960
+ if (result.target === "cloudflare") {
961
+ console.log(`Worker: ${result.workerPath}`);
962
+ console.log(`Wrangler: ${result.wranglerPath}`);
963
+ if (result.tursoSchemaPath) {
964
+ console.log(`Turso: ${result.tursoSchemaPath}`);
1331
965
  }
966
+ console.log(`Bundle: ${result.bundleSha256}`);
967
+ }
1332
968
 
1333
- const capsule = await loadAgentCapsule(process.cwd());
1334
- printChannelsTable(
1335
- (capsule.config.channels ?? []).map((channel) => ({
1336
- id: "",
1337
- name: channel.name,
1338
- type: channel.type,
1339
- provider: channel.provider,
1340
- status: "local_config",
1341
- webhook_url: "",
1342
- required_secrets: channel.secrets.map((name) => ({ name, status: "missing" })),
1343
- })),
1344
- );
1345
- return;
969
+ if (result.target === "container") {
970
+ console.log(`Server: ${result.serverPath}`);
971
+ console.log(`Docker: ${result.dockerfilePath}`);
972
+ console.log(`Migrations:${result.migrationsDir}`);
1346
973
  }
1347
974
 
1348
- if (subcommand === "add") {
1349
- const type = first;
1350
- const name = second;
975
+ if (result.warnings.length > 0) {
976
+ console.log("");
977
+ console.log("Warnings:");
1351
978
 
1352
- if (!type || !name || (type !== "website" && type !== "telegram" && type !== "whatsapp")) {
1353
- throw new Error("Usage: agentkit channels add <website|telegram|whatsapp> <name> [--provider zapster|meta]");
979
+ for (const warning of result.warnings) {
980
+ console.log(` - ${warning}`);
1354
981
  }
1355
-
1356
- const deployState = await readDeployStateRequired(process.cwd());
1357
- const provider = channelProviderForCli(type, args.flags.provider);
1358
- const payload = {
1359
- name,
1360
- type,
1361
- provider,
1362
- secrets: defaultChannelSecretsForCli(type, provider),
1363
- };
1364
- const response = await cloudPost<{ channel: CloudChannel }>(
1365
- parseCloudApiUrl(args.flags.api),
1366
- `/v1/deploys/${encodeURIComponent(deployState.deploy_id)}/channels`,
1367
- payload,
1368
- );
1369
- printChannelStatus(response.channel);
1370
- return;
1371
982
  }
983
+ }
1372
984
 
1373
- if (subcommand === "setup") {
1374
- if (!first) {
1375
- throw new Error("Usage: agentkit channels setup <name> [--apply]");
1376
- }
985
+ function printVpsDeployPlan(plan: Awaited<ReturnType<typeof prepareVpsDeploy>>): void {
986
+ console.log("");
987
+ console.log("VPS deploy plan ready");
988
+ console.log(`Host: ${plan.host}`);
989
+ console.log(`Compose: ${plan.composePath}`);
990
+ console.log(`Env: ${plan.envExamplePath}`);
991
+ console.log(`Secrets: ${plan.secretsPath}`);
992
+ console.log(`Handoff: ${plan.handoffPath}`);
993
+ console.log(`State: ${plan.statePath}`);
994
+ console.log("");
995
+ console.log("Commands:");
1377
996
 
1378
- const channel = await resolveCloudChannelByName(first, args.flags.api);
1379
- printChannelStatus(channel);
1380
- if (channel.type === "telegram") {
1381
- if (args.flags.apply) {
1382
- await applyTelegramWebhook(channel);
1383
- console.log("Telegram webhook updated.");
1384
- } else {
1385
- console.log(`Setup: call Telegram setWebhook with ${channel.webhook_url}`);
1386
- console.log("Use --apply to mutate Telegram webhook settings.");
1387
- }
1388
- } else if (channel.type === "whatsapp") {
1389
- console.log(`Next step: paste this URL into ${channel.provider}'s webhook settings.`);
1390
- } else {
1391
- console.log(`Website endpoint: ${channel.webhook_url}`);
1392
- console.log("Use this endpoint from the AgentKit website client or a signed server-to-server test.");
1393
- }
1394
- return;
997
+ for (const command of plan.commands) {
998
+ console.log(` ${command}`);
1395
999
  }
1000
+ }
1396
1001
 
1397
- if (subcommand === "status") {
1398
- if (!first) {
1399
- throw new Error("Usage: agentkit channels status <name>");
1400
- }
1002
+ async function deployCloudflare(wranglerPath: string, buildDir: string): Promise<void> {
1003
+ const exitCode = await runInheritedCommand("npx", ["wrangler", "deploy", "--config", wranglerPath], buildDir);
1401
1004
 
1402
- const channel = await resolveCloudChannelByName(first, args.flags.api);
1403
- printChannelStatus(channel);
1404
- await printChannelDeliverySummary(channel, args.flags.api);
1405
- return;
1005
+ if (exitCode !== 0) {
1006
+ throw new Error(`Cloudflare deploy failed with exit code ${exitCode}.`);
1406
1007
  }
1008
+ }
1407
1009
 
1408
- if (subcommand === "test") {
1409
- if (!first) {
1410
- throw new Error('Usage: agentkit channels test <name> [--message "hello"] [--fixture <path>]');
1411
- }
1412
-
1413
- const channel = await resolveCloudChannelByName(first, args.flags.api);
1414
- const apiUrl = parseCloudApiUrl(args.flags.api);
1415
- const body = await channelTestBody(channel, args.flags.message, args.flags.fixture);
1416
- const path = new URL(channel.webhook_url).pathname;
1417
- const headers = channelTestHeaders(channel);
1418
- const response = await cloudFetch(apiUrl, path, {
1419
- method: channel.type === "whatsapp" && channel.provider === "meta" ? "POST" : "POST",
1420
- headers,
1421
- body,
1422
- });
1423
- const payload = await response.json();
1010
+ async function installProjectDependencies(projectPath: string): Promise<void> {
1011
+ const exitCode = await runInheritedCommand("npm", ["install"], projectPath);
1424
1012
 
1425
- console.log(JSON.stringify(payload, null, 2));
1426
- if (!response.ok) {
1427
- process.exitCode = 1;
1428
- }
1429
- return;
1013
+ if (exitCode !== 0) {
1014
+ throw new Error(`Dependency install failed with exit code ${exitCode}. Run \`npm install\` in ${projectPath}.`);
1430
1015
  }
1431
-
1432
- if (subcommand === "deliveries") {
1433
- if (first === "list") {
1434
- if (!second) {
1435
- throw new Error("Usage: agentkit channels deliveries list <name>");
1436
- }
1437
-
1438
- const channel = await resolveCloudChannelByName(second, args.flags.api);
1439
- const response = await cloudGet<{ deliveries: CloudDelivery[] }>(
1440
- parseCloudApiUrl(args.flags.api),
1441
- `/v1/channels/${encodeURIComponent(channel.id)}/deliveries`,
1442
- );
1443
-
1444
- if (response.deliveries.length === 0) {
1445
- console.log("No channel deliveries found.");
1446
- return;
1447
- }
1448
-
1449
- console.log("ID\tCHANNEL\tDIRECTION\tSTATUS\tPROVIDER_EVENT\tRECEIVED");
1450
- for (const delivery of response.deliveries) {
1451
- console.log(
1452
- [
1453
- delivery.id,
1454
- delivery.channel_id,
1455
- delivery.direction,
1456
- delivery.status,
1457
- delivery.provider.event_id ?? "",
1458
- delivery.received_at,
1459
- ].join("\t"),
1460
- );
1461
- }
1462
- return;
1463
- }
1464
-
1465
- if (first === "show") {
1466
- if (!second) {
1467
- throw new Error("Usage: agentkit channels deliveries show <delivery-id>");
1468
- }
1469
-
1470
- const response = await cloudGet<{ delivery: CloudDelivery }>(
1471
- parseCloudApiUrl(args.flags.api),
1472
- `/v1/channel-deliveries/${encodeURIComponent(second)}`,
1473
- );
1474
- console.log(JSON.stringify(response.delivery, null, 2));
1475
- return;
1476
- }
1477
- }
1478
-
1479
- throw new Error(
1480
- "Usage: agentkit channels list | add | setup | status | test | deliveries list | deliveries show",
1481
- );
1482
- }
1483
-
1484
- type DeployState = LocalDeployState;
1485
-
1486
- type CloudChannel = {
1487
- id: string;
1488
- name: string;
1489
- type: "website" | "telegram" | "whatsapp";
1490
- provider: "agentkit" | "telegram" | "zapster" | "meta";
1491
- status: string;
1492
- webhook_url: string;
1493
- required_secrets: Array<{ name: string; status: string }>;
1494
- };
1495
-
1496
- type CloudDelivery = {
1497
- id: string;
1498
- channel_id: string;
1499
- direction: string;
1500
- status: string;
1501
- provider: {
1502
- event_id?: string;
1503
- };
1504
- received_at: string;
1505
- };
1506
-
1507
- async function applyTelegramWebhook(channel: CloudChannel): Promise<void> {
1508
- const botToken = process.env.TELEGRAM_BOT_TOKEN;
1509
- const secretToken = process.env.TELEGRAM_WEBHOOK_SECRET;
1510
-
1511
- if (!botToken || !secretToken) {
1512
- throw new Error("TELEGRAM_BOT_TOKEN and TELEGRAM_WEBHOOK_SECRET must be set to apply Telegram setup.");
1513
- }
1514
-
1515
- let response: Response;
1516
-
1517
- try {
1518
- response = await fetch(`https://api.telegram.org/bot${botToken}/setWebhook`, {
1519
- method: "POST",
1520
- headers: { "Content-Type": "application/json" },
1521
- body: JSON.stringify({
1522
- url: channel.webhook_url,
1523
- secret_token: secretToken,
1524
- allowed_updates: ["message"],
1525
- }),
1526
- });
1527
- } catch {
1528
- throw new Error("Telegram setWebhook request failed before Telegram returned a response.");
1529
- }
1530
-
1531
- const payload = await response.json().catch(() => ({}));
1532
-
1533
- if (!response.ok || !isTelegramOk(payload)) {
1534
- throw new Error(readTelegramSetupError(payload, response.status));
1535
- }
1536
- }
1537
-
1538
- function isTelegramOk(payload: unknown): payload is { ok: true } {
1539
- return Boolean(payload && typeof payload === "object" && "ok" in payload && payload.ok === true);
1540
- }
1541
-
1542
- function readTelegramSetupError(payload: unknown, status: number): string {
1543
- if (payload && typeof payload === "object" && "description" in payload && typeof payload.description === "string") {
1544
- return `Telegram setWebhook failed: ${payload.description}`;
1545
- }
1546
-
1547
- return `Telegram setWebhook failed with HTTP ${status}.`;
1548
- }
1549
-
1550
- async function readDeployStateIfExists(cwd: string): Promise<DeployState | null> {
1551
- return readLocalDeployStateIfExists(cwd);
1552
- }
1553
-
1554
- async function readDeployStateRequired(cwd: string): Promise<DeployState> {
1555
- const state = await readDeployStateIfExists(cwd);
1556
-
1557
- if (!state?.deploy_id) {
1558
- throw new Error("No .agentkit/deploy.json found. Run agentkit deploy before creating hosted channels.");
1559
- }
1560
-
1561
- return state;
1562
- }
1563
-
1564
- function printChannelsTable(channels: CloudChannel[]): void {
1565
- if (channels.length === 0) {
1566
- console.log("No channels found.");
1567
- return;
1568
- }
1569
-
1570
- console.log("NAME\tTYPE\tPROVIDER\tSTATUS\tLAST_EVENT");
1571
- for (const channel of channels) {
1572
- console.log([channel.name, channel.type, channel.provider, channel.status, ""].join("\t"));
1573
- }
1574
- }
1575
-
1576
- function printChannelStatus(channel: CloudChannel): void {
1577
- console.log(`Channel: ${channel.name}`);
1578
- if (channel.id) {
1579
- console.log(`ID: ${channel.id}`);
1580
- }
1581
- console.log(`Type: ${channel.type}`);
1582
- console.log(`Provider: ${channel.provider}`);
1583
- console.log(`Status: ${channel.status}`);
1584
- if (channel.webhook_url) {
1585
- console.log(`Webhook URL: ${channel.webhook_url}`);
1586
- }
1587
- console.log("Required secrets:");
1588
- for (const secret of channel.required_secrets) {
1589
- console.log(` ${secret.name}: ${secret.status}`);
1590
- }
1591
- }
1592
-
1593
- async function printChannelDeliverySummary(
1594
- channel: CloudChannel,
1595
- apiFlag: string | boolean | undefined,
1596
- ): Promise<void> {
1597
- const response = await cloudGet<{ deliveries: CloudDelivery[] }>(
1598
- parseCloudApiUrl(apiFlag),
1599
- `/v1/channels/${encodeURIComponent(channel.id)}/deliveries`,
1600
- );
1601
- const inbound = [...response.deliveries].reverse().find((delivery) => delivery.direction === "inbound");
1602
- const outbound = [...response.deliveries].reverse().find((delivery) => delivery.direction === "outbound");
1603
-
1604
- console.log("Recent delivery:");
1605
- console.log(` Last inbound: ${inbound ? `${inbound.status} ${inbound.received_at}` : "none"}`);
1606
- console.log(` Last outbound: ${outbound ? `${outbound.status} ${outbound.received_at}` : "none"}`);
1607
- console.log("Provider health: not_checked");
1608
- }
1609
-
1610
- async function resolveCloudChannelByName(name: string, apiFlag: string | boolean | undefined): Promise<CloudChannel> {
1611
- const deployState = await readDeployStateRequired(process.cwd());
1612
- const response = await cloudGet<{ channels: CloudChannel[] }>(
1613
- parseCloudApiUrl(apiFlag),
1614
- `/v1/deploys/${encodeURIComponent(deployState.deploy_id)}/channels`,
1615
- );
1616
- const channel = response.channels.find((candidate) => candidate.name === name);
1617
-
1618
- if (!channel) {
1619
- throw new Error(`Channel "${name}" was not found for deploy ${deployState.deploy_id}.`);
1620
- }
1621
-
1622
- return channel;
1623
- }
1624
-
1625
- function channelProviderForCli(type: string, providerFlag: string | boolean | undefined): string {
1626
- if (type === "website") {
1627
- return "agentkit";
1628
- }
1629
-
1630
- if (type === "telegram") {
1631
- return "telegram";
1632
- }
1633
-
1634
- const provider = providerFlag ?? "zapster";
1635
-
1636
- if (provider !== "zapster" && provider !== "meta") {
1637
- throw new Error('WhatsApp provider must be "zapster" or "meta".');
1638
- }
1639
-
1640
- return provider;
1641
- }
1642
-
1643
- function defaultChannelSecretsForCli(type: string, provider: string): string[] {
1644
- if (type === "website") {
1645
- return ["AGENTKIT_WEBSITE_CHANNEL_TOKEN"];
1646
- }
1647
-
1648
- if (type === "telegram") {
1649
- return ["TELEGRAM_BOT_TOKEN", "TELEGRAM_WEBHOOK_SECRET"];
1650
- }
1651
-
1652
- if (type === "whatsapp" && provider === "zapster") {
1653
- return ["ZAPSTER_API_KEY", "ZAPSTER_WEBHOOK_SECRET"];
1654
- }
1655
-
1656
- if (type === "whatsapp" && provider === "meta") {
1657
- return ["META_WHATSAPP_ACCESS_TOKEN", "META_WHATSAPP_APP_SECRET", "META_WHATSAPP_VERIFY_TOKEN"];
1658
- }
1659
-
1660
- return [];
1661
- }
1662
-
1663
- async function channelTestBody(
1664
- channel: CloudChannel,
1665
- messageFlag: string | boolean | undefined,
1666
- fixtureFlag: string | boolean | undefined,
1667
- ): Promise<string> {
1668
- if (typeof fixtureFlag === "string") {
1669
- return readFile(resolve(process.cwd(), fixtureFlag), "utf8");
1670
- }
1671
-
1672
- const message = typeof messageFlag === "string" ? messageFlag : "hello";
1673
-
1674
- if (channel.type === "telegram") {
1675
- return JSON.stringify({
1676
- update_id: Date.now(),
1677
- message: {
1678
- message_id: 1,
1679
- chat: { id: 456, type: "private" },
1680
- from: { id: 123, first_name: "AgentKit" },
1681
- text: message,
1682
- },
1683
- });
1684
- }
1685
-
1686
- if (channel.type === "whatsapp") {
1687
- return JSON.stringify({
1688
- event_id: `evt_${Date.now()}`,
1689
- instance_id: "local",
1690
- message: {
1691
- id: `msg_${Date.now()}`,
1692
- from: "5511999999999",
1693
- type: "text",
1694
- text: { body: message },
1695
- },
1696
- });
1697
- }
1698
-
1699
- return JSON.stringify({
1700
- event_id: `web_${Date.now()}`,
1701
- visitor_id: "visitor_test",
1702
- message: {
1703
- id: `msg_${Date.now()}`,
1704
- text: message,
1705
- },
1706
- });
1707
- }
1708
-
1709
- function channelTestHeaders(channel: CloudChannel): Headers {
1710
- const headers = new Headers({ "Content-Type": "application/json" });
1711
-
1712
- if (channel.type === "website") {
1713
- const token = process.env.AGENTKIT_WEBSITE_CHANNEL_TOKEN;
1714
-
1715
- if (token) {
1716
- headers.set("Authorization", `Bearer ${token}`);
1717
- }
1718
- }
1719
-
1720
- if (channel.type === "telegram") {
1721
- headers.set("X-Telegram-Bot-Api-Secret-Token", process.env.TELEGRAM_WEBHOOK_SECRET ?? "");
1722
- }
1723
-
1724
- if (channel.type === "whatsapp" && channel.provider === "zapster") {
1725
- headers.set("X-Zapster-Webhook-Secret", process.env.ZAPSTER_WEBHOOK_SECRET ?? "");
1726
- }
1727
-
1728
- return headers;
1729
- }
1730
-
1731
- async function cloudGet<T>(apiUrl: string, path: string): Promise<T> {
1732
- const response = await cloudFetch(apiUrl, path);
1733
- const payload = await response.json();
1734
-
1735
- if (!response.ok) {
1736
- throw new Error(readCloudError(payload, response.status));
1737
- }
1738
-
1739
- return payload as T;
1740
- }
1741
-
1742
- async function cloudPost<T>(apiUrl: string, path: string, body: unknown): Promise<T> {
1743
- const response = await cloudFetch(apiUrl, path, {
1744
- method: "POST",
1745
- headers: { "Content-Type": "application/json" },
1746
- body: JSON.stringify(body),
1747
- });
1748
- const payload = await response.json();
1749
-
1750
- if (!response.ok) {
1751
- throw new Error(readCloudError(payload, response.status));
1752
- }
1753
-
1754
- return payload as T;
1755
- }
1756
-
1757
- function cloudFetch(apiUrl: string, path: string, init: RequestInit = {}): Promise<Response> {
1758
- const headers = new Headers(init.headers);
1759
- const apiToken = process.env.AGENTKIT_CLOUD_API_TOKEN;
1760
-
1761
- if (apiToken && !headers.has("Authorization")) {
1762
- headers.set("Authorization", `Bearer ${apiToken}`);
1763
- }
1764
-
1765
- return fetch(new URL(path, parseCloudApiUrl(apiUrl)).href, {
1766
- ...init,
1767
- headers,
1768
- });
1769
- }
1770
-
1771
- type CloudAuthConfig = {
1772
- apiUrl: string;
1773
- token: string;
1774
- source?: "environment" | "file";
1775
- };
1776
-
1777
- function cloudAuthPath(): string {
1778
- return join(homedir(), ".agentkit", "cloud.json");
1779
- }
1780
-
1781
- async function readCloudAuth(): Promise<CloudAuthConfig | null> {
1782
- if (process.env.AGENTKIT_CLOUD_API_TOKEN) {
1783
- return {
1784
- apiUrl: process.env.AGENTKIT_CLOUD_API_URL ?? defaultAgentKitCloudApiUrl,
1785
- token: process.env.AGENTKIT_CLOUD_API_TOKEN,
1786
- source: "environment",
1787
- };
1788
- }
1789
-
1790
- try {
1791
- const parsed = JSON.parse(await readFile(cloudAuthPath(), "utf8"));
1792
-
1793
- if (parsed && typeof parsed === "object" && typeof parsed.token === "string") {
1794
- return {
1795
- apiUrl: typeof parsed.apiUrl === "string" ? parsed.apiUrl : defaultAgentKitCloudApiUrl,
1796
- token: parsed.token,
1797
- source: "file",
1798
- };
1799
- }
1800
- } catch {
1801
- return null;
1802
- }
1803
-
1804
- return null;
1805
- }
1806
-
1807
- async function writeCloudAuth(config: CloudAuthConfig): Promise<void> {
1808
- await mkdir(join(homedir(), ".agentkit"), { recursive: true });
1809
- await writeFile(cloudAuthPath(), `${JSON.stringify(config, null, 2)}\n`, { mode: 0o600 });
1810
- }
1811
-
1812
- async function ensureDeployChatAccessToken(options: {
1813
- apiUrl: string;
1814
- root: string;
1815
- deployId: string;
1816
- skip: boolean;
1817
- }): Promise<
1818
- | { status: "ready"; path: string; tokenId: string; created: boolean }
1819
- | { status: "skipped" }
1820
- | { status: "failed"; error: unknown }
1821
- > {
1822
- if (options.skip) {
1823
- return { status: "skipped" };
1824
- }
1825
-
1826
- try {
1827
- const path = join(options.root, defaultDeployChatAccessTokenPath);
1828
- const existing = await readDeployAccessTokenFile(path);
1829
-
1830
- if (existing?.deploy_id === options.deployId && existing.id) {
1831
- const listed = (await cloudApiRequest(
1832
- options.apiUrl,
1833
- `/v1/deploys/${encodeURIComponent(options.deployId)}/access-tokens`,
1834
- { method: "GET" },
1835
- )) as DeployAccessTokenListResponse;
1836
- const stillExists = (listed.access_tokens ?? []).some((token) => token.id === existing.id);
1837
-
1838
- if (stillExists) {
1839
- return { status: "ready", path, tokenId: existing.id, created: false };
1840
- }
1841
- }
1842
-
1843
- const created = (await cloudApiRequest(
1844
- options.apiUrl,
1845
- `/v1/deploys/${encodeURIComponent(options.deployId)}/access-tokens`,
1846
- {
1847
- method: "POST",
1848
- body: JSON.stringify({ name: defaultDeployChatAccessTokenName }),
1849
- },
1850
- )) as DeployAccessTokenCreateResponse;
1851
- const token = created.access_token;
1852
-
1853
- if (!token?.id || token.deploy_id !== options.deployId || !token.token) {
1854
- throw new AgentKitError("access_token_invalid", "AgentKit Cloud returned an invalid deploy access token payload.");
1855
- }
1856
-
1857
- await writeSecretJson(path, created);
1858
- return { status: "ready", path, tokenId: token.id, created: true };
1859
- } catch (error) {
1860
- return { status: "failed", error };
1861
- }
1862
- }
1863
-
1864
- async function readDeployAccessTokenFile(path: string): Promise<{ id?: string; deploy_id?: string } | null> {
1865
- try {
1866
- const parsed = JSON.parse(await readFile(path, "utf8")) as DeployAccessTokenCreateResponse;
1867
- return parsed.access_token ?? null;
1868
- } catch {
1869
- return null;
1870
- }
1871
- }
1872
-
1873
- async function writeSecretJson(path: string, value: unknown): Promise<void> {
1874
- await mkdir(dirname(path), { recursive: true });
1875
- await writeFile(path, `${JSON.stringify(value, null, 2)}\n`, { mode: 0o600 });
1876
- }
1877
-
1878
- async function verifyCloudLogin(
1879
- apiUrl: string,
1880
- token: string,
1881
- ): Promise<{ me: CloudMeResponse; capabilities: CloudCapabilitiesResponse }> {
1882
- const me = await cloudApiRequestWithToken<CloudMeResponse>(apiUrl, "/v1/me", token, { method: "GET" });
1883
- const capabilities = await cloudApiRequestWithToken<CloudCapabilitiesResponse>(
1884
- apiUrl,
1885
- "/v1/me/capabilities",
1886
- token,
1887
- { method: "GET" },
1888
- );
1889
-
1890
- return { me, capabilities };
1891
- }
1892
-
1893
- async function clearCloudAuth(): Promise<void> {
1894
- await rm(cloudAuthPath(), { force: true });
1895
- }
1896
-
1897
- async function resolveCloudApiUrl(flag: string | boolean | undefined): Promise<string> {
1898
- if (flag !== undefined) {
1899
- return parseCloudApiUrl(flag);
1900
- }
1901
-
1902
- const auth = await readCloudAuth();
1903
- return auth?.apiUrl ?? parseCloudApiUrl(undefined);
1904
- }
1905
-
1906
- async function requireLocalDeployState(): Promise<LocalDeployState> {
1907
- const state = await readLocalDeployStateIfExists(process.cwd());
1908
-
1909
- if (!state) {
1910
- throw new Error("No .agentkit/deploy.json found. Run agentkit deploy first.");
1911
- }
1912
-
1913
- return state;
1914
- }
1915
-
1916
- function requireDeployUrl(state: Pick<LocalDeployState, "deploy_id" | "url">): string {
1917
- if (!state.url) {
1918
- throw new Error(`Deploy ${state.deploy_id} does not have a URL. Run agentkit deploy again.`);
1919
- }
1920
-
1921
- return state.url;
1922
- }
1923
-
1924
- async function readDeployAccessTokenForSmoke(cwd: string, state: LocalDeployState): Promise<string | undefined> {
1925
- if (state.access?.mode === "public") {
1926
- return undefined;
1927
- }
1928
-
1929
- const root = await findAgentCapsuleRoot(cwd);
1930
- return readDeployAccessTokenForUi(join(root, defaultDeployChatAccessTokenPath));
1931
- }
1932
-
1933
- async function runDeploySmoke(input: { deployUrl: string; message: string; token?: string }): Promise<void> {
1934
- const headers = new Headers({ "Content-Type": "application/json" });
1935
-
1936
- if (input.token) {
1937
- headers.set("Authorization", `Bearer ${input.token}`);
1938
- }
1939
-
1940
- let response: Response;
1941
-
1942
- try {
1943
- response = await fetch(new URL("/v1/chat", input.deployUrl).href, {
1944
- method: "POST",
1945
- headers,
1946
- body: JSON.stringify({ message: input.message }),
1947
- });
1948
- } catch (error) {
1949
- throw new AgentKitError(
1950
- "deploy_smoke_failed",
1951
- `Deploy smoke request failed before a response: ${error instanceof Error ? error.message : String(error)}`,
1952
- );
1953
- }
1954
-
1955
- const payload = await response.json().catch(() => null);
1956
-
1957
- if (!response.ok) {
1958
- throw new AgentKitError("deploy_smoke_failed", readCloudError(payload, response.status));
1959
- }
1960
-
1961
- const content =
1962
- payload && typeof payload === "object" && "message" in payload && isRecord((payload as { message?: unknown }).message)
1963
- ? (payload as { message: { content?: unknown } }).message.content
1964
- : undefined;
1965
-
1966
- console.log("");
1967
- console.log("Deploy smoke verified");
1968
- console.log(`URL: ${input.deployUrl}`);
1969
- console.log(`Message: ${input.message}`);
1970
- if (typeof content === "string") {
1971
- console.log(`Response: ${content}`);
1972
- }
1973
- }
1974
-
1975
- async function resolveProjectIdForSecretCommand(cwd: string): Promise<string> {
1976
- const state = await readLocalDeployStateIfExists(cwd);
1977
-
1978
- if (state?.project_id) {
1979
- return state.project_id;
1980
- }
1981
-
1982
- const root = await findAgentCapsuleRoot(cwd);
1983
- const capsule = await loadAgentCapsule(root);
1984
- return stableCloudProjectId(capsule.config.name);
1985
- }
1986
-
1987
- async function cloudApiRequest(apiUrl: string, path: string, init: RequestInit): Promise<unknown> {
1988
- const auth = await readCloudAuth();
1989
- const headers = new Headers(init.headers);
1990
-
1991
- if (!headers.has("Content-Type") && init.body !== undefined) {
1992
- headers.set("Content-Type", "application/json");
1993
- }
1994
-
1995
- if (auth?.token && !headers.has("Authorization")) {
1996
- headers.set("Authorization", `Bearer ${auth.token}`);
1997
- }
1998
-
1999
- const response = await fetch(new URL(path, parseCloudApiUrl(apiUrl)).href, {
2000
- ...init,
2001
- headers,
2002
- });
2003
- const payload = await response.json().catch(() => null);
2004
-
2005
- if (!response.ok) {
2006
- const code =
2007
- payload && typeof payload === "object" && "error" in payload
2008
- ? String((payload as { error?: { code?: unknown } }).error?.code ?? "cloud_request_failed")
2009
- : "cloud_request_failed";
2010
- let message = readCloudError(payload, response.status);
2011
-
2012
- if (code === "auth_token_invalid") {
2013
- message = `${message} Token source: ${auth ? formatCloudAuthSource(auth) : "none"}. Run: agentkit login --token <token>`;
2014
- }
2015
-
2016
- throw new AgentKitError(code, message);
2017
- }
2018
-
2019
- return payload;
2020
- }
2021
-
2022
- async function cloudApiRequestWithToken<T>(
2023
- apiUrl: string,
2024
- path: string,
2025
- token: string,
2026
- init: RequestInit,
2027
- ): Promise<T> {
2028
- const headers = new Headers(init.headers);
2029
-
2030
- if (!headers.has("Content-Type") && init.body !== undefined) {
2031
- headers.set("Content-Type", "application/json");
2032
- }
2033
-
2034
- headers.set("Authorization", `Bearer ${token}`);
2035
-
2036
- const response = await fetch(new URL(path, parseCloudApiUrl(apiUrl)).href, {
2037
- ...init,
2038
- headers,
2039
- });
2040
- const payload = await response.json().catch(() => null);
2041
-
2042
- if (!response.ok) {
2043
- const code =
2044
- payload && typeof payload === "object" && "error" in payload
2045
- ? String((payload as { error?: { code?: unknown } }).error?.code ?? "cloud_request_failed")
2046
- : "cloud_request_failed";
2047
- throw new AgentKitError(code, readCloudError(payload, response.status));
2048
- }
2049
-
2050
- return payload as T;
2051
- }
2052
-
2053
- function printJson(value: unknown): void {
2054
- console.log(JSON.stringify(value, null, 2));
2055
- }
2056
-
2057
- async function printJsonOrWrite(value: unknown, outFlag: string | boolean | undefined, usage: string): Promise<void> {
2058
- const outPath = parseOptionalStringFlag(outFlag, "out", usage);
2059
-
2060
- if (!outPath) {
2061
- printJson(value);
2062
- return;
2063
- }
2064
-
2065
- const absolutePath = resolve(process.cwd(), outPath);
2066
- await writeSecretJson(absolutePath, value);
2067
- console.log(`Deploy access token written to ${absolutePath}`);
2068
- }
2069
-
2070
- function readCloudError(payload: unknown, status: number): string {
2071
- if (payload && typeof payload === "object" && "error" in payload) {
2072
- const error = (payload as { error?: { message?: unknown } }).error;
2073
- if (typeof error?.message === "string") {
2074
- return error.message;
2075
- }
2076
- }
2077
-
2078
- return `AgentKit Cloud request failed with HTTP ${status}.`;
2079
- }
2080
-
2081
- function formatCloudAuthSource(auth: CloudAuthConfig): string {
2082
- if (auth.source === "environment") {
2083
- return "AGENTKIT_CLOUD_API_TOKEN";
2084
- }
2085
-
2086
- if (auth.source === "file") {
2087
- return cloudAuthPath();
2088
- }
2089
-
2090
- return "AgentKit Cloud auth";
2091
- }
2092
-
2093
- function relativePath(root: string, path: string): string {
2094
- const output = relative(root, path);
2095
- return output.startsWith("..") ? path : output || ".";
2096
- }
2097
-
2098
- function parsePort(value: string | boolean | undefined, usage = "agentkit dev --port <number>"): number | undefined {
2099
- if (value === undefined) {
2100
- return undefined;
2101
- }
2102
-
2103
- if (typeof value !== "string") {
2104
- throw new Error(`Missing port. Usage: ${usage}`);
2105
- }
2106
-
2107
- const port = Number(value);
2108
-
2109
- if (!Number.isInteger(port) || port <= 0 || port > 65535) {
2110
- throw new Error(`Invalid port. Usage: ${usage}`);
2111
- }
2112
-
2113
- return port;
2114
- }
2115
-
2116
- function parseTargetFlag(value: string | boolean | undefined, capability: "build" | "deploy"): string {
2117
- if (value === undefined) {
2118
- return "cloudflare";
2119
- }
2120
-
2121
- if (typeof value !== "string") {
2122
- throw new Error(
2123
- `Missing target. Usage: agentkit ${capability} --target ${listTargetsWithCapability(capability).join("|")}`,
2124
- );
2125
- }
2126
-
2127
- return value;
2128
- }
2129
-
2130
- function parseCloudApiUrl(value: string | boolean | undefined): string {
2131
- if (value === undefined) {
2132
- return process.env.AGENTKIT_CLOUD_API_URL ?? defaultAgentKitCloudApiUrl;
2133
- }
2134
-
2135
- if (typeof value !== "string") {
2136
- throw new Error("Missing API URL. Usage: agentkit deploy --api <url>");
2137
- }
2138
-
2139
- return value;
2140
- }
2141
-
2142
- function parseRequiredStringFlag(value: string | boolean | undefined, name: string, usage: string): string {
2143
- if (typeof value !== "string" || value.trim().length === 0) {
2144
- throw new Error(`Missing ${name}. Usage: ${usage}`);
2145
- }
2146
-
2147
- return value.trim();
2148
- }
2149
-
2150
- function parseOptionalStringFlag(value: string | boolean | undefined, name: string, usage: string): string | undefined {
2151
- if (value === undefined) {
2152
- return undefined;
2153
- }
2154
-
2155
- if (typeof value !== "string" || value.trim().length === 0) {
2156
- throw new Error(`Missing ${name}. Usage: ${usage}`);
2157
- }
2158
-
2159
- return value.trim();
2160
- }
2161
-
2162
- async function resolveSecretInputValue(options: {
2163
- name: string | undefined;
2164
- valueParts: string[];
2165
- stdinFlag: string | boolean | undefined;
2166
- fromEnvFlag: string | boolean | undefined;
2167
- fromLocalEnvFlag: string | boolean | undefined;
2168
- localEnvRoot?: string;
2169
- usage: string;
2170
- inlineSensitiveHelp?: string;
2171
- }): Promise<string> {
2172
- const { name, valueParts, stdinFlag, fromEnvFlag, fromLocalEnvFlag, localEnvRoot, usage, inlineSensitiveHelp } = options;
2173
-
2174
- if (!name) {
2175
- throw new Error(`Missing name. Usage: ${usage}`);
2176
- }
2177
-
2178
- const hasInlineValue = valueParts.length > 0;
2179
- const hasStdin = stdinFlag !== undefined;
2180
- const hasFromEnv = fromEnvFlag !== undefined;
2181
- const hasFromLocalEnv = fromLocalEnvFlag !== undefined;
2182
- const inputModes = [hasInlineValue, hasStdin, hasFromEnv, hasFromLocalEnv].filter(Boolean).length;
2183
-
2184
- if (inputModes !== 1) {
2185
- throw new Error(`Provide exactly one value source. Usage: ${usage}`);
2186
- }
2187
-
2188
- let value: string;
2189
-
2190
- if (hasInlineValue) {
2191
- value = valueParts.join(" ");
2192
- if (looksLikeSecretName(name)) {
2193
- console.error(
2194
- `Warning: ${name} looks sensitive. ${inlineSensitiveHelp ?? "Prefer --stdin or --from-env so the value does not enter shell history."}`,
2195
- );
2196
- }
2197
- } else if (hasStdin) {
2198
- if (stdinFlag !== true) {
2199
- throw new Error(`--stdin does not take a value. Usage: ${usage}`);
2200
- }
2201
-
2202
- value = stripOneTrailingNewline(await readStdinText());
2203
- } else if (hasFromEnv) {
2204
- const envName = fromEnvFlag === true ? name : parseRequiredStringFlag(fromEnvFlag, "env name", usage);
2205
- const envValue = process.env[envName];
2206
-
2207
- if (envValue === undefined) {
2208
- throw new AgentKitError("secret_not_found", `Environment variable ${envName} is not set.`);
2209
- }
2210
-
2211
- value = envValue;
2212
- } else {
2213
- if (fromLocalEnvFlag !== true) {
2214
- throw new Error(`--from-local-env does not take a value. Usage: ${usage}`);
2215
- }
2216
-
2217
- if (!localEnvRoot) {
2218
- throw new Error(`--from-local-env is not available for this command. Usage: ${usage}`);
2219
- }
2220
-
2221
- const localEnv = await loadCapsuleEnv(localEnvRoot, {});
2222
- const localValue = localEnv[name];
2223
-
2224
- if (localValue === undefined) {
2225
- throw new AgentKitError("secret_not_found", `Local .env does not contain ${name}.`);
2226
- }
2227
-
2228
- value = localValue;
2229
- }
2230
-
2231
- if (value.length === 0) {
2232
- throw new Error(`Secret value cannot be empty. Usage: ${usage}`);
2233
- }
2234
-
2235
- return value;
2236
- }
2237
-
2238
- function looksLikeSecretName(name: string): boolean {
2239
- return /(?:SECRET|TOKEN|KEY|PASSWORD|CREDENTIAL|PRIVATE)/i.test(name);
2240
- }
2241
-
2242
- async function readStdinText(): Promise<string> {
2243
- const chunks: Buffer[] = [];
2244
-
2245
- for await (const chunk of process.stdin) {
2246
- chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
2247
- }
2248
-
2249
- return Buffer.concat(chunks).toString("utf8");
2250
- }
2251
-
2252
- function stripOneTrailingNewline(value: string): string {
2253
- return value.replace(/\r?\n$/, "");
2254
- }
2255
-
2256
- function printBuildResult(result: Awaited<ReturnType<typeof buildAgentCapsule>>): void {
2257
- console.log("AgentKit build complete");
2258
- console.log("");
2259
- console.log(`Target: ${result.target}`);
2260
- console.log(`Build dir: ${result.buildDir}`);
2261
- console.log(`Manifest: ${result.manifestPath}`);
2262
-
2263
- if (result.target === "cloudflare") {
2264
- console.log(`Worker: ${result.workerPath}`);
2265
- console.log(`Wrangler: ${result.wranglerPath}`);
2266
- if (result.tursoSchemaPath) {
2267
- console.log(`Turso: ${result.tursoSchemaPath}`);
2268
- }
2269
- console.log(`Bundle: ${result.bundleSha256}`);
2270
- }
2271
-
2272
- if (result.target === "container") {
2273
- console.log(`Server: ${result.serverPath}`);
2274
- console.log(`Docker: ${result.dockerfilePath}`);
2275
- console.log(`Migrations:${result.migrationsDir}`);
2276
- }
2277
-
2278
- if (result.warnings.length > 0) {
2279
- console.log("");
2280
- console.log("Warnings:");
2281
-
2282
- for (const warning of result.warnings) {
2283
- console.log(` - ${warning}`);
2284
- }
2285
- }
2286
- }
2287
-
2288
- function printVpsDeployPlan(plan: Awaited<ReturnType<typeof prepareVpsDeploy>>): void {
2289
- console.log("");
2290
- console.log("VPS deploy plan ready");
2291
- console.log(`Host: ${plan.host}`);
2292
- console.log(`Compose: ${plan.composePath}`);
2293
- console.log(`Env: ${plan.envExamplePath}`);
2294
- console.log(`Secrets: ${plan.secretsPath}`);
2295
- console.log(`Handoff: ${plan.handoffPath}`);
2296
- console.log(`State: ${plan.statePath}`);
2297
- console.log("");
2298
- console.log("Commands:");
2299
-
2300
- for (const command of plan.commands) {
2301
- console.log(` ${command}`);
2302
- }
2303
- }
2304
-
2305
- async function deployCloudflare(wranglerPath: string, buildDir: string): Promise<void> {
2306
- const exitCode = await runInheritedCommand("npx", ["wrangler", "deploy", "--config", wranglerPath], buildDir);
2307
-
2308
- if (exitCode !== 0) {
2309
- throw new Error(`Cloudflare deploy failed with exit code ${exitCode}.`);
2310
- }
2311
- }
2312
-
2313
- async function installProjectDependencies(projectPath: string): Promise<void> {
2314
- const exitCode = await runInheritedCommand("npm", ["install"], projectPath);
2315
-
2316
- if (exitCode !== 0) {
2317
- throw new Error(`Dependency install failed with exit code ${exitCode}. Run \`npm install\` in ${projectPath}.`);
2318
- }
2319
- }
2320
-
2321
- function runInheritedCommand(command: string, args: string[], cwd: string): Promise<number> {
2322
- return new Promise((resolvePromise, reject) => {
2323
- const child = spawn(command, args, {
2324
- cwd,
2325
- stdio: "inherit",
2326
- });
2327
-
2328
- child.once("error", reject);
2329
- child.once("close", (code) => resolvePromise(code ?? 1));
2330
- });
2331
- }
1016
+ }
2332
1017
 
2333
1018
  async function parseToolInput(
2334
1019
  cwd: string,
@@ -2451,396 +1136,6 @@ async function openLocalUrl(url: string): Promise<void> {
2451
1136
  });
2452
1137
  }
2453
1138
 
2454
- async function startDeployChatUiServer(
2455
- cwd: string,
2456
- options: { port: number; tokenFile?: string },
2457
- ): Promise<{ url: string; deployUrl: string; tokenSource: string; stop(): Promise<void> }> {
2458
- const root = await findAgentCapsuleRoot(cwd);
2459
- const state = await readLocalDeployStateIfExists(root);
2460
-
2461
- if (!state?.url) {
2462
- throw new Error("No hosted deploy URL found. Run agentkit deploy first.");
2463
- }
2464
-
2465
- const tokenPath = options.tokenFile
2466
- ? resolve(root, options.tokenFile)
2467
- : join(root, defaultDeployChatAccessTokenPath);
2468
- const hostname = "localhost";
2469
- const deployUrl = state.url;
2470
- const server = createServer(async (request, response) => {
2471
- try {
2472
- const requestUrl = new URL(request.url ?? "/", `http://${request.headers.host ?? `${hostname}:${options.port}`}`);
2473
-
2474
- if (request.method === "GET" && requestUrl.pathname === "/") {
2475
- sendHtml(response, renderDeployChatUi({ deployUrl }));
2476
- return;
2477
- }
2478
-
2479
- if (request.method === "GET" && requestUrl.pathname === "/health") {
2480
- sendJson(response, 200, { status: "ok", target: "deploy-chat-ui", deployUrl });
2481
- return;
2482
- }
2483
-
2484
- if (request.method === "GET" && requestUrl.pathname === "/_agentkit") {
2485
- sendJson(response, 200, {
2486
- deployUrl,
2487
- access: "proxied",
2488
- tokenSource: process.env.AGENTKIT_DEPLOY_ACCESS_TOKEN ? "AGENTKIT_DEPLOY_ACCESS_TOKEN" : relativePath(root, tokenPath),
2489
- });
2490
- return;
2491
- }
2492
-
2493
- if (request.method === "POST" && (requestUrl.pathname === "/v1/chat" || requestUrl.pathname === "/chat")) {
2494
- const token = await readDeployAccessTokenForUi(tokenPath);
2495
- const body = await readIncomingBody(request);
2496
- const contentTypeHeader = request.headers["content-type"];
2497
- const contentType = Array.isArray(contentTypeHeader)
2498
- ? contentTypeHeader[0] ?? "application/json"
2499
- : contentTypeHeader ?? "application/json";
2500
- const upstream = await fetch(new URL("/v1/chat", deployUrl).href, {
2501
- method: "POST",
2502
- headers: {
2503
- "Authorization": `Bearer ${token}`,
2504
- "Content-Type": contentType,
2505
- },
2506
- body,
2507
- });
2508
- const text = await upstream.text();
2509
-
2510
- response.writeHead(upstream.status, {
2511
- "content-type": upstream.headers.get("content-type") ?? "application/json",
2512
- });
2513
- response.end(text);
2514
- return;
2515
- }
2516
-
2517
- sendJson(response, 404, { error: { code: "not_found", message: "Route not found." } });
2518
- } catch (error) {
2519
- sendJson(response, 500, {
2520
- error: {
2521
- code: isAgentKitError(error) ? error.code : "deploy_chat_ui_error",
2522
- message: error instanceof Error ? error.message : String(error),
2523
- },
2524
- });
2525
- }
2526
- });
2527
-
2528
- await new Promise<void>((resolvePromise, reject) => {
2529
- server.once("error", reject);
2530
- server.listen(options.port, hostname, () => {
2531
- server.off("error", reject);
2532
- resolvePromise();
2533
- });
2534
- });
2535
-
2536
- return {
2537
- url: `http://${hostname}:${options.port}`,
2538
- deployUrl,
2539
- tokenSource: process.env.AGENTKIT_DEPLOY_ACCESS_TOKEN ? "AGENTKIT_DEPLOY_ACCESS_TOKEN" : relativePath(root, tokenPath),
2540
- stop: () =>
2541
- new Promise<void>((resolvePromise, reject) => {
2542
- server.close((error) => (error ? reject(error) : resolvePromise()));
2543
- }),
2544
- };
2545
- }
2546
-
2547
- async function readDeployAccessTokenForUi(tokenPath: string): Promise<string> {
2548
- if (process.env.AGENTKIT_DEPLOY_ACCESS_TOKEN) {
2549
- return process.env.AGENTKIT_DEPLOY_ACCESS_TOKEN;
2550
- }
2551
-
2552
- let payload: unknown;
2553
-
2554
- try {
2555
- payload = JSON.parse(await readFile(tokenPath, "utf8"));
2556
- } catch (error) {
2557
- throw new AgentKitError(
2558
- "deploy_access_token_missing",
2559
- `Could not read a clean deploy access token file at ${tokenPath}. Run: agentkit access token create ${defaultDeployChatAccessTokenName} --out ${defaultDeployChatAccessTokenPath}`,
2560
- );
2561
- }
2562
-
2563
- const token =
2564
- payload && typeof payload === "object"
2565
- ? (payload as { access_token?: { token?: unknown }; token?: unknown; accessToken?: unknown }).access_token?.token ??
2566
- (payload as { token?: unknown }).token ??
2567
- (payload as { accessToken?: unknown }).accessToken
2568
- : undefined;
2569
-
2570
- if (typeof token !== "string" || token.trim().length === 0) {
2571
- throw new AgentKitError(
2572
- "deploy_access_token_invalid",
2573
- `Deploy access token file ${tokenPath} does not contain access_token.token. Recreate it with: agentkit access token create ${defaultDeployChatAccessTokenName} --out ${defaultDeployChatAccessTokenPath}`,
2574
- );
2575
- }
2576
-
2577
- return token.trim();
2578
- }
2579
-
2580
- function readIncomingBody(request: IncomingMessage): Promise<string> {
2581
- return new Promise((resolvePromise, reject) => {
2582
- let body = "";
2583
- request.setEncoding("utf8");
2584
- request.on("data", (chunk) => {
2585
- body += chunk;
2586
- });
2587
- request.on("end", () => resolvePromise(body));
2588
- request.on("error", reject);
2589
- });
2590
- }
2591
-
2592
- function sendJson(response: ServerResponse, status: number, body: unknown): void {
2593
- response.writeHead(status, { "content-type": "application/json; charset=utf-8" });
2594
- response.end(JSON.stringify(body, null, 2));
2595
- }
2596
-
2597
- function sendHtml(response: ServerResponse, body: string): void {
2598
- response.writeHead(200, { "content-type": "text/html; charset=utf-8" });
2599
- response.end(body);
2600
- }
2601
-
2602
- function renderDeployChatUi(input: { deployUrl: string }): string {
2603
- const deployUrl = escapeHtml(input.deployUrl);
2604
-
2605
- return `<!doctype html>
2606
- <html lang="en">
2607
- <head>
2608
- <meta charset="utf-8">
2609
- <meta name="viewport" content="width=device-width, initial-scale=1">
2610
- <title>AgentKit Hosted Chat</title>
2611
- <style>
2612
- :root {
2613
- color-scheme: light;
2614
- --bg: #f6f7f5;
2615
- --panel: #ffffff;
2616
- --ink: #182022;
2617
- --muted: #667175;
2618
- --line: #d9ded9;
2619
- --accent: #176b5d;
2620
- --danger: #9f2d2d;
2621
- }
2622
-
2623
- * { box-sizing: border-box; }
2624
-
2625
- body {
2626
- margin: 0;
2627
- min-height: 100svh;
2628
- background: var(--bg);
2629
- color: var(--ink);
2630
- font-family: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
2631
- }
2632
-
2633
- main {
2634
- min-height: 100svh;
2635
- display: grid;
2636
- grid-template-rows: auto 1fr auto;
2637
- background: var(--panel);
2638
- }
2639
-
2640
- header {
2641
- border-bottom: 1px solid var(--line);
2642
- padding: 18px 22px;
2643
- }
2644
-
2645
- h1 {
2646
- margin: 0 0 4px;
2647
- font-size: 20px;
2648
- line-height: 1.2;
2649
- }
2650
-
2651
- code {
2652
- color: var(--muted);
2653
- font-size: 12px;
2654
- overflow-wrap: anywhere;
2655
- }
2656
-
2657
- #messages {
2658
- padding: 22px;
2659
- overflow: auto;
2660
- display: flex;
2661
- flex-direction: column;
2662
- gap: 12px;
2663
- }
2664
-
2665
- .message {
2666
- max-width: 780px;
2667
- display: grid;
2668
- gap: 4px;
2669
- }
2670
-
2671
- .message.user {
2672
- align-self: end;
2673
- }
2674
-
2675
- .role {
2676
- color: var(--muted);
2677
- font-size: 12px;
2678
- font-weight: 700;
2679
- text-transform: uppercase;
2680
- }
2681
-
2682
- .bubble {
2683
- border: 1px solid var(--line);
2684
- border-radius: 8px;
2685
- padding: 12px 14px;
2686
- white-space: pre-wrap;
2687
- line-height: 1.45;
2688
- background: #fbfbfa;
2689
- }
2690
-
2691
- .user .bubble {
2692
- border-color: #b8d8ce;
2693
- background: #e8f5f0;
2694
- }
2695
-
2696
- .error .bubble {
2697
- border-color: #e1b1b1;
2698
- color: var(--danger);
2699
- background: #fff6f6;
2700
- }
2701
-
2702
- form {
2703
- border-top: 1px solid var(--line);
2704
- padding: 14px;
2705
- display: grid;
2706
- grid-template-columns: 1fr auto;
2707
- gap: 10px;
2708
- }
2709
-
2710
- textarea {
2711
- min-height: 52px;
2712
- max-height: 180px;
2713
- resize: vertical;
2714
- border: 1px solid var(--line);
2715
- border-radius: 8px;
2716
- padding: 12px;
2717
- color: var(--ink);
2718
- font: inherit;
2719
- }
2720
-
2721
- button {
2722
- min-width: 96px;
2723
- border: 0;
2724
- border-radius: 8px;
2725
- padding: 0 18px;
2726
- color: white;
2727
- background: var(--accent);
2728
- font: inherit;
2729
- font-weight: 700;
2730
- cursor: pointer;
2731
- }
2732
-
2733
- button:disabled {
2734
- cursor: wait;
2735
- opacity: 0.65;
2736
- }
2737
-
2738
- @media (max-width: 640px) {
2739
- form {
2740
- grid-template-columns: 1fr;
2741
- }
2742
-
2743
- button {
2744
- min-height: 44px;
2745
- }
2746
- }
2747
- </style>
2748
- </head>
2749
- <body>
2750
- <main>
2751
- <header>
2752
- <h1>AgentKit Hosted Chat</h1>
2753
- <code>${deployUrl}</code>
2754
- </header>
2755
- <section id="messages" aria-live="polite"></section>
2756
- <form id="form">
2757
- <textarea id="message" name="message" placeholder="Type a message" required autofocus></textarea>
2758
- <button id="send" type="submit">Send</button>
2759
- </form>
2760
- </main>
2761
- <script>
2762
- const form = document.querySelector("#form");
2763
- const textarea = document.querySelector("#message");
2764
- const send = document.querySelector("#send");
2765
- const messages = document.querySelector("#messages");
2766
- let conversationId = null;
2767
-
2768
- function addMessage(role, content, className = "") {
2769
- const wrapper = document.createElement("article");
2770
- wrapper.className = ["message", role, className].filter(Boolean).join(" ");
2771
- const label = document.createElement("div");
2772
- label.className = "role";
2773
- label.textContent = role;
2774
- const bubble = document.createElement("div");
2775
- bubble.className = "bubble";
2776
- bubble.textContent = content;
2777
- wrapper.append(label, bubble);
2778
- messages.append(wrapper);
2779
- messages.scrollTop = messages.scrollHeight;
2780
- }
2781
-
2782
- form.addEventListener("submit", async (event) => {
2783
- event.preventDefault();
2784
- const message = textarea.value.trim();
2785
- if (!message) return;
2786
-
2787
- addMessage("user", message);
2788
- textarea.value = "";
2789
- send.disabled = true;
2790
-
2791
- try {
2792
- const response = await fetch("/v1/chat", {
2793
- method: "POST",
2794
- headers: { "content-type": "application/json" },
2795
- body: JSON.stringify({ message, conversationId }),
2796
- });
2797
- const payload = await response.json();
2798
-
2799
- if (!response.ok) {
2800
- throw new Error(payload.error?.message || "Request failed");
2801
- }
2802
-
2803
- conversationId = payload.conversationId || conversationId;
2804
- addMessage("assistant", payload.message?.content || "");
2805
- } catch (error) {
2806
- addMessage("error", error instanceof Error ? error.message : String(error), "error");
2807
- } finally {
2808
- send.disabled = false;
2809
- textarea.focus();
2810
- }
2811
- });
2812
- </script>
2813
- </body>
2814
- </html>`;
2815
- }
2816
-
2817
- function escapeHtml(value: string): string {
2818
- return value
2819
- .replace(/&/g, "&amp;")
2820
- .replace(/</g, "&lt;")
2821
- .replace(/>/g, "&gt;")
2822
- .replace(/"/g, "&quot;")
2823
- .replace(/'/g, "&#39;");
2824
- }
2825
-
2826
- async function waitForShutdown(stop: () => Promise<void>): Promise<void> {
2827
- await new Promise<void>((resolve) => {
2828
- let stopped = false;
2829
-
2830
- const shutdown = () => {
2831
- if (stopped) {
2832
- return;
2833
- }
2834
-
2835
- stopped = true;
2836
- void stop().finally(resolve);
2837
- };
2838
-
2839
- process.once("SIGINT", shutdown);
2840
- process.once("SIGTERM", shutdown);
2841
- });
2842
- }
2843
-
2844
1139
  main().catch((error: unknown) => {
2845
1140
  const message = error instanceof Error ? error.message : String(error);
2846
1141
  const prefix = isAgentKitError(error) ? `agentkit ${error.code}` : "agentkit";