@andreprado/agentkit 0.1.0-alpha.3 → 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,177 +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 defaultDeployChatAccessTokenName = "agentkit-chat-ui";
33
- const defaultDeployChatAccessTokenPath = ".agentkit/chat-access-token.json";
34
-
35
- function renderHelp(): string {
36
- return `AgentKit
37
-
38
- Create, run, inspect, and deploy Agent Capsules.
39
-
40
- Quick start:
41
- agentkit new support-agent --template support
42
- cd support-agent
43
- npm run dev
44
- npm run chat -- --message "hello"
45
-
46
- Usage:
47
- agentkit <command> [options]
48
-
49
- Start:
50
- agentkit new <name> [--template blank|support] [--no-install]
51
- agentkit dev
52
- agentkit chat-ui --deploy
53
- agentkit chat --message <text>
54
- agentkit inspect
55
-
56
- Build:
57
- agentkit tool <name> [--input <path-or-json>]
58
- agentkit db migrate
59
- agentkit eval run
60
- agentkit conversations list
61
-
62
- Deploy:
63
- agentkit login --token <token>
64
- agentkit deploy doctor
65
- agentkit secret set <NAME> --stdin
66
- agentkit deploy
67
- agentkit deploy status
68
-
69
- More:
70
- agentkit channels ...
71
- agentkit access token ...
72
- agentkit env ...
73
- agentkit docs full
74
- agentkit help commands
75
- `;
76
- }
77
-
78
- function renderCommandReference(): string {
79
- return `AgentKit Command Reference
80
-
81
- Usage:
82
- agentkit new <name> [--template blank|support] [--no-install]
83
- agentkit dev
84
- agentkit chat-ui --deploy [--port <number>] [--token-file <path>]
85
- agentkit chat --message <text>
86
- agentkit tool <name> [--input <path-or-json>]
87
- agentkit db migrate
88
- agentkit db reset --yes
89
- agentkit db shell
90
- agentkit db seed [--file <path>]
91
- agentkit eval run
92
- agentkit conversations list
93
- agentkit conversations show <conversation-id>
94
- agentkit channels list
95
- agentkit channels add <website|telegram|whatsapp> <name> [--provider zapster|meta] [--api <url>]
96
- agentkit channels setup <name> [--apply] [--api <url>]
97
- agentkit channels status <name> [--api <url>]
98
- agentkit channels test <name> [--message <text>] [--fixture <path>] [--api <url>]
99
- agentkit channels deliveries list <name> [--api <url>]
100
- agentkit channels deliveries show <delivery-id> [--api <url>]
101
- agentkit inspect
102
- agentkit build [--target cloudflare]
103
- agentkit login --token <token> [--api <url>]
104
- agentkit logout
105
- agentkit deploy [--target cloudflare|vps] [--host <host>] [--api <url>] [--dry-run] [--anonymous] [--local-wrangler]
106
- agentkit deploy doctor [--api <url>] [--anonymous]
107
- agentkit deploy status [--api <url>]
108
- agentkit deploy pause [--api <url>]
109
- agentkit deploy resume [--api <url>]
110
- agentkit secret set <NAME> <VALUE> [--api <url>]
111
- agentkit secret set <NAME> --stdin [--api <url>]
112
- agentkit secret set <NAME> --from-env [ENV_NAME] [--api <url>]
113
- agentkit secret set <NAME> --from-local-env [--api <url>]
114
- agentkit secret list [--api <url>]
115
- agentkit secret unset <NAME> [--api <url>]
116
- agentkit access token create <name> [--api <url>] [--out <path>]
117
- agentkit access token list [--api <url>]
118
- agentkit access token revoke <token-id> [--api <url>]
119
- agentkit env set <NAME> <VALUE>
120
- agentkit env set <NAME> --stdin
121
- agentkit env set <NAME> --from-env [ENV_NAME]
122
- agentkit env list
123
- agentkit env unset <NAME>
124
- agentkit docs path
125
- agentkit docs llms
126
- agentkit docs full
127
- agentkit handoff codex [goal]
128
- agentkit help commands
129
- agentkit --help
130
-
131
- Commands:
132
- new Create a new Agent Capsule
133
- dev Run the Agent Capsule locally
134
- chat-ui Run a local chat UI, optionally pointed at a hosted deploy
135
- chat Send one message to the local Agent Capsule
136
- tool Run one registered tool directly
137
- db Manage the local SQLite database
138
- eval Run local eval files
139
- conversations List and inspect stored local conversations
140
- channels Manage hosted channel resources
141
- inspect Print machine-readable Agent Capsule state
142
- build Build a hosted deploy artifact
143
- login Store an AgentKit Cloud alpha token outside the capsule
144
- logout Remove the stored AgentKit Cloud alpha token
145
- deploy Build and send a hosted artifact to AgentKit Cloud
146
- secret Manage hosted AgentKit Cloud secrets without readback
147
- access Manage hosted deploy access tokens
148
- env Manage local .env values without opening code
149
- docs Print AgentKit docs locations
150
- handoff Print a ready-to-paste prompt for another coding agent
151
- help Print focused help or the full command reference
152
-
153
- Examples:
154
- agentkit new support-agent --template support
155
- cd support-agent
156
- agentkit dev
157
- agentkit tool lookup_order --input '{"orderId":"A100"}'
158
- agentkit eval run
159
- `;
160
- }
161
-
162
- type ParsedArgs = {
163
- command?: string;
164
- positional: string[];
165
- flags: Record<string, string | boolean>;
166
- };
167
-
168
- function parseArgs(argv: string[]): ParsedArgs {
169
- const [command, ...rest] = argv;
170
- const positional: string[] = [];
171
- const flags: Record<string, string | boolean> = {};
172
-
173
- for (let index = 0; index < rest.length; index += 1) {
174
- const value = rest[index];
175
-
176
- if (!value.startsWith("--")) {
177
- positional.push(value);
178
- continue;
179
- }
180
-
181
- const name = value.slice(2);
182
- const next = rest[index + 1];
183
-
184
- if (next && !next.startsWith("--")) {
185
- flags[name] = next;
186
- index += 1;
187
- } else {
188
- flags[name] = true;
189
- }
190
- }
191
-
192
- return { command, positional, flags };
193
- }
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";
194
64
 
195
65
  async function main() {
196
66
  const args = parseArgs(process.argv.slice(2));
@@ -200,6 +70,11 @@ async function main() {
200
70
  return;
201
71
  }
202
72
 
73
+ if (isHelpRequested(args)) {
74
+ console.log(renderCommandHelp(args.command));
75
+ return;
76
+ }
77
+
203
78
  if (args.command === "help") {
204
79
  const [topic] = args.positional;
205
80
 
@@ -222,7 +97,7 @@ async function main() {
222
97
  const shouldInstall = args.flags["no-install"] !== true;
223
98
 
224
99
  if (!name) {
225
- throw new Error("Missing project name. Usage: agentkit new <name> [--template blank|support] [--no-install]");
100
+ throw new Error("Missing project name. Usage: agentkit new <name> [--template blank|support|dentista] [--no-install]");
226
101
  }
227
102
 
228
103
  const result = await createProject({ name, template, cwd: process.cwd() });
@@ -254,10 +129,17 @@ async function main() {
254
129
 
255
130
  if (args.command === "dev") {
256
131
  const port = parsePort(args.flags.port, "agentkit dev --port <number>");
257
- const devServer = await startAgentDevServer(process.cwd(), { port });
132
+ const devServer = await startAgentDevServer(process.cwd(), { port, basePort: localDevBasePort() });
258
133
 
259
134
  console.log("Agent Capsule running");
260
135
  console.log("");
136
+ if (devServer.portConflict) {
137
+ const owner = devServer.portConflict.agent ? ` by ${devServer.portConflict.agent}` : "";
138
+ console.log(
139
+ `Port ${devServer.portConflict.port} is already used${owner}. Started ${devServer.capsule.config.name} at ${devServer.urls.chat}.`,
140
+ );
141
+ console.log("");
142
+ }
261
143
  console.log(`Chat: ${devServer.urls.chat}`);
262
144
  console.log(`API: ${devServer.urls.api}`);
263
145
  console.log(`Inspect: ${devServer.urls.inspect}`);
@@ -266,6 +148,20 @@ async function main() {
266
148
  return;
267
149
  }
268
150
 
151
+ if (args.command === "open") {
152
+ const server = await findRunningLocalDevServer(process.cwd());
153
+
154
+ if (!server) {
155
+ console.log("No running AgentKit dev server found for this capsule.");
156
+ console.log("Run: npm run dev");
157
+ return;
158
+ }
159
+
160
+ await openLocalUrl(server.url);
161
+ console.log(`Opened ${server.url}`);
162
+ return;
163
+ }
164
+
269
165
  if (args.command === "chat-ui") {
270
166
  if (!args.flags.deploy) {
271
167
  throw new Error("Usage: agentkit chat-ui --deploy [--port <number>] [--token-file <path>]");
@@ -290,12 +186,17 @@ async function main() {
290
186
 
291
187
  if (args.command === "chat") {
292
188
  const message = args.flags.message;
189
+ const conversationId = parseOptionalStringFlag(
190
+ args.flags["conversation-id"] ?? args.flags.conversation,
191
+ "conversation id",
192
+ 'agentkit chat --message "hello" [--conversation-id <id>]',
193
+ );
293
194
 
294
195
  if (typeof message !== "string") {
295
- throw new Error('Missing message. Usage: agentkit chat --message "hello"');
196
+ throw new Error('Missing message. Usage: agentkit chat --message "hello" [--conversation-id <id>]');
296
197
  }
297
198
 
298
- const result = await runAgentMessageFromCwd(process.cwd(), { message });
199
+ const result = await runAgentMessageFromCwd(process.cwd(), { message, conversationId });
299
200
  console.log(result.message.content);
300
201
  return;
301
202
  }
@@ -485,6 +386,20 @@ async function main() {
485
386
  if (args.command === "deploy") {
486
387
  const [deploySubcommand] = args.positional;
487
388
 
389
+ if (deploySubcommand === "smoke") {
390
+ const message =
391
+ parseOptionalStringFlag(args.flags.message ?? args.flags.smoke, "message", 'agentkit deploy smoke --message "hello"') ??
392
+ "hello";
393
+ const state = await requireLocalDeployState();
394
+ const token = await readDeployAccessTokenForSmoke(process.cwd(), state);
395
+ await runDeploySmoke({
396
+ deployUrl: requireDeployUrl(state),
397
+ message,
398
+ token,
399
+ });
400
+ return;
401
+ }
402
+
488
403
  if (deploySubcommand === "doctor") {
489
404
  const report = await checkDeployReadiness({
490
405
  apiUrl: await resolveCloudApiUrl(args.flags.api),
@@ -520,6 +435,7 @@ async function main() {
520
435
 
521
436
  const target = resolveDeployTarget(parseTargetFlag(args.flags.target, "deploy"));
522
437
  const anonymous = Boolean(args.flags.anonymous);
438
+ const smokeMessage = parseOptionalStringFlag(args.flags.smoke, "smoke", 'agentkit deploy --smoke "hello"');
523
439
 
524
440
  if (target.id === "vps") {
525
441
  const host = parseRequiredStringFlag(args.flags.host, "host", "agentkit deploy --target vps --host <host>");
@@ -592,6 +508,24 @@ async function main() {
592
508
  console.log(`Chat auth: could not create local chat token. ${formatReadinessError(chatAccess.error)}`);
593
509
  console.log(`Run: agentkit access token create ${defaultDeployChatAccessTokenName} --out ${defaultDeployChatAccessTokenPath}`);
594
510
  }
511
+
512
+ if (smokeMessage) {
513
+ if (chatAccess?.status === "failed") {
514
+ throw new AgentKitError("deploy_smoke_failed", "Could not run deploy smoke because chat access token creation failed.");
515
+ }
516
+
517
+ await runDeploySmoke({
518
+ deployUrl: requireDeployUrl({
519
+ deploy_id: deploy.deploy.id,
520
+ url: deploy.deploy.url,
521
+ }),
522
+ message: smokeMessage,
523
+ token:
524
+ chatAccess?.status === "ready"
525
+ ? await readDeployAccessTokenForUi(chatAccess.path)
526
+ : undefined,
527
+ });
528
+ }
595
529
  return;
596
530
  }
597
531
 
@@ -617,6 +551,7 @@ async function main() {
617
551
  fromLocalEnvFlag: args.flags["from-local-env"],
618
552
  localEnvRoot: args.flags["from-local-env"] !== undefined ? await findAgentCapsuleRoot(process.cwd()) : undefined,
619
553
  usage: "agentkit secret set <NAME> <VALUE> | agentkit secret set <NAME> --stdin | agentkit secret set <NAME> --from-env [ENV_NAME] | agentkit secret set <NAME> --from-local-env [--api <url>]",
554
+ inlineSensitiveHelp: "Prefer --stdin, --from-env, or --from-local-env so the value does not enter shell history.",
620
555
  });
621
556
 
622
557
  if (!name) {
@@ -631,6 +566,15 @@ async function main() {
631
566
  return;
632
567
  }
633
568
 
569
+ if (subcommand === "sync") {
570
+ if (args.flags["from-local"] !== true) {
571
+ throw new Error("Usage: agentkit secret sync --from-local [--api <url>]");
572
+ }
573
+
574
+ await syncHostedSecretsFromLocal({ apiUrl, projectId });
575
+ return;
576
+ }
577
+
634
578
  if (subcommand === "unset") {
635
579
  if (!name) {
636
580
  throw new Error("Usage: agentkit secret unset <NAME> [--api <url>]");
@@ -643,7 +587,7 @@ async function main() {
643
587
  return;
644
588
  }
645
589
 
646
- throw new Error("Usage: agentkit secret set <NAME> <VALUE> | agentkit secret set <NAME> --stdin | agentkit secret set <NAME> --from-env [ENV_NAME] | agentkit secret set <NAME> --from-local-env | agentkit secret list | agentkit secret unset <NAME>");
590
+ throw new Error("Usage: agentkit secret set <NAME> <VALUE> | agentkit secret set <NAME> --stdin | agentkit secret set <NAME> --from-env [ENV_NAME] | agentkit secret set <NAME> --from-local-env | agentkit secret sync --from-local | agentkit secret list | agentkit secret unset <NAME>");
647
591
  }
648
592
 
649
593
  if (args.command === "access") {
@@ -718,6 +662,7 @@ async function main() {
718
662
  fromEnvFlag: args.flags["from-env"],
719
663
  fromLocalEnvFlag: undefined,
720
664
  usage: "agentkit env set <NAME> <VALUE> | agentkit env set <NAME> --stdin | agentkit env set <NAME> --from-env [ENV_NAME]",
665
+ inlineSensitiveHelp: "Prefer --stdin or --from-env so the value does not enter shell history.",
721
666
  });
722
667
 
723
668
  if (!name) {
@@ -803,9 +748,12 @@ Rules:
803
748
  - Follow the AgentKit contract in llms-full.txt.
804
749
  - Treat the owner's request as the brief.
805
750
  - Build or update the agent inside this capsule as far as you can without asking for obvious restatements.
751
+ - Do not wait for a wizard or recipe; implement directly in the capsule files.
806
752
  - Edit agent behavior in prompts/instructions.md.
807
753
  - Edit the agent contract in agentkit.config.ts.
808
754
  - Add TypeScript tools under tools/.
755
+ - Keep the first useful version runnable with test/fake unless the owner has already chosen a real provider.
756
+ - Before testing real conversation behavior, ask the owner to choose OpenRouter, OpenAI, Anthropic, or another supported provider. Do not choose for them.
809
757
  - Keep local secrets in .env only.
810
758
  - Do not commit or print secret values.
811
759
  - Do not edit .agentkit/ directly.
@@ -827,1212 +775,187 @@ If you add a tool, also run a tool smoke test with the fake provider, for exampl
827
775
  npm run agentkit -- tool tool_name --input '{}'
828
776
  \`\`\`
829
777
 
778
+ UI testing:
779
+ - For local UI testing, run \`npm run dev\`, open the printed \`Chat:\` URL, and tell the owner the exact URL.
780
+ - For hosted UI testing after deploy, run \`npm run agentkit -- chat-ui --deploy\`, open the printed \`Chat:\` URL, and tell the owner it is connected to the hosted deploy.
781
+ - If the provider is still \`test/fake\`, say the UI was tested only with the deterministic fake provider.
782
+
830
783
  Final response:
831
784
  - Summarize what changed.
832
785
  - List files changed.
833
786
  - Paste the verification commands and results.
787
+ - State whether testing used \`test/fake\` or a real provider selected by the owner.
834
788
  - Call out any missing user details or blocked checks.
835
789
  `;
836
790
  }
837
791
 
838
- type DeployReadinessStatus = "pass" | "fail" | "warn" | "info";
839
-
840
- type DeployReadinessCheck = {
841
- status: DeployReadinessStatus;
842
- title: string;
843
- message: string;
844
- action?: string;
845
- };
792
+ function isRecord(value: unknown): value is Record<string, unknown> {
793
+ return typeof value === "object" && value !== null && !Array.isArray(value);
794
+ }
846
795
 
847
- type DeployReadinessReport = {
848
- ok: boolean;
849
- apiUrl: string;
850
- context: DeployReadinessContext;
851
- checks: DeployReadinessCheck[];
852
- };
853
-
854
- type CloudMeResponse = {
855
- account?: {
856
- email?: string;
857
- status?: string;
858
- };
859
- };
860
-
861
- type CloudCapabilitiesResponse = {
862
- capabilities?: Array<{
863
- name?: string;
864
- status?: string;
865
- }>;
866
- };
867
-
868
- type CloudLimitsResponse = {
869
- online_agents?: {
870
- account?: {
871
- used?: number | null;
872
- limit?: number | null;
873
- };
874
- global?: {
875
- used?: number | null;
876
- limit?: number | null;
877
- };
878
- };
879
- };
880
-
881
- type DeployAccessTokenCreateResponse = {
882
- access_token?: {
883
- id?: string;
884
- deploy_id?: string;
885
- name?: string;
886
- token?: string;
887
- };
888
- };
889
-
890
- type DeployAccessTokenListResponse = {
891
- access_tokens?: Array<{
892
- id?: string;
893
- deploy_id?: string;
894
- name?: string;
895
- }>;
896
- };
897
-
898
- type CloudSecretsResponse = {
899
- secrets?: Array<{
900
- name?: string;
901
- status?: string;
902
- updated_at?: string;
903
- }>;
904
- };
905
-
906
- async function checkDeployReadiness(options: {
796
+ async function ensureDeployChatAccessToken(options: {
907
797
  apiUrl: string;
908
- requireLogin: boolean;
909
- }): Promise<DeployReadinessReport> {
910
- const context = await loadDeployReadinessContext(process.cwd(), process.env);
911
- const auth = await readCloudAuth();
912
- const checks: DeployReadinessCheck[] = [];
913
- let cloudAuthUsable = false;
914
-
915
- if (context.runtime !== "edge") {
916
- checks.push({
917
- status: "fail",
918
- title: "Target runtime",
919
- message: 'Cloudflare deploy requires runtime: "edge" in agentkit.config.ts.',
920
- action: "set runtime: \"edge\" before deploying to AgentKit Cloud",
921
- });
922
- } else {
923
- checks.push({
924
- status: "pass",
925
- title: "Target runtime",
926
- message: "Capsule is configured for edge deploy.",
927
- });
928
- }
929
-
930
- if (!auth?.token) {
931
- checks.push({
932
- status: options.requireLogin ? "fail" : "warn",
933
- title: "Login",
934
- message: options.requireLogin
935
- ? "No AgentKit Cloud token was found for hosted production deploy."
936
- : "No AgentKit Cloud token was found; deploy will request anonymous mode.",
937
- action: "agentkit login --token <token>",
938
- });
939
- } else {
940
- try {
941
- const me = (await cloudApiRequest(options.apiUrl, "/v1/me", { method: "GET" })) as CloudMeResponse;
942
- const email = me.account?.email;
943
- checks.push({
944
- status: "pass",
945
- title: "Login",
946
- message: email ? `Authenticated as ${email}.` : "AgentKit Cloud token is accepted.",
947
- });
948
- cloudAuthUsable = true;
949
- } catch (error) {
950
- checks.push({
951
- status: "fail",
952
- title: "Login",
953
- message: `AgentKit Cloud rejected the token from ${formatCloudAuthSource(auth)}. ${formatReadinessError(error)}`,
954
- action: "agentkit login --token <token>",
955
- });
956
- }
957
- }
958
-
959
- if (cloudAuthUsable) {
960
- try {
961
- const payload = (await cloudApiRequest(options.apiUrl, "/v1/me/capabilities", {
962
- method: "GET",
963
- })) as CloudCapabilitiesResponse;
964
- const capabilities = payload.capabilities ?? [];
965
- const hasCloudflareAlpha = capabilities.some(
966
- (capability) => capability.name === "cloudflare_deploy_alpha" && capability.status === "active",
967
- );
968
-
969
- checks.push({
970
- status: hasCloudflareAlpha ? "pass" : "fail",
971
- title: "Deploy entitlement",
972
- message: hasCloudflareAlpha
973
- ? "cloudflare_deploy_alpha is active."
974
- : "This account is missing the cloudflare_deploy_alpha entitlement.",
975
- ...(hasCloudflareAlpha ? {} : { action: "log in with an invited AgentKit Cloud alpha token" }),
976
- });
977
- } catch (error) {
978
- checks.push({
979
- status: "fail",
980
- title: "Deploy entitlement",
981
- message: `Could not verify AgentKit Cloud capabilities. ${formatReadinessError(error)}`,
982
- });
983
- }
984
-
985
- try {
986
- const payload = (await cloudApiRequest(options.apiUrl, "/v1/me/limits", {
987
- method: "GET",
988
- })) as CloudLimitsResponse;
989
- const account = payload.online_agents?.account;
990
- const global = payload.online_agents?.global;
991
- const accountLimit = normalizeOptionalNumber(account?.limit);
992
- const accountUsed = normalizeOptionalNumber(account?.used);
993
- const globalLimit = normalizeOptionalNumber(global?.limit);
994
- const globalUsed = normalizeOptionalNumber(global?.used);
995
-
996
- if (accountLimit !== undefined && accountUsed !== undefined && accountUsed >= accountLimit) {
997
- checks.push({
998
- status: "fail",
999
- title: "Online agent capacity",
1000
- message: `This account is already using ${accountUsed}/${accountLimit} online AgentKit deploys.`,
1001
- action: "agentkit deploy pause",
1002
- });
1003
- } else if (globalLimit !== undefined && globalUsed !== undefined && globalUsed >= globalLimit) {
1004
- checks.push({
1005
- status: "fail",
1006
- title: "Online agent capacity",
1007
- message: `AgentKit Cloud is already using ${globalUsed}/${globalLimit} global online deploy slots.`,
1008
- action: "agentkit deploy status",
1009
- });
1010
- } else if (accountLimit !== undefined && accountUsed !== undefined) {
1011
- checks.push({
1012
- status: "pass",
1013
- title: "Online agent capacity",
1014
- message: `Account has capacity: ${accountUsed}/${accountLimit} online deploys are in use.`,
1015
- });
1016
- } else if (globalLimit !== undefined && globalUsed !== undefined) {
1017
- checks.push({
1018
- status: "pass",
1019
- title: "Online agent capacity",
1020
- message: `Global capacity is available: ${globalUsed}/${globalLimit} online deploys are in use.`,
1021
- });
1022
- } else {
1023
- checks.push({
1024
- status: "info",
1025
- title: "Online agent capacity",
1026
- message: "No online deploy limit is configured for this account.",
1027
- });
1028
- }
1029
- } catch (error) {
1030
- checks.push({
1031
- status: "warn",
1032
- title: "Online agent capacity",
1033
- message: `Could not verify online deploy capacity. ${formatReadinessError(error)}`,
1034
- });
1035
- }
1036
- }
1037
-
1038
- if (context.agentKitManagedSecrets.length > 0) {
1039
- checks.push({
1040
- status: "info",
1041
- title: "AgentKit-managed secrets",
1042
- message: `${context.agentKitManagedSecrets.join(", ")} ${
1043
- context.agentKitManagedSecrets.length === 1 ? "is" : "are"
1044
- } provisioned by AgentKit Cloud during deploy.`,
1045
- });
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" };
1046
808
  }
1047
809
 
1048
- if (context.accessMode === "private") {
1049
- checks.push({
1050
- status: "info",
1051
- title: "Private access",
1052
- message: "AGENTKIT_PRIVATE_TOKEN is required at runtime; AgentKit Cloud generates it and saves it in .agentkit/deploy.json.",
1053
- });
1054
- }
810
+ try {
811
+ const path = join(options.root, defaultDeployChatAccessTokenPath);
812
+ const existing = await readDeployAccessTokenFile(path);
1055
813
 
1056
- if (context.userManagedSecrets.length === 0) {
1057
- checks.push({
1058
- status: "pass",
1059
- title: "Hosted secrets",
1060
- message: "No user-managed hosted secrets are required by this capsule.",
1061
- });
1062
- } else if (!cloudAuthUsable) {
1063
- checks.push({
1064
- status: options.requireLogin ? "fail" : "warn",
1065
- title: "Hosted secrets",
1066
- message: `Could not check hosted secrets without a valid AgentKit Cloud login. Required: ${context.userManagedSecrets.join(", ")}.`,
1067
- action: "agentkit secret list",
1068
- });
1069
- } else {
1070
- try {
1071
- const payload = (await cloudApiRequest(
814
+ if (existing?.deploy_id === options.deployId && existing.id) {
815
+ const listed = (await cloudApiRequest(
1072
816
  options.apiUrl,
1073
- `/v1/projects/${encodeURIComponent(context.projectId)}/secrets`,
817
+ `/v1/deploys/${encodeURIComponent(options.deployId)}/access-tokens`,
1074
818
  { method: "GET" },
1075
- )) as CloudSecretsResponse;
1076
- const hostedSecretNames = new Set((payload.secrets ?? []).map((secret) => secret.name).filter(isString));
1077
- const missing = context.userManagedSecrets.filter((name) => !hostedSecretNames.has(name));
1078
-
1079
- if (missing.length === 0) {
1080
- checks.push({
1081
- status: "pass",
1082
- title: "Hosted secrets",
1083
- message: "All user-managed hosted secrets are set.",
1084
- });
1085
- } else {
1086
- for (const secret of missing) {
1087
- const localSource = context.localSecrets[secret];
1088
- checks.push({
1089
- status: "fail",
1090
- title: `Hosted secret ${secret}`,
1091
- message: localSource
1092
- ? `${secret} is present locally but has not been set in AgentKit Cloud.`
1093
- : `${secret} is required but missing from AgentKit Cloud.`,
1094
- action: secretSetCommand(secret, localSource),
1095
- });
1096
- }
819
+ )) as DeployAccessTokenListResponse;
820
+ const stillExists = (listed.access_tokens ?? []).some((token) => token.id === existing.id);
821
+
822
+ if (stillExists) {
823
+ return { status: "ready", path, tokenId: existing.id, created: false };
1097
824
  }
1098
- } catch (error) {
1099
- checks.push({
1100
- status: "fail",
1101
- title: "Hosted secrets",
1102
- message: `Could not list hosted secrets for project ${context.projectId}. ${formatReadinessError(error)}`,
1103
- action: "agentkit secret list",
1104
- });
1105
825
  }
1106
- }
1107
826
 
1108
- return {
1109
- ok: checks.every((check) => check.status !== "fail"),
1110
- apiUrl: options.apiUrl,
1111
- context,
1112
- checks,
1113
- };
1114
- }
1115
-
1116
- function printDeployReadinessReport(report: DeployReadinessReport): void {
1117
- console.log("AgentKit deploy readiness");
1118
- console.log(`Project: ${report.context.projectName}`);
1119
- console.log(`Project ID: ${report.context.projectId}`);
1120
- console.log(`Target: ${report.context.target}`);
1121
- console.log(`API: ${report.apiUrl}`);
1122
- console.log("");
1123
-
1124
- for (const check of report.checks) {
1125
- console.log(`[${check.status}] ${check.title}: ${check.message}`);
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;
1126
836
 
1127
- if (check.action) {
1128
- console.log(` Run: ${check.action}`);
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.");
1129
839
  }
1130
- }
1131
- }
1132
-
1133
- function secretSetCommand(secret: string, source: DeployReadinessContext["localSecrets"][string] | undefined): string {
1134
- if (source?.dotenv) {
1135
- return `agentkit secret set ${secret} --from-local-env`;
1136
- }
1137
840
 
1138
- if (source?.environment) {
1139
- return `agentkit secret set ${secret} --from-env`;
841
+ await writeSecretJson(path, created);
842
+ return { status: "ready", path, tokenId: token.id, created: true };
843
+ } catch (error) {
844
+ return { status: "failed", error };
1140
845
  }
1141
-
1142
- return `agentkit secret set ${secret} --stdin`;
1143
846
  }
1144
847
 
1145
- function normalizeOptionalNumber(value: number | null | undefined): number | undefined {
1146
- return typeof value === "number" && Number.isFinite(value) ? value : undefined;
1147
- }
1148
-
1149
- function formatReadinessError(error: unknown): string {
1150
- if (isAgentKitError(error)) {
1151
- return `${error.code}: ${error.message}`;
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;
1152
854
  }
1153
-
1154
- return error instanceof Error ? error.message : String(error);
1155
855
  }
1156
856
 
1157
- function isString(value: unknown): value is string {
1158
- return typeof value === "string";
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 });
1159
860
  }
1160
861
 
1161
- async function handleChannelsCommand(args: ParsedArgs): Promise<void> {
1162
- const [subcommand, first, second, third] = args.positional;
1163
-
1164
- if (subcommand === "list") {
1165
- const deployState = await readDeployStateIfExists(process.cwd());
1166
-
1167
- if (deployState) {
1168
- const channels = await cloudGet<{ channels: CloudChannel[] }>(
1169
- parseCloudApiUrl(args.flags.api),
1170
- `/v1/deploys/${encodeURIComponent(deployState.deploy_id)}/channels`,
1171
- );
1172
- printChannelsTable(channels.channels);
1173
- return;
1174
- }
1175
-
1176
- const capsule = await loadAgentCapsule(process.cwd());
1177
- printChannelsTable(
1178
- (capsule.config.channels ?? []).map((channel) => ({
1179
- id: "",
1180
- name: channel.name,
1181
- type: channel.type,
1182
- provider: channel.provider,
1183
- status: "local_config",
1184
- webhook_url: "",
1185
- required_secrets: channel.secrets.map((name) => ({ name, status: "missing" })),
1186
- })),
1187
- );
1188
- return;
1189
- }
1190
-
1191
- if (subcommand === "add") {
1192
- const type = first;
1193
- const name = second;
1194
-
1195
- if (!type || !name || (type !== "website" && type !== "telegram" && type !== "whatsapp")) {
1196
- throw new Error("Usage: agentkit channels add <website|telegram|whatsapp> <name> [--provider zapster|meta]");
1197
- }
1198
-
1199
- const deployState = await readDeployStateRequired(process.cwd());
1200
- const provider = channelProviderForCli(type, args.flags.provider);
1201
- const payload = {
1202
- name,
1203
- type,
1204
- provider,
1205
- secrets: defaultChannelSecretsForCli(type, provider),
1206
- };
1207
- const response = await cloudPost<{ channel: CloudChannel }>(
1208
- parseCloudApiUrl(args.flags.api),
1209
- `/v1/deploys/${encodeURIComponent(deployState.deploy_id)}/channels`,
1210
- payload,
1211
- );
1212
- printChannelStatus(response.channel);
1213
- return;
1214
- }
1215
-
1216
- if (subcommand === "setup") {
1217
- if (!first) {
1218
- throw new Error("Usage: agentkit channels setup <name> [--apply]");
1219
- }
1220
-
1221
- const channel = await resolveCloudChannelByName(first, args.flags.api);
1222
- printChannelStatus(channel);
1223
- if (channel.type === "telegram") {
1224
- if (args.flags.apply) {
1225
- await applyTelegramWebhook(channel);
1226
- console.log("Telegram webhook updated.");
1227
- } else {
1228
- console.log(`Setup: call Telegram setWebhook with ${channel.webhook_url}`);
1229
- console.log("Use --apply to mutate Telegram webhook settings.");
1230
- }
1231
- } else if (channel.type === "whatsapp") {
1232
- console.log(`Next step: paste this URL into ${channel.provider}'s webhook settings.`);
1233
- } else {
1234
- console.log(`Website endpoint: ${channel.webhook_url}`);
1235
- console.log("Use this endpoint from the AgentKit website client or a signed server-to-server test.");
1236
- }
1237
- return;
1238
- }
1239
-
1240
- if (subcommand === "status") {
1241
- if (!first) {
1242
- throw new Error("Usage: agentkit channels status <name>");
1243
- }
862
+ async function requireLocalDeployState(): Promise<LocalDeployState> {
863
+ const state = await readLocalDeployStateIfExists(process.cwd());
1244
864
 
1245
- const channel = await resolveCloudChannelByName(first, args.flags.api);
1246
- printChannelStatus(channel);
1247
- await printChannelDeliverySummary(channel, args.flags.api);
1248
- return;
865
+ if (!state) {
866
+ throw new Error("No .agentkit/deploy.json found. Run agentkit deploy first.");
1249
867
  }
1250
868
 
1251
- if (subcommand === "test") {
1252
- if (!first) {
1253
- throw new Error('Usage: agentkit channels test <name> [--message "hello"] [--fixture <path>]');
1254
- }
1255
-
1256
- const channel = await resolveCloudChannelByName(first, args.flags.api);
1257
- const apiUrl = parseCloudApiUrl(args.flags.api);
1258
- const body = await channelTestBody(channel, args.flags.message, args.flags.fixture);
1259
- const path = new URL(channel.webhook_url).pathname;
1260
- const headers = channelTestHeaders(channel);
1261
- const response = await cloudFetch(apiUrl, path, {
1262
- method: channel.type === "whatsapp" && channel.provider === "meta" ? "POST" : "POST",
1263
- headers,
1264
- body,
1265
- });
1266
- const payload = await response.json();
869
+ return state;
870
+ }
1267
871
 
1268
- console.log(JSON.stringify(payload, null, 2));
1269
- if (!response.ok) {
1270
- process.exitCode = 1;
1271
- }
1272
- return;
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.`);
1273
875
  }
1274
876
 
1275
- if (subcommand === "deliveries") {
1276
- if (first === "list") {
1277
- if (!second) {
1278
- throw new Error("Usage: agentkit channels deliveries list <name>");
1279
- }
1280
-
1281
- const channel = await resolveCloudChannelByName(second, args.flags.api);
1282
- const response = await cloudGet<{ deliveries: CloudDelivery[] }>(
1283
- parseCloudApiUrl(args.flags.api),
1284
- `/v1/channels/${encodeURIComponent(channel.id)}/deliveries`,
1285
- );
1286
-
1287
- if (response.deliveries.length === 0) {
1288
- console.log("No channel deliveries found.");
1289
- return;
1290
- }
1291
-
1292
- console.log("ID\tCHANNEL\tDIRECTION\tSTATUS\tPROVIDER_EVENT\tRECEIVED");
1293
- for (const delivery of response.deliveries) {
1294
- console.log(
1295
- [
1296
- delivery.id,
1297
- delivery.channel_id,
1298
- delivery.direction,
1299
- delivery.status,
1300
- delivery.provider.event_id ?? "",
1301
- delivery.received_at,
1302
- ].join("\t"),
1303
- );
1304
- }
1305
- return;
1306
- }
1307
-
1308
- if (first === "show") {
1309
- if (!second) {
1310
- throw new Error("Usage: agentkit channels deliveries show <delivery-id>");
1311
- }
877
+ return state.url;
878
+ }
1312
879
 
1313
- const response = await cloudGet<{ delivery: CloudDelivery }>(
1314
- parseCloudApiUrl(args.flags.api),
1315
- `/v1/channel-deliveries/${encodeURIComponent(second)}`,
1316
- );
1317
- console.log(JSON.stringify(response.delivery, null, 2));
1318
- return;
1319
- }
880
+ async function readDeployAccessTokenForSmoke(cwd: string, state: LocalDeployState): Promise<string | undefined> {
881
+ if (state.access?.mode === "public") {
882
+ return undefined;
1320
883
  }
1321
884
 
1322
- throw new Error(
1323
- "Usage: agentkit channels list | add | setup | status | test | deliveries list | deliveries show",
1324
- );
885
+ const root = await findAgentCapsuleRoot(cwd);
886
+ return readDeployAccessTokenForUi(join(root, defaultDeployChatAccessTokenPath));
1325
887
  }
1326
888
 
1327
- type DeployState = LocalDeployState;
1328
-
1329
- type CloudChannel = {
1330
- id: string;
1331
- name: string;
1332
- type: "website" | "telegram" | "whatsapp";
1333
- provider: "agentkit" | "telegram" | "zapster" | "meta";
1334
- status: string;
1335
- webhook_url: string;
1336
- required_secrets: Array<{ name: string; status: string }>;
1337
- };
1338
-
1339
- type CloudDelivery = {
1340
- id: string;
1341
- channel_id: string;
1342
- direction: string;
1343
- status: string;
1344
- provider: {
1345
- event_id?: string;
1346
- };
1347
- received_at: string;
1348
- };
1349
-
1350
- async function applyTelegramWebhook(channel: CloudChannel): Promise<void> {
1351
- const botToken = process.env.TELEGRAM_BOT_TOKEN;
1352
- const secretToken = process.env.TELEGRAM_WEBHOOK_SECRET;
1353
-
1354
- if (!botToken || !secretToken) {
1355
- throw new Error("TELEGRAM_BOT_TOKEN and TELEGRAM_WEBHOOK_SECRET must be set to apply Telegram setup.");
889
+ async function runDeploySmoke(input: { deployUrl: string; message: string; token?: string }): Promise<void> {
890
+ const headers = new Headers({ "Content-Type": "application/json" });
891
+
892
+ if (input.token) {
893
+ headers.set("Authorization", `Bearer ${input.token}`);
1356
894
  }
1357
895
 
1358
896
  let response: Response;
1359
897
 
1360
898
  try {
1361
- response = await fetch(`https://api.telegram.org/bot${botToken}/setWebhook`, {
899
+ response = await fetch(new URL("/v1/chat", input.deployUrl).href, {
1362
900
  method: "POST",
1363
- headers: { "Content-Type": "application/json" },
1364
- body: JSON.stringify({
1365
- url: channel.webhook_url,
1366
- secret_token: secretToken,
1367
- allowed_updates: ["message"],
1368
- }),
901
+ headers,
902
+ body: JSON.stringify({ message: input.message }),
1369
903
  });
1370
- } catch {
1371
- throw new Error("Telegram setWebhook request failed before Telegram returned a response.");
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
+ );
1372
909
  }
1373
910
 
1374
- const payload = await response.json().catch(() => ({}));
911
+ const payload = await response.json().catch(() => null);
1375
912
 
1376
- if (!response.ok || !isTelegramOk(payload)) {
1377
- throw new Error(readTelegramSetupError(payload, response.status));
913
+ if (!response.ok) {
914
+ throw new AgentKitError("deploy_smoke_failed", readCloudError(payload, response.status));
1378
915
  }
1379
- }
1380
916
 
1381
- function isTelegramOk(payload: unknown): payload is { ok: true } {
1382
- return Boolean(payload && typeof payload === "object" && "ok" in payload && payload.ok === true);
1383
- }
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;
1384
921
 
1385
- function readTelegramSetupError(payload: unknown, status: number): string {
1386
- if (payload && typeof payload === "object" && "description" in payload && typeof payload.description === "string") {
1387
- return `Telegram setWebhook failed: ${payload.description}`;
922
+ console.log("");
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}`);
1388
928
  }
1389
-
1390
- return `Telegram setWebhook failed with HTTP ${status}.`;
1391
929
  }
1392
930
 
1393
- async function readDeployStateIfExists(cwd: string): Promise<DeployState | null> {
1394
- return readLocalDeployStateIfExists(cwd);
931
+ function printJson(value: unknown): void {
932
+ console.log(JSON.stringify(value, null, 2));
1395
933
  }
1396
934
 
1397
- async function readDeployStateRequired(cwd: string): Promise<DeployState> {
1398
- const state = await readDeployStateIfExists(cwd);
1399
-
1400
- if (!state?.deploy_id) {
1401
- throw new Error("No .agentkit/deploy.json found. Run agentkit deploy before creating hosted channels.");
1402
- }
1403
-
1404
- return state;
1405
- }
935
+ async function printJsonOrWrite(value: unknown, outFlag: string | boolean | undefined, usage: string): Promise<void> {
936
+ const outPath = parseOptionalStringFlag(outFlag, "out", usage);
1406
937
 
1407
- function printChannelsTable(channels: CloudChannel[]): void {
1408
- if (channels.length === 0) {
1409
- console.log("No channels found.");
938
+ if (!outPath) {
939
+ printJson(value);
1410
940
  return;
1411
941
  }
1412
942
 
1413
- console.log("NAME\tTYPE\tPROVIDER\tSTATUS\tLAST_EVENT");
1414
- for (const channel of channels) {
1415
- console.log([channel.name, channel.type, channel.provider, channel.status, ""].join("\t"));
1416
- }
1417
- }
1418
-
1419
- function printChannelStatus(channel: CloudChannel): void {
1420
- console.log(`Channel: ${channel.name}`);
1421
- if (channel.id) {
1422
- console.log(`ID: ${channel.id}`);
1423
- }
1424
- console.log(`Type: ${channel.type}`);
1425
- console.log(`Provider: ${channel.provider}`);
1426
- console.log(`Status: ${channel.status}`);
1427
- if (channel.webhook_url) {
1428
- console.log(`Webhook URL: ${channel.webhook_url}`);
1429
- }
1430
- console.log("Required secrets:");
1431
- for (const secret of channel.required_secrets) {
1432
- console.log(` ${secret.name}: ${secret.status}`);
1433
- }
943
+ const absolutePath = resolve(process.cwd(), outPath);
944
+ await writeSecretJson(absolutePath, value);
945
+ console.log(`Deploy access token written to ${absolutePath}`);
1434
946
  }
1435
947
 
1436
- async function printChannelDeliverySummary(
1437
- channel: CloudChannel,
1438
- apiFlag: string | boolean | undefined,
1439
- ): Promise<void> {
1440
- const response = await cloudGet<{ deliveries: CloudDelivery[] }>(
1441
- parseCloudApiUrl(apiFlag),
1442
- `/v1/channels/${encodeURIComponent(channel.id)}/deliveries`,
1443
- );
1444
- const inbound = [...response.deliveries].reverse().find((delivery) => delivery.direction === "inbound");
1445
- const outbound = [...response.deliveries].reverse().find((delivery) => delivery.direction === "outbound");
1446
-
1447
- console.log("Recent delivery:");
1448
- console.log(` Last inbound: ${inbound ? `${inbound.status} ${inbound.received_at}` : "none"}`);
1449
- console.log(` Last outbound: ${outbound ? `${outbound.status} ${outbound.received_at}` : "none"}`);
1450
- console.log("Provider health: not_checked");
948
+ function relativePath(root: string, path: string): string {
949
+ const output = relative(root, path);
950
+ return output.startsWith("..") ? path : output || ".";
1451
951
  }
1452
952
 
1453
- async function resolveCloudChannelByName(name: string, apiFlag: string | boolean | undefined): Promise<CloudChannel> {
1454
- const deployState = await readDeployStateRequired(process.cwd());
1455
- const response = await cloudGet<{ channels: CloudChannel[] }>(
1456
- parseCloudApiUrl(apiFlag),
1457
- `/v1/deploys/${encodeURIComponent(deployState.deploy_id)}/channels`,
1458
- );
1459
- const channel = response.channels.find((candidate) => candidate.name === name);
1460
-
1461
- if (!channel) {
1462
- throw new Error(`Channel "${name}" was not found for deploy ${deployState.deploy_id}.`);
1463
- }
1464
-
1465
- return channel;
1466
- }
1467
-
1468
- function channelProviderForCli(type: string, providerFlag: string | boolean | undefined): string {
1469
- if (type === "website") {
1470
- return "agentkit";
1471
- }
1472
-
1473
- if (type === "telegram") {
1474
- return "telegram";
1475
- }
1476
-
1477
- const provider = providerFlag ?? "zapster";
1478
-
1479
- if (provider !== "zapster" && provider !== "meta") {
1480
- throw new Error('WhatsApp provider must be "zapster" or "meta".');
1481
- }
1482
-
1483
- return provider;
1484
- }
1485
-
1486
- function defaultChannelSecretsForCli(type: string, provider: string): string[] {
1487
- if (type === "website") {
1488
- return ["AGENTKIT_WEBSITE_CHANNEL_TOKEN"];
1489
- }
1490
-
1491
- if (type === "telegram") {
1492
- return ["TELEGRAM_BOT_TOKEN", "TELEGRAM_WEBHOOK_SECRET"];
1493
- }
1494
-
1495
- if (type === "whatsapp" && provider === "zapster") {
1496
- return ["ZAPSTER_API_KEY", "ZAPSTER_WEBHOOK_SECRET"];
1497
- }
1498
-
1499
- if (type === "whatsapp" && provider === "meta") {
1500
- return ["META_WHATSAPP_ACCESS_TOKEN", "META_WHATSAPP_APP_SECRET", "META_WHATSAPP_VERIFY_TOKEN"];
1501
- }
1502
-
1503
- return [];
1504
- }
1505
-
1506
- async function channelTestBody(
1507
- channel: CloudChannel,
1508
- messageFlag: string | boolean | undefined,
1509
- fixtureFlag: string | boolean | undefined,
1510
- ): Promise<string> {
1511
- if (typeof fixtureFlag === "string") {
1512
- return readFile(resolve(process.cwd(), fixtureFlag), "utf8");
1513
- }
1514
-
1515
- const message = typeof messageFlag === "string" ? messageFlag : "hello";
1516
-
1517
- if (channel.type === "telegram") {
1518
- return JSON.stringify({
1519
- update_id: Date.now(),
1520
- message: {
1521
- message_id: 1,
1522
- chat: { id: 456, type: "private" },
1523
- from: { id: 123, first_name: "AgentKit" },
1524
- text: message,
1525
- },
1526
- });
1527
- }
1528
-
1529
- if (channel.type === "whatsapp") {
1530
- return JSON.stringify({
1531
- event_id: `evt_${Date.now()}`,
1532
- instance_id: "local",
1533
- message: {
1534
- id: `msg_${Date.now()}`,
1535
- from: "5511999999999",
1536
- type: "text",
1537
- text: { body: message },
1538
- },
1539
- });
1540
- }
1541
-
1542
- return JSON.stringify({
1543
- event_id: `web_${Date.now()}`,
1544
- visitor_id: "visitor_test",
1545
- message: {
1546
- id: `msg_${Date.now()}`,
1547
- text: message,
1548
- },
1549
- });
1550
- }
1551
-
1552
- function channelTestHeaders(channel: CloudChannel): Headers {
1553
- const headers = new Headers({ "Content-Type": "application/json" });
1554
-
1555
- if (channel.type === "website") {
1556
- const token = process.env.AGENTKIT_WEBSITE_CHANNEL_TOKEN;
1557
-
1558
- if (token) {
1559
- headers.set("Authorization", `Bearer ${token}`);
1560
- }
1561
- }
1562
-
1563
- if (channel.type === "telegram") {
1564
- headers.set("X-Telegram-Bot-Api-Secret-Token", process.env.TELEGRAM_WEBHOOK_SECRET ?? "");
1565
- }
1566
-
1567
- if (channel.type === "whatsapp" && channel.provider === "zapster") {
1568
- headers.set("X-Zapster-Webhook-Secret", process.env.ZAPSTER_WEBHOOK_SECRET ?? "");
1569
- }
1570
-
1571
- return headers;
1572
- }
1573
-
1574
- async function cloudGet<T>(apiUrl: string, path: string): Promise<T> {
1575
- const response = await cloudFetch(apiUrl, path);
1576
- const payload = await response.json();
1577
-
1578
- if (!response.ok) {
1579
- throw new Error(readCloudError(payload, response.status));
1580
- }
1581
-
1582
- return payload as T;
1583
- }
1584
-
1585
- async function cloudPost<T>(apiUrl: string, path: string, body: unknown): Promise<T> {
1586
- const response = await cloudFetch(apiUrl, path, {
1587
- method: "POST",
1588
- headers: { "Content-Type": "application/json" },
1589
- body: JSON.stringify(body),
1590
- });
1591
- const payload = await response.json();
1592
-
1593
- if (!response.ok) {
1594
- throw new Error(readCloudError(payload, response.status));
1595
- }
1596
-
1597
- return payload as T;
1598
- }
1599
-
1600
- function cloudFetch(apiUrl: string, path: string, init: RequestInit = {}): Promise<Response> {
1601
- const headers = new Headers(init.headers);
1602
- const apiToken = process.env.AGENTKIT_CLOUD_API_TOKEN;
1603
-
1604
- if (apiToken && !headers.has("Authorization")) {
1605
- headers.set("Authorization", `Bearer ${apiToken}`);
1606
- }
1607
-
1608
- return fetch(new URL(path, parseCloudApiUrl(apiUrl)).href, {
1609
- ...init,
1610
- headers,
1611
- });
1612
- }
1613
-
1614
- type CloudAuthConfig = {
1615
- apiUrl: string;
1616
- token: string;
1617
- source?: "environment" | "file";
1618
- };
1619
-
1620
- function cloudAuthPath(): string {
1621
- return join(homedir(), ".agentkit", "cloud.json");
1622
- }
1623
-
1624
- async function readCloudAuth(): Promise<CloudAuthConfig | null> {
1625
- if (process.env.AGENTKIT_CLOUD_API_TOKEN) {
1626
- return {
1627
- apiUrl: process.env.AGENTKIT_CLOUD_API_URL ?? defaultAgentKitCloudApiUrl,
1628
- token: process.env.AGENTKIT_CLOUD_API_TOKEN,
1629
- source: "environment",
1630
- };
1631
- }
1632
-
1633
- try {
1634
- const parsed = JSON.parse(await readFile(cloudAuthPath(), "utf8"));
1635
-
1636
- if (parsed && typeof parsed === "object" && typeof parsed.token === "string") {
1637
- return {
1638
- apiUrl: typeof parsed.apiUrl === "string" ? parsed.apiUrl : defaultAgentKitCloudApiUrl,
1639
- token: parsed.token,
1640
- source: "file",
1641
- };
1642
- }
1643
- } catch {
1644
- return null;
1645
- }
1646
-
1647
- return null;
1648
- }
1649
-
1650
- async function writeCloudAuth(config: CloudAuthConfig): Promise<void> {
1651
- await mkdir(join(homedir(), ".agentkit"), { recursive: true });
1652
- await writeFile(cloudAuthPath(), `${JSON.stringify(config, null, 2)}\n`, { mode: 0o600 });
1653
- }
1654
-
1655
- async function ensureDeployChatAccessToken(options: {
1656
- apiUrl: string;
1657
- root: string;
1658
- deployId: string;
1659
- skip: boolean;
1660
- }): Promise<
1661
- | { status: "ready"; path: string; tokenId: string; created: boolean }
1662
- | { status: "skipped" }
1663
- | { status: "failed"; error: unknown }
1664
- > {
1665
- if (options.skip) {
1666
- return { status: "skipped" };
1667
- }
1668
-
1669
- try {
1670
- const path = join(options.root, defaultDeployChatAccessTokenPath);
1671
- const existing = await readDeployAccessTokenFile(path);
1672
-
1673
- if (existing?.deploy_id === options.deployId && existing.id) {
1674
- const listed = (await cloudApiRequest(
1675
- options.apiUrl,
1676
- `/v1/deploys/${encodeURIComponent(options.deployId)}/access-tokens`,
1677
- { method: "GET" },
1678
- )) as DeployAccessTokenListResponse;
1679
- const stillExists = (listed.access_tokens ?? []).some((token) => token.id === existing.id);
1680
-
1681
- if (stillExists) {
1682
- return { status: "ready", path, tokenId: existing.id, created: false };
1683
- }
1684
- }
1685
-
1686
- const created = (await cloudApiRequest(
1687
- options.apiUrl,
1688
- `/v1/deploys/${encodeURIComponent(options.deployId)}/access-tokens`,
1689
- {
1690
- method: "POST",
1691
- body: JSON.stringify({ name: defaultDeployChatAccessTokenName }),
1692
- },
1693
- )) as DeployAccessTokenCreateResponse;
1694
- const token = created.access_token;
1695
-
1696
- if (!token?.id || token.deploy_id !== options.deployId || !token.token) {
1697
- throw new AgentKitError("access_token_invalid", "AgentKit Cloud returned an invalid deploy access token payload.");
1698
- }
1699
-
1700
- await writeSecretJson(path, created);
1701
- return { status: "ready", path, tokenId: token.id, created: true };
1702
- } catch (error) {
1703
- return { status: "failed", error };
1704
- }
1705
- }
1706
-
1707
- async function readDeployAccessTokenFile(path: string): Promise<{ id?: string; deploy_id?: string } | null> {
1708
- try {
1709
- const parsed = JSON.parse(await readFile(path, "utf8")) as DeployAccessTokenCreateResponse;
1710
- return parsed.access_token ?? null;
1711
- } catch {
1712
- return null;
1713
- }
1714
- }
1715
-
1716
- async function writeSecretJson(path: string, value: unknown): Promise<void> {
1717
- await mkdir(dirname(path), { recursive: true });
1718
- await writeFile(path, `${JSON.stringify(value, null, 2)}\n`, { mode: 0o600 });
1719
- }
1720
-
1721
- async function verifyCloudLogin(
1722
- apiUrl: string,
1723
- token: string,
1724
- ): Promise<{ me: CloudMeResponse; capabilities: CloudCapabilitiesResponse }> {
1725
- const me = await cloudApiRequestWithToken<CloudMeResponse>(apiUrl, "/v1/me", token, { method: "GET" });
1726
- const capabilities = await cloudApiRequestWithToken<CloudCapabilitiesResponse>(
1727
- apiUrl,
1728
- "/v1/me/capabilities",
1729
- token,
1730
- { method: "GET" },
1731
- );
1732
-
1733
- return { me, capabilities };
1734
- }
1735
-
1736
- async function clearCloudAuth(): Promise<void> {
1737
- await rm(cloudAuthPath(), { force: true });
1738
- }
1739
-
1740
- async function resolveCloudApiUrl(flag: string | boolean | undefined): Promise<string> {
1741
- if (flag !== undefined) {
1742
- return parseCloudApiUrl(flag);
1743
- }
1744
-
1745
- const auth = await readCloudAuth();
1746
- return auth?.apiUrl ?? parseCloudApiUrl(undefined);
1747
- }
1748
-
1749
- async function requireLocalDeployState(): Promise<LocalDeployState> {
1750
- const state = await readLocalDeployStateIfExists(process.cwd());
1751
-
1752
- if (!state) {
1753
- throw new Error("No .agentkit/deploy.json found. Run agentkit deploy first.");
1754
- }
1755
-
1756
- return state;
1757
- }
1758
-
1759
- async function resolveProjectIdForSecretCommand(cwd: string): Promise<string> {
1760
- const state = await readLocalDeployStateIfExists(cwd);
1761
-
1762
- if (state?.project_id) {
1763
- return state.project_id;
1764
- }
1765
-
1766
- const root = await findAgentCapsuleRoot(cwd);
1767
- const capsule = await loadAgentCapsule(root);
1768
- return stableCloudProjectId(capsule.config.name);
1769
- }
1770
-
1771
- async function cloudApiRequest(apiUrl: string, path: string, init: RequestInit): Promise<unknown> {
1772
- const auth = await readCloudAuth();
1773
- const headers = new Headers(init.headers);
1774
-
1775
- if (!headers.has("Content-Type") && init.body !== undefined) {
1776
- headers.set("Content-Type", "application/json");
1777
- }
1778
-
1779
- if (auth?.token && !headers.has("Authorization")) {
1780
- headers.set("Authorization", `Bearer ${auth.token}`);
1781
- }
1782
-
1783
- const response = await fetch(new URL(path, parseCloudApiUrl(apiUrl)).href, {
1784
- ...init,
1785
- headers,
1786
- });
1787
- const payload = await response.json().catch(() => null);
1788
-
1789
- if (!response.ok) {
1790
- const code =
1791
- payload && typeof payload === "object" && "error" in payload
1792
- ? String((payload as { error?: { code?: unknown } }).error?.code ?? "cloud_request_failed")
1793
- : "cloud_request_failed";
1794
- let message = readCloudError(payload, response.status);
1795
-
1796
- if (code === "auth_token_invalid") {
1797
- message = `${message} Token source: ${auth ? formatCloudAuthSource(auth) : "none"}. Run: agentkit login --token <token>`;
1798
- }
1799
-
1800
- throw new AgentKitError(code, message);
1801
- }
1802
-
1803
- return payload;
1804
- }
1805
-
1806
- async function cloudApiRequestWithToken<T>(
1807
- apiUrl: string,
1808
- path: string,
1809
- token: string,
1810
- init: RequestInit,
1811
- ): Promise<T> {
1812
- const headers = new Headers(init.headers);
1813
-
1814
- if (!headers.has("Content-Type") && init.body !== undefined) {
1815
- headers.set("Content-Type", "application/json");
1816
- }
1817
-
1818
- headers.set("Authorization", `Bearer ${token}`);
1819
-
1820
- const response = await fetch(new URL(path, parseCloudApiUrl(apiUrl)).href, {
1821
- ...init,
1822
- headers,
1823
- });
1824
- const payload = await response.json().catch(() => null);
1825
-
1826
- if (!response.ok) {
1827
- const code =
1828
- payload && typeof payload === "object" && "error" in payload
1829
- ? String((payload as { error?: { code?: unknown } }).error?.code ?? "cloud_request_failed")
1830
- : "cloud_request_failed";
1831
- throw new AgentKitError(code, readCloudError(payload, response.status));
1832
- }
1833
-
1834
- return payload as T;
1835
- }
1836
-
1837
- function printJson(value: unknown): void {
1838
- console.log(JSON.stringify(value, null, 2));
1839
- }
1840
-
1841
- async function printJsonOrWrite(value: unknown, outFlag: string | boolean | undefined, usage: string): Promise<void> {
1842
- const outPath = parseOptionalStringFlag(outFlag, "out", usage);
1843
-
1844
- if (!outPath) {
1845
- printJson(value);
1846
- return;
1847
- }
1848
-
1849
- const absolutePath = resolve(process.cwd(), outPath);
1850
- await writeSecretJson(absolutePath, value);
1851
- console.log(`Deploy access token written to ${absolutePath}`);
1852
- }
1853
-
1854
- function readCloudError(payload: unknown, status: number): string {
1855
- if (payload && typeof payload === "object" && "error" in payload) {
1856
- const error = (payload as { error?: { message?: unknown } }).error;
1857
- if (typeof error?.message === "string") {
1858
- return error.message;
1859
- }
1860
- }
1861
-
1862
- return `AgentKit Cloud request failed with HTTP ${status}.`;
1863
- }
1864
-
1865
- function formatCloudAuthSource(auth: CloudAuthConfig): string {
1866
- if (auth.source === "environment") {
1867
- return "AGENTKIT_CLOUD_API_TOKEN";
1868
- }
1869
-
1870
- if (auth.source === "file") {
1871
- return cloudAuthPath();
1872
- }
1873
-
1874
- return "AgentKit Cloud auth";
1875
- }
1876
-
1877
- function relativePath(root: string, path: string): string {
1878
- const output = relative(root, path);
1879
- return output.startsWith("..") ? path : output || ".";
1880
- }
1881
-
1882
- function parsePort(value: string | boolean | undefined, usage = "agentkit dev --port <number>"): number | undefined {
1883
- if (value === undefined) {
1884
- return undefined;
1885
- }
1886
-
1887
- if (typeof value !== "string") {
1888
- throw new Error(`Missing port. Usage: ${usage}`);
1889
- }
1890
-
1891
- const port = Number(value);
1892
-
1893
- if (!Number.isInteger(port) || port <= 0 || port > 65535) {
1894
- throw new Error(`Invalid port. Usage: ${usage}`);
1895
- }
1896
-
1897
- return port;
1898
- }
1899
-
1900
- function parseTargetFlag(value: string | boolean | undefined, capability: "build" | "deploy"): string {
1901
- if (value === undefined) {
1902
- return "cloudflare";
1903
- }
1904
-
1905
- if (typeof value !== "string") {
1906
- throw new Error(
1907
- `Missing target. Usage: agentkit ${capability} --target ${listTargetsWithCapability(capability).join("|")}`,
1908
- );
1909
- }
1910
-
1911
- return value;
1912
- }
1913
-
1914
- function parseCloudApiUrl(value: string | boolean | undefined): string {
1915
- if (value === undefined) {
1916
- return process.env.AGENTKIT_CLOUD_API_URL ?? defaultAgentKitCloudApiUrl;
1917
- }
1918
-
1919
- if (typeof value !== "string") {
1920
- throw new Error("Missing API URL. Usage: agentkit deploy --api <url>");
1921
- }
1922
-
1923
- return value;
1924
- }
1925
-
1926
- function parseRequiredStringFlag(value: string | boolean | undefined, name: string, usage: string): string {
1927
- if (typeof value !== "string" || value.trim().length === 0) {
1928
- throw new Error(`Missing ${name}. Usage: ${usage}`);
1929
- }
1930
-
1931
- return value.trim();
1932
- }
1933
-
1934
- function parseOptionalStringFlag(value: string | boolean | undefined, name: string, usage: string): string | undefined {
1935
- if (value === undefined) {
1936
- return undefined;
1937
- }
1938
-
1939
- if (typeof value !== "string" || value.trim().length === 0) {
1940
- throw new Error(`Missing ${name}. Usage: ${usage}`);
1941
- }
1942
-
1943
- return value.trim();
1944
- }
1945
-
1946
- async function resolveSecretInputValue(options: {
1947
- name: string | undefined;
1948
- valueParts: string[];
1949
- stdinFlag: string | boolean | undefined;
1950
- fromEnvFlag: string | boolean | undefined;
1951
- fromLocalEnvFlag: string | boolean | undefined;
1952
- localEnvRoot?: string;
1953
- usage: string;
1954
- }): Promise<string> {
1955
- const { name, valueParts, stdinFlag, fromEnvFlag, fromLocalEnvFlag, localEnvRoot, usage } = options;
1956
-
1957
- if (!name) {
1958
- throw new Error(`Missing name. Usage: ${usage}`);
1959
- }
1960
-
1961
- const hasInlineValue = valueParts.length > 0;
1962
- const hasStdin = stdinFlag !== undefined;
1963
- const hasFromEnv = fromEnvFlag !== undefined;
1964
- const hasFromLocalEnv = fromLocalEnvFlag !== undefined;
1965
- const inputModes = [hasInlineValue, hasStdin, hasFromEnv, hasFromLocalEnv].filter(Boolean).length;
1966
-
1967
- if (inputModes !== 1) {
1968
- throw new Error(`Provide exactly one value source. Usage: ${usage}`);
1969
- }
1970
-
1971
- let value: string;
1972
-
1973
- if (hasInlineValue) {
1974
- value = valueParts.join(" ");
1975
- } else if (hasStdin) {
1976
- if (stdinFlag !== true) {
1977
- throw new Error(`--stdin does not take a value. Usage: ${usage}`);
1978
- }
1979
-
1980
- value = stripOneTrailingNewline(await readStdinText());
1981
- } else if (hasFromEnv) {
1982
- const envName = fromEnvFlag === true ? name : parseRequiredStringFlag(fromEnvFlag, "env name", usage);
1983
- const envValue = process.env[envName];
1984
-
1985
- if (envValue === undefined) {
1986
- throw new AgentKitError("secret_not_found", `Environment variable ${envName} is not set.`);
1987
- }
1988
-
1989
- value = envValue;
1990
- } else {
1991
- if (fromLocalEnvFlag !== true) {
1992
- throw new Error(`--from-local-env does not take a value. Usage: ${usage}`);
1993
- }
1994
-
1995
- if (!localEnvRoot) {
1996
- throw new Error(`--from-local-env is not available for this command. Usage: ${usage}`);
1997
- }
1998
-
1999
- const localEnv = await loadCapsuleEnv(localEnvRoot, {});
2000
- const localValue = localEnv[name];
2001
-
2002
- if (localValue === undefined) {
2003
- throw new AgentKitError("secret_not_found", `Local .env does not contain ${name}.`);
2004
- }
2005
-
2006
- value = localValue;
2007
- }
2008
-
2009
- if (value.length === 0) {
2010
- throw new Error(`Secret value cannot be empty. Usage: ${usage}`);
2011
- }
2012
-
2013
- return value;
2014
- }
2015
-
2016
- async function readStdinText(): Promise<string> {
2017
- const chunks: Buffer[] = [];
2018
-
2019
- for await (const chunk of process.stdin) {
2020
- chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
2021
- }
2022
-
2023
- return Buffer.concat(chunks).toString("utf8");
2024
- }
2025
-
2026
- function stripOneTrailingNewline(value: string): string {
2027
- return value.replace(/\r?\n$/, "");
2028
- }
2029
-
2030
- function printBuildResult(result: Awaited<ReturnType<typeof buildAgentCapsule>>): void {
2031
- console.log("AgentKit build complete");
2032
- console.log("");
2033
- console.log(`Target: ${result.target}`);
2034
- console.log(`Build dir: ${result.buildDir}`);
2035
- console.log(`Manifest: ${result.manifestPath}`);
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}`);
2036
959
 
2037
960
  if (result.target === "cloudflare") {
2038
961
  console.log(`Worker: ${result.workerPath}`);
@@ -2092,18 +1015,6 @@ async function installProjectDependencies(projectPath: string): Promise<void> {
2092
1015
  }
2093
1016
  }
2094
1017
 
2095
- function runInheritedCommand(command: string, args: string[], cwd: string): Promise<number> {
2096
- return new Promise((resolvePromise, reject) => {
2097
- const child = spawn(command, args, {
2098
- cwd,
2099
- stdio: "inherit",
2100
- });
2101
-
2102
- child.once("error", reject);
2103
- child.once("close", (code) => resolvePromise(code ?? 1));
2104
- });
2105
- }
2106
-
2107
1018
  async function parseToolInput(
2108
1019
  cwd: string,
2109
1020
  inputFlag: string | boolean | undefined,
@@ -2147,393 +1058,81 @@ function parseJsonInput(source: string, label: string): unknown {
2147
1058
  }
2148
1059
  }
2149
1060
 
2150
- async function startDeployChatUiServer(
2151
- cwd: string,
2152
- options: { port: number; tokenFile?: string },
2153
- ): Promise<{ url: string; deployUrl: string; tokenSource: string; stop(): Promise<void> }> {
2154
- const root = await findAgentCapsuleRoot(cwd);
2155
- const state = await readLocalDeployStateIfExists(root);
1061
+ async function findRunningLocalDevServer(cwd: string): Promise<{ url: string; agent: string } | null> {
1062
+ const capsule = await loadAgentCapsule(cwd);
1063
+ const basePort = localDevBasePort();
2156
1064
 
2157
- if (!state?.url) {
2158
- throw new Error("No hosted deploy URL found. Run agentkit deploy first.");
2159
- }
1065
+ for (let port = basePort; port <= basePort + 50; port += 1) {
1066
+ const state = await fetchLocalAgentKitState(`http://localhost:${port}/_agentkit`);
2160
1067
 
2161
- const tokenPath = options.tokenFile
2162
- ? resolve(root, options.tokenFile)
2163
- : join(root, defaultDeployChatAccessTokenPath);
2164
- const hostname = "localhost";
2165
- const deployUrl = state.url;
2166
- const server = createServer(async (request, response) => {
2167
- try {
2168
- const requestUrl = new URL(request.url ?? "/", `http://${request.headers.host ?? `${hostname}:${options.port}`}`);
2169
-
2170
- if (request.method === "GET" && requestUrl.pathname === "/") {
2171
- sendHtml(response, renderDeployChatUi({ deployUrl }));
2172
- return;
2173
- }
2174
-
2175
- if (request.method === "GET" && requestUrl.pathname === "/health") {
2176
- sendJson(response, 200, { status: "ok", target: "deploy-chat-ui", deployUrl });
2177
- return;
2178
- }
2179
-
2180
- if (request.method === "GET" && requestUrl.pathname === "/_agentkit") {
2181
- sendJson(response, 200, {
2182
- deployUrl,
2183
- access: "proxied",
2184
- tokenSource: process.env.AGENTKIT_DEPLOY_ACCESS_TOKEN ? "AGENTKIT_DEPLOY_ACCESS_TOKEN" : relativePath(root, tokenPath),
2185
- });
2186
- return;
2187
- }
2188
-
2189
- if (request.method === "POST" && (requestUrl.pathname === "/v1/chat" || requestUrl.pathname === "/chat")) {
2190
- const token = await readDeployAccessTokenForUi(tokenPath);
2191
- const body = await readIncomingBody(request);
2192
- const contentTypeHeader = request.headers["content-type"];
2193
- const contentType = Array.isArray(contentTypeHeader)
2194
- ? contentTypeHeader[0] ?? "application/json"
2195
- : contentTypeHeader ?? "application/json";
2196
- const upstream = await fetch(new URL("/v1/chat", deployUrl).href, {
2197
- method: "POST",
2198
- headers: {
2199
- "Authorization": `Bearer ${token}`,
2200
- "Content-Type": contentType,
2201
- },
2202
- body,
2203
- });
2204
- const text = await upstream.text();
2205
-
2206
- response.writeHead(upstream.status, {
2207
- "content-type": upstream.headers.get("content-type") ?? "application/json",
2208
- });
2209
- response.end(text);
2210
- return;
2211
- }
2212
-
2213
- sendJson(response, 404, { error: { code: "not_found", message: "Route not found." } });
2214
- } catch (error) {
2215
- sendJson(response, 500, {
2216
- error: {
2217
- code: isAgentKitError(error) ? error.code : "deploy_chat_ui_error",
2218
- message: error instanceof Error ? error.message : String(error),
2219
- },
2220
- });
1068
+ if (state?.agent === capsule.config.name) {
1069
+ return {
1070
+ url: `http://localhost:${port}`,
1071
+ agent: state.agent,
1072
+ };
2221
1073
  }
2222
- });
2223
-
2224
- await new Promise<void>((resolvePromise, reject) => {
2225
- server.once("error", reject);
2226
- server.listen(options.port, hostname, () => {
2227
- server.off("error", reject);
2228
- resolvePromise();
2229
- });
2230
- });
2231
-
2232
- return {
2233
- url: `http://${hostname}:${options.port}`,
2234
- deployUrl,
2235
- tokenSource: process.env.AGENTKIT_DEPLOY_ACCESS_TOKEN ? "AGENTKIT_DEPLOY_ACCESS_TOKEN" : relativePath(root, tokenPath),
2236
- stop: () =>
2237
- new Promise<void>((resolvePromise, reject) => {
2238
- server.close((error) => (error ? reject(error) : resolvePromise()));
2239
- }),
2240
- };
2241
- }
2242
-
2243
- async function readDeployAccessTokenForUi(tokenPath: string): Promise<string> {
2244
- if (process.env.AGENTKIT_DEPLOY_ACCESS_TOKEN) {
2245
- return process.env.AGENTKIT_DEPLOY_ACCESS_TOKEN;
2246
- }
2247
-
2248
- let payload: unknown;
2249
-
2250
- try {
2251
- payload = JSON.parse(await readFile(tokenPath, "utf8"));
2252
- } catch (error) {
2253
- throw new AgentKitError(
2254
- "deploy_access_token_missing",
2255
- `Could not read a clean deploy access token file at ${tokenPath}. Run: agentkit access token create ${defaultDeployChatAccessTokenName} --out ${defaultDeployChatAccessTokenPath}`,
2256
- );
2257
- }
2258
-
2259
- const token =
2260
- payload && typeof payload === "object"
2261
- ? (payload as { access_token?: { token?: unknown }; token?: unknown; accessToken?: unknown }).access_token?.token ??
2262
- (payload as { token?: unknown }).token ??
2263
- (payload as { accessToken?: unknown }).accessToken
2264
- : undefined;
2265
-
2266
- if (typeof token !== "string" || token.trim().length === 0) {
2267
- throw new AgentKitError(
2268
- "deploy_access_token_invalid",
2269
- `Deploy access token file ${tokenPath} does not contain access_token.token. Recreate it with: agentkit access token create ${defaultDeployChatAccessTokenName} --out ${defaultDeployChatAccessTokenPath}`,
2270
- );
2271
1074
  }
2272
1075
 
2273
- return token.trim();
1076
+ return null;
2274
1077
  }
2275
1078
 
2276
- function readIncomingBody(request: IncomingMessage): Promise<string> {
2277
- return new Promise((resolvePromise, reject) => {
2278
- let body = "";
2279
- request.setEncoding("utf8");
2280
- request.on("data", (chunk) => {
2281
- body += chunk;
2282
- });
2283
- request.on("end", () => resolvePromise(body));
2284
- request.on("error", reject);
2285
- });
2286
- }
1079
+ function localDevBasePort(): number {
1080
+ const value = process.env.AGENTKIT_DEV_BASE_PORT;
2287
1081
 
2288
- function sendJson(response: ServerResponse, status: number, body: unknown): void {
2289
- response.writeHead(status, { "content-type": "application/json; charset=utf-8" });
2290
- response.end(JSON.stringify(body, null, 2));
2291
- }
1082
+ if (!value) {
1083
+ return defaultAgentKitDevPort;
1084
+ }
2292
1085
 
2293
- function sendHtml(response: ServerResponse, body: string): void {
2294
- response.writeHead(200, { "content-type": "text/html; charset=utf-8" });
2295
- response.end(body);
1086
+ const parsed = Number(value);
1087
+ return Number.isInteger(parsed) && parsed > 0 && parsed <= 65535 ? parsed : defaultAgentKitDevPort;
2296
1088
  }
2297
1089
 
2298
- function renderDeployChatUi(input: { deployUrl: string }): string {
2299
- const deployUrl = escapeHtml(input.deployUrl);
2300
-
2301
- return `<!doctype html>
2302
- <html lang="en">
2303
- <head>
2304
- <meta charset="utf-8">
2305
- <meta name="viewport" content="width=device-width, initial-scale=1">
2306
- <title>AgentKit Hosted Chat</title>
2307
- <style>
2308
- :root {
2309
- color-scheme: light;
2310
- --bg: #f6f7f5;
2311
- --panel: #ffffff;
2312
- --ink: #182022;
2313
- --muted: #667175;
2314
- --line: #d9ded9;
2315
- --accent: #176b5d;
2316
- --danger: #9f2d2d;
2317
- }
2318
-
2319
- * { box-sizing: border-box; }
2320
-
2321
- body {
2322
- margin: 0;
2323
- min-height: 100svh;
2324
- background: var(--bg);
2325
- color: var(--ink);
2326
- font-family: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
2327
- }
2328
-
2329
- main {
2330
- min-height: 100svh;
2331
- display: grid;
2332
- grid-template-rows: auto 1fr auto;
2333
- background: var(--panel);
2334
- }
1090
+ async function fetchLocalAgentKitState(url: string): Promise<{ agent?: string } | null> {
1091
+ const controller = new AbortController();
1092
+ const timeout = setTimeout(() => controller.abort(), 300);
2335
1093
 
2336
- header {
2337
- border-bottom: 1px solid var(--line);
2338
- padding: 18px 22px;
2339
- }
2340
-
2341
- h1 {
2342
- margin: 0 0 4px;
2343
- font-size: 20px;
2344
- line-height: 1.2;
2345
- }
2346
-
2347
- code {
2348
- color: var(--muted);
2349
- font-size: 12px;
2350
- overflow-wrap: anywhere;
2351
- }
2352
-
2353
- #messages {
2354
- padding: 22px;
2355
- overflow: auto;
2356
- display: flex;
2357
- flex-direction: column;
2358
- gap: 12px;
2359
- }
2360
-
2361
- .message {
2362
- max-width: 780px;
2363
- display: grid;
2364
- gap: 4px;
2365
- }
2366
-
2367
- .message.user {
2368
- align-self: end;
2369
- }
2370
-
2371
- .role {
2372
- color: var(--muted);
2373
- font-size: 12px;
2374
- font-weight: 700;
2375
- text-transform: uppercase;
2376
- }
2377
-
2378
- .bubble {
2379
- border: 1px solid var(--line);
2380
- border-radius: 8px;
2381
- padding: 12px 14px;
2382
- white-space: pre-wrap;
2383
- line-height: 1.45;
2384
- background: #fbfbfa;
2385
- }
2386
-
2387
- .user .bubble {
2388
- border-color: #b8d8ce;
2389
- background: #e8f5f0;
2390
- }
2391
-
2392
- .error .bubble {
2393
- border-color: #e1b1b1;
2394
- color: var(--danger);
2395
- background: #fff6f6;
2396
- }
2397
-
2398
- form {
2399
- border-top: 1px solid var(--line);
2400
- padding: 14px;
2401
- display: grid;
2402
- grid-template-columns: 1fr auto;
2403
- gap: 10px;
2404
- }
2405
-
2406
- textarea {
2407
- min-height: 52px;
2408
- max-height: 180px;
2409
- resize: vertical;
2410
- border: 1px solid var(--line);
2411
- border-radius: 8px;
2412
- padding: 12px;
2413
- color: var(--ink);
2414
- font: inherit;
2415
- }
2416
-
2417
- button {
2418
- min-width: 96px;
2419
- border: 0;
2420
- border-radius: 8px;
2421
- padding: 0 18px;
2422
- color: white;
2423
- background: var(--accent);
2424
- font: inherit;
2425
- font-weight: 700;
2426
- cursor: pointer;
2427
- }
2428
-
2429
- button:disabled {
2430
- cursor: wait;
2431
- opacity: 0.65;
2432
- }
2433
-
2434
- @media (max-width: 640px) {
2435
- form {
2436
- grid-template-columns: 1fr;
2437
- }
2438
-
2439
- button {
2440
- min-height: 44px;
2441
- }
2442
- }
2443
- </style>
2444
- </head>
2445
- <body>
2446
- <main>
2447
- <header>
2448
- <h1>AgentKit Hosted Chat</h1>
2449
- <code>${deployUrl}</code>
2450
- </header>
2451
- <section id="messages" aria-live="polite"></section>
2452
- <form id="form">
2453
- <textarea id="message" name="message" placeholder="Type a message" required autofocus></textarea>
2454
- <button id="send" type="submit">Send</button>
2455
- </form>
2456
- </main>
2457
- <script>
2458
- const form = document.querySelector("#form");
2459
- const textarea = document.querySelector("#message");
2460
- const send = document.querySelector("#send");
2461
- const messages = document.querySelector("#messages");
2462
- let conversationId = null;
2463
-
2464
- function addMessage(role, content, className = "") {
2465
- const wrapper = document.createElement("article");
2466
- wrapper.className = ["message", role, className].filter(Boolean).join(" ");
2467
- const label = document.createElement("div");
2468
- label.className = "role";
2469
- label.textContent = role;
2470
- const bubble = document.createElement("div");
2471
- bubble.className = "bubble";
2472
- bubble.textContent = content;
2473
- wrapper.append(label, bubble);
2474
- messages.append(wrapper);
2475
- messages.scrollTop = messages.scrollHeight;
2476
- }
1094
+ try {
1095
+ const response = await fetch(url, { signal: controller.signal });
1096
+ const payload = await response.json().catch(() => null);
2477
1097
 
2478
- form.addEventListener("submit", async (event) => {
2479
- event.preventDefault();
2480
- const message = textarea.value.trim();
2481
- if (!message) return;
2482
-
2483
- addMessage("user", message);
2484
- textarea.value = "";
2485
- send.disabled = true;
2486
-
2487
- try {
2488
- const response = await fetch("/v1/chat", {
2489
- method: "POST",
2490
- headers: { "content-type": "application/json" },
2491
- body: JSON.stringify({ message, conversationId }),
2492
- });
2493
- const payload = await response.json();
2494
-
2495
- if (!response.ok) {
2496
- throw new Error(payload.error?.message || "Request failed");
2497
- }
2498
-
2499
- conversationId = payload.conversationId || conversationId;
2500
- addMessage("assistant", payload.message?.content || "");
2501
- } catch (error) {
2502
- addMessage("error", error instanceof Error ? error.message : String(error), "error");
2503
- } finally {
2504
- send.disabled = false;
2505
- textarea.focus();
2506
- }
2507
- });
2508
- </script>
2509
- </body>
2510
- </html>`;
2511
- }
1098
+ if (payload && typeof payload === "object") {
1099
+ return {
1100
+ agent: typeof (payload as { agent?: unknown }).agent === "string" ? (payload as { agent: string }).agent : undefined,
1101
+ };
1102
+ }
1103
+ } catch {
1104
+ return null;
1105
+ } finally {
1106
+ clearTimeout(timeout);
1107
+ }
2512
1108
 
2513
- function escapeHtml(value: string): string {
2514
- return value
2515
- .replace(/&/g, "&amp;")
2516
- .replace(/</g, "&lt;")
2517
- .replace(/>/g, "&gt;")
2518
- .replace(/"/g, "&quot;")
2519
- .replace(/'/g, "&#39;");
1109
+ return null;
2520
1110
  }
2521
1111
 
2522
- async function waitForShutdown(stop: () => Promise<void>): Promise<void> {
2523
- await new Promise<void>((resolve) => {
2524
- let stopped = false;
2525
-
2526
- const shutdown = () => {
2527
- if (stopped) {
2528
- return;
2529
- }
1112
+ async function openLocalUrl(url: string): Promise<void> {
1113
+ if (process.env.AGENTKIT_OPEN_COMMAND === "none") {
1114
+ return;
1115
+ }
2530
1116
 
2531
- stopped = true;
2532
- void stop().finally(resolve);
2533
- };
1117
+ const command =
1118
+ process.env.AGENTKIT_OPEN_COMMAND ??
1119
+ (process.platform === "darwin" ? "open" : process.platform === "win32" ? "cmd" : "xdg-open");
1120
+ const args = process.env.AGENTKIT_OPEN_COMMAND
1121
+ ? [url]
1122
+ : process.platform === "win32"
1123
+ ? ["/c", "start", "", url]
1124
+ : [url];
2534
1125
 
2535
- process.once("SIGINT", shutdown);
2536
- process.once("SIGTERM", shutdown);
1126
+ await new Promise<void>((resolvePromise, reject) => {
1127
+ const child = spawn(command, args, {
1128
+ detached: true,
1129
+ stdio: "ignore",
1130
+ });
1131
+ child.once("error", reject);
1132
+ child.once("spawn", () => {
1133
+ child.unref();
1134
+ resolvePromise();
1135
+ });
2537
1136
  });
2538
1137
  }
2539
1138