@andreprado/agentkit 0.1.0-alpha.2 → 0.1.0-alpha.3

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,7 +1,8 @@
1
1
  import { spawn } from "node:child_process";
2
2
  import { mkdir, readFile, rm, writeFile } from "node:fs/promises";
3
+ import { createServer, type IncomingMessage, type ServerResponse } from "node:http";
3
4
  import { homedir } from "node:os";
4
- import { join, resolve } from "node:path";
5
+ import { dirname, join, relative, resolve } from "node:path";
5
6
 
6
7
  import { createProject } from "../create-project";
7
8
  import { buildAgentCapsule } from "../runtime/build";
@@ -19,7 +20,7 @@ import {
19
20
  } from "../runtime/db-commands";
20
21
  import { runEvalsFromCwd } from "../runtime/evals";
21
22
  import { findAgentKitDocs } from "../runtime/docs";
22
- import { listCapsuleEnv, setCapsuleEnv, unsetCapsuleEnv } from "../runtime/env";
23
+ import { listCapsuleEnv, loadCapsuleEnv, setCapsuleEnv, unsetCapsuleEnv } from "../runtime/env";
23
24
  import { loadDeployReadinessContext, stableCloudProjectId, type DeployReadinessContext } from "../runtime/deploy-readiness";
24
25
  import { runToolFromCwd } from "../runtime/tool-runner";
25
26
  import { startAgentDevServer } from "../runtime/dev-server";
@@ -28,13 +29,59 @@ import { AgentKitError, isAgentKitError } from "../runtime/errors";
28
29
  import { prepareVpsDeploy } from "../runtime/targets/vps/deploy";
29
30
 
30
31
  const defaultAgentKitCloudApiUrl = "https://agentkit-cloud.aibuilders.com.br";
32
+ const defaultDeployChatAccessTokenName = "agentkit-chat-ui";
33
+ const defaultDeployChatAccessTokenPath = ".agentkit/chat-access-token.json";
31
34
 
32
35
  function renderHelp(): string {
33
36
  return `AgentKit
34
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
+
35
81
  Usage:
36
- agentkit new <name> [--template blank|support]
82
+ agentkit new <name> [--template blank|support] [--no-install]
37
83
  agentkit dev
84
+ agentkit chat-ui --deploy [--port <number>] [--token-file <path>]
38
85
  agentkit chat --message <text>
39
86
  agentkit tool <name> [--input <path-or-json>]
40
87
  agentkit db migrate
@@ -61,23 +108,30 @@ Usage:
61
108
  agentkit deploy pause [--api <url>]
62
109
  agentkit deploy resume [--api <url>]
63
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>]
64
114
  agentkit secret list [--api <url>]
65
115
  agentkit secret unset <NAME> [--api <url>]
66
- agentkit access token create <name> [--api <url>]
116
+ agentkit access token create <name> [--api <url>] [--out <path>]
67
117
  agentkit access token list [--api <url>]
68
118
  agentkit access token revoke <token-id> [--api <url>]
69
119
  agentkit env set <NAME> <VALUE>
120
+ agentkit env set <NAME> --stdin
121
+ agentkit env set <NAME> --from-env [ENV_NAME]
70
122
  agentkit env list
71
123
  agentkit env unset <NAME>
72
124
  agentkit docs path
73
125
  agentkit docs llms
74
126
  agentkit docs full
75
127
  agentkit handoff codex [goal]
128
+ agentkit help commands
76
129
  agentkit --help
77
130
 
78
131
  Commands:
79
132
  new Create a new Agent Capsule
80
133
  dev Run the Agent Capsule locally
134
+ chat-ui Run a local chat UI, optionally pointed at a hosted deploy
81
135
  chat Send one message to the local Agent Capsule
82
136
  tool Run one registered tool directly
83
137
  db Manage the local SQLite database
@@ -94,6 +148,7 @@ Commands:
94
148
  env Manage local .env values without opening code
95
149
  docs Print AgentKit docs locations
96
150
  handoff Print a ready-to-paste prompt for another coding agent
151
+ help Print focused help or the full command reference
97
152
 
98
153
  Examples:
99
154
  agentkit new support-agent --template support
@@ -145,34 +200,60 @@ async function main() {
145
200
  return;
146
201
  }
147
202
 
203
+ if (args.command === "help") {
204
+ const [topic] = args.positional;
205
+
206
+ if (!topic) {
207
+ console.log(renderHelp());
208
+ return;
209
+ }
210
+
211
+ if (topic === "commands") {
212
+ console.log(renderCommandReference());
213
+ return;
214
+ }
215
+
216
+ throw new Error("Usage: agentkit help [commands]");
217
+ }
218
+
148
219
  if (args.command === "new") {
149
220
  const [name] = args.positional;
150
221
  const template = String(args.flags.template ?? "blank");
222
+ const shouldInstall = args.flags["no-install"] !== true;
151
223
 
152
224
  if (!name) {
153
- throw new Error("Missing project name. Usage: agentkit new <name> [--template blank|support]");
225
+ throw new Error("Missing project name. Usage: agentkit new <name> [--template blank|support] [--no-install]");
154
226
  }
155
227
 
156
228
  const result = await createProject({ name, template, cwd: process.cwd() });
157
229
  console.log(`Created ${result.name} at ${result.path}`);
230
+
231
+ if (shouldInstall) {
232
+ console.log("");
233
+ console.log("Installing dependencies...");
234
+ await installProjectDependencies(result.path);
235
+ }
236
+
158
237
  console.log("");
159
238
  console.log("Next:");
160
239
  if (resolve(result.path) !== resolve(process.cwd())) {
161
240
  console.log(` cd ${result.name}`);
162
241
  }
163
- console.log(" npm install");
242
+ if (!shouldInstall) {
243
+ console.log(" npm install");
244
+ }
164
245
  console.log(" open this folder in Codex or Claude Code");
165
246
  console.log(' ask: "Develop an appointment and intake agent for an ophthalmology office"');
166
247
  console.log(' npm run chat -- --message "hello"');
167
248
  console.log(" npm run dev");
168
249
  console.log(" npm run agentkit -- deploy");
169
250
  console.log("");
170
- console.log("Optional: manage local secrets with `npm run agentkit -- env set <NAME> <VALUE>`.");
251
+ console.log("Optional: manage local secrets with `printf %s \"$VALUE\" | npm run agentkit -- env set <NAME> --stdin`.");
171
252
  return;
172
253
  }
173
254
 
174
255
  if (args.command === "dev") {
175
- const port = parsePort(args.flags.port);
256
+ const port = parsePort(args.flags.port, "agentkit dev --port <number>");
176
257
  const devServer = await startAgentDevServer(process.cwd(), { port });
177
258
 
178
259
  console.log("Agent Capsule running");
@@ -185,6 +266,28 @@ async function main() {
185
266
  return;
186
267
  }
187
268
 
269
+ if (args.command === "chat-ui") {
270
+ if (!args.flags.deploy) {
271
+ throw new Error("Usage: agentkit chat-ui --deploy [--port <number>] [--token-file <path>]");
272
+ }
273
+
274
+ const port = parsePort(args.flags.port, "agentkit chat-ui --deploy --port <number>") ?? 4124;
275
+ const tokenFile = parseOptionalStringFlag(
276
+ args.flags["token-file"],
277
+ "token-file",
278
+ "agentkit chat-ui --deploy --token-file <path>",
279
+ );
280
+ const server = await startDeployChatUiServer(process.cwd(), { port, tokenFile });
281
+
282
+ console.log("AgentKit hosted chat UI running");
283
+ console.log("");
284
+ console.log(`Chat: ${server.url}`);
285
+ console.log(`Production: ${server.deployUrl}`);
286
+ console.log(`Token: ${server.tokenSource}`);
287
+ await waitForShutdown(server.stop);
288
+ return;
289
+ }
290
+
188
291
  if (args.command === "chat") {
189
292
  const message = args.flags.message;
190
293
 
@@ -351,8 +454,25 @@ async function main() {
351
454
 
352
455
  if (args.command === "login") {
353
456
  const token = parseRequiredStringFlag(args.flags.token, "token", "agentkit login --token <token> [--api <url>]");
354
- await writeCloudAuth({ apiUrl: parseCloudApiUrl(args.flags.api), token });
355
- console.log("AgentKit Cloud token stored.");
457
+ const apiUrl = parseCloudApiUrl(args.flags.api);
458
+ const verified = await verifyCloudLogin(apiUrl, token);
459
+ await writeCloudAuth({ apiUrl, token });
460
+ const email = verified.me.account?.email;
461
+ console.log(email ? `Logged in to AgentKit Cloud as ${email}.` : "AgentKit Cloud token verified and stored.");
462
+
463
+ const hasCloudflareAlpha = (verified.capabilities.capabilities ?? []).some(
464
+ (capability) => capability.name === "cloudflare_deploy_alpha" && capability.status === "active",
465
+ );
466
+ console.log(
467
+ hasCloudflareAlpha
468
+ ? "Entitlement: cloudflare_deploy_alpha active."
469
+ : "Warning: cloudflare_deploy_alpha entitlement was not found for this account.",
470
+ );
471
+
472
+ if (process.env.AGENTKIT_CLOUD_API_TOKEN && process.env.AGENTKIT_CLOUD_API_TOKEN !== token) {
473
+ console.log("Warning: AGENTKIT_CLOUD_API_TOKEN is set and will override this saved login in the current shell.");
474
+ }
475
+
356
476
  return;
357
477
  }
358
478
 
@@ -450,6 +570,12 @@ async function main() {
450
570
  anonymous: anonymous || !auth?.token,
451
571
  });
452
572
  const statePath = await writeLocalDeployState(result.root, deploy);
573
+ const chatAccess = await ensureDeployChatAccessToken({
574
+ apiUrl,
575
+ root: result.root,
576
+ deployId: deploy.deploy.id,
577
+ skip: anonymous || !auth?.token || deploy.deploy.access.mode === "public",
578
+ });
453
579
 
454
580
  console.log("");
455
581
  console.log("Deploy ready");
@@ -457,6 +583,15 @@ async function main() {
457
583
  console.log(`Project: ${deploy.project.id}`);
458
584
  console.log(`Deploy: ${deploy.deploy.id}`);
459
585
  console.log(`State: ${statePath}`);
586
+
587
+ if (chatAccess?.status === "ready") {
588
+ console.log(`Chat auth: ${relativePath(process.cwd(), chatAccess.path)}`);
589
+ } else if (chatAccess?.status === "skipped") {
590
+ console.log("Chat auth: public or anonymous deploy; no local chat token was created.");
591
+ } else if (chatAccess?.status === "failed") {
592
+ console.log(`Chat auth: could not create local chat token. ${formatReadinessError(chatAccess.error)}`);
593
+ console.log(`Run: agentkit access token create ${defaultDeployChatAccessTokenName} --out ${defaultDeployChatAccessTokenPath}`);
594
+ }
460
595
  return;
461
596
  }
462
597
 
@@ -474,10 +609,18 @@ async function main() {
474
609
  }
475
610
 
476
611
  if (subcommand === "set") {
477
- const value = valueParts.join(" ");
612
+ const value = await resolveSecretInputValue({
613
+ name,
614
+ valueParts,
615
+ stdinFlag: args.flags.stdin,
616
+ fromEnvFlag: args.flags["from-env"],
617
+ fromLocalEnvFlag: args.flags["from-local-env"],
618
+ localEnvRoot: args.flags["from-local-env"] !== undefined ? await findAgentCapsuleRoot(process.cwd()) : undefined,
619
+ 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>]",
620
+ });
478
621
 
479
- if (!name || value.length === 0) {
480
- throw new Error("Usage: agentkit secret set <NAME> <VALUE> [--api <url>]");
622
+ if (!name) {
623
+ 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 [--api <url>]");
481
624
  }
482
625
 
483
626
  const payload = await cloudApiRequest(apiUrl, `/v1/projects/${encodeURIComponent(projectId)}/secrets/${encodeURIComponent(name)}`, {
@@ -500,14 +643,14 @@ async function main() {
500
643
  return;
501
644
  }
502
645
 
503
- throw new Error("Usage: agentkit secret set <NAME> <VALUE> | agentkit secret list | agentkit secret unset <NAME>");
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>");
504
647
  }
505
648
 
506
649
  if (args.command === "access") {
507
650
  const [subcommand, noun, name] = args.positional;
508
651
 
509
652
  if (subcommand !== "token") {
510
- throw new Error("Usage: agentkit access token create <name> | list | revoke <token-id> [--api <url>]");
653
+ throw new Error("Usage: agentkit access token create <name> [--out <path>] | list | revoke <token-id> [--api <url>]");
511
654
  }
512
655
 
513
656
  const state = await requireLocalDeployState();
@@ -518,7 +661,7 @@ async function main() {
518
661
  method: "POST",
519
662
  body: JSON.stringify({ name }),
520
663
  });
521
- printJson(payload);
664
+ await printJsonOrWrite(payload, args.flags.out, "agentkit access token create <name> [--api <url>] [--out <path>]");
522
665
  return;
523
666
  }
524
667
 
@@ -540,7 +683,7 @@ async function main() {
540
683
  return;
541
684
  }
542
685
 
543
- throw new Error("Usage: agentkit access token create <name> | list | revoke <token-id> [--api <url>]");
686
+ throw new Error("Usage: agentkit access token create <name> [--out <path>] | list | revoke <token-id> [--api <url>]");
544
687
  }
545
688
 
546
689
  if (args.command === "channels") {
@@ -568,10 +711,17 @@ async function main() {
568
711
  }
569
712
 
570
713
  if (subcommand === "set") {
571
- const value = valueParts.join(" ");
714
+ const value = await resolveSecretInputValue({
715
+ name,
716
+ valueParts,
717
+ stdinFlag: args.flags.stdin,
718
+ fromEnvFlag: args.flags["from-env"],
719
+ fromLocalEnvFlag: undefined,
720
+ usage: "agentkit env set <NAME> <VALUE> | agentkit env set <NAME> --stdin | agentkit env set <NAME> --from-env [ENV_NAME]",
721
+ });
572
722
 
573
- if (!name || value.length === 0) {
574
- throw new Error("Usage: agentkit env set <NAME> <VALUE>");
723
+ if (!name) {
724
+ throw new Error("Usage: agentkit env set <NAME> <VALUE> | agentkit env set <NAME> --stdin | agentkit env set <NAME> --from-env [ENV_NAME]");
575
725
  }
576
726
 
577
727
  await setCapsuleEnv(root, name, value);
@@ -589,7 +739,7 @@ async function main() {
589
739
  return;
590
740
  }
591
741
 
592
- throw new Error("Usage: agentkit env list | agentkit env set <NAME> <VALUE> | agentkit env unset <NAME>");
742
+ throw new Error("Usage: agentkit env list | agentkit env set <NAME> <VALUE> | agentkit env set <NAME> --stdin | agentkit env set <NAME> --from-env [ENV_NAME] | agentkit env unset <NAME>");
593
743
  }
594
744
 
595
745
  if (args.command === "docs") {
@@ -666,7 +816,6 @@ Verification:
666
816
  Run these before you finish:
667
817
 
668
818
  \`\`\`sh
669
- npm install
670
819
  npm run typecheck
671
820
  npm run agentkit -- inspect
672
821
  npm run chat -- --message "hello"
@@ -716,6 +865,36 @@ type CloudCapabilitiesResponse = {
716
865
  }>;
717
866
  };
718
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
+
719
898
  type CloudSecretsResponse = {
720
899
  secrets?: Array<{
721
900
  name?: string;
@@ -771,7 +950,7 @@ async function checkDeployReadiness(options: {
771
950
  checks.push({
772
951
  status: "fail",
773
952
  title: "Login",
774
- message: `AgentKit Cloud rejected the stored token. ${formatReadinessError(error)}`,
953
+ message: `AgentKit Cloud rejected the token from ${formatCloudAuthSource(auth)}. ${formatReadinessError(error)}`,
775
954
  action: "agentkit login --token <token>",
776
955
  });
777
956
  }
@@ -802,6 +981,58 @@ async function checkDeployReadiness(options: {
802
981
  message: `Could not verify AgentKit Cloud capabilities. ${formatReadinessError(error)}`,
803
982
  });
804
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
+ }
805
1036
  }
806
1037
 
807
1038
  if (context.agentKitManagedSecrets.length > 0) {
@@ -901,14 +1132,18 @@ function printDeployReadinessReport(report: DeployReadinessReport): void {
901
1132
 
902
1133
  function secretSetCommand(secret: string, source: DeployReadinessContext["localSecrets"][string] | undefined): string {
903
1134
  if (source?.dotenv) {
904
- return `agentkit secret set ${secret} <value-from-.env>`;
1135
+ return `agentkit secret set ${secret} --from-local-env`;
905
1136
  }
906
1137
 
907
1138
  if (source?.environment) {
908
- return `agentkit secret set ${secret} "$${secret}"`;
1139
+ return `agentkit secret set ${secret} --from-env`;
909
1140
  }
910
1141
 
911
- return `agentkit secret set ${secret} <value>`;
1142
+ return `agentkit secret set ${secret} --stdin`;
1143
+ }
1144
+
1145
+ function normalizeOptionalNumber(value: number | null | undefined): number | undefined {
1146
+ return typeof value === "number" && Number.isFinite(value) ? value : undefined;
912
1147
  }
913
1148
 
914
1149
  function formatReadinessError(error: unknown): string {
@@ -1379,6 +1614,7 @@ function cloudFetch(apiUrl: string, path: string, init: RequestInit = {}): Promi
1379
1614
  type CloudAuthConfig = {
1380
1615
  apiUrl: string;
1381
1616
  token: string;
1617
+ source?: "environment" | "file";
1382
1618
  };
1383
1619
 
1384
1620
  function cloudAuthPath(): string {
@@ -1390,6 +1626,7 @@ async function readCloudAuth(): Promise<CloudAuthConfig | null> {
1390
1626
  return {
1391
1627
  apiUrl: process.env.AGENTKIT_CLOUD_API_URL ?? defaultAgentKitCloudApiUrl,
1392
1628
  token: process.env.AGENTKIT_CLOUD_API_TOKEN,
1629
+ source: "environment",
1393
1630
  };
1394
1631
  }
1395
1632
 
@@ -1400,6 +1637,7 @@ async function readCloudAuth(): Promise<CloudAuthConfig | null> {
1400
1637
  return {
1401
1638
  apiUrl: typeof parsed.apiUrl === "string" ? parsed.apiUrl : defaultAgentKitCloudApiUrl,
1402
1639
  token: parsed.token,
1640
+ source: "file",
1403
1641
  };
1404
1642
  }
1405
1643
  } catch {
@@ -1414,6 +1652,87 @@ async function writeCloudAuth(config: CloudAuthConfig): Promise<void> {
1414
1652
  await writeFile(cloudAuthPath(), `${JSON.stringify(config, null, 2)}\n`, { mode: 0o600 });
1415
1653
  }
1416
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
+
1417
1736
  async function clearCloudAuth(): Promise<void> {
1418
1737
  await rm(cloudAuthPath(), { force: true });
1419
1738
  }
@@ -1472,16 +1791,66 @@ async function cloudApiRequest(apiUrl: string, path: string, init: RequestInit):
1472
1791
  payload && typeof payload === "object" && "error" in payload
1473
1792
  ? String((payload as { error?: { code?: unknown } }).error?.code ?? "cloud_request_failed")
1474
1793
  : "cloud_request_failed";
1475
- throw new AgentKitError(code, readCloudError(payload, response.status));
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);
1476
1801
  }
1477
1802
 
1478
1803
  return payload;
1479
1804
  }
1480
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
+
1481
1837
  function printJson(value: unknown): void {
1482
1838
  console.log(JSON.stringify(value, null, 2));
1483
1839
  }
1484
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
+
1485
1854
  function readCloudError(payload: unknown, status: number): string {
1486
1855
  if (payload && typeof payload === "object" && "error" in payload) {
1487
1856
  const error = (payload as { error?: { message?: unknown } }).error;
@@ -1493,19 +1862,36 @@ function readCloudError(payload: unknown, status: number): string {
1493
1862
  return `AgentKit Cloud request failed with HTTP ${status}.`;
1494
1863
  }
1495
1864
 
1496
- function parsePort(value: string | boolean | undefined): number | undefined {
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 {
1497
1883
  if (value === undefined) {
1498
1884
  return undefined;
1499
1885
  }
1500
1886
 
1501
1887
  if (typeof value !== "string") {
1502
- throw new Error("Missing port. Usage: agentkit dev --port <number>");
1888
+ throw new Error(`Missing port. Usage: ${usage}`);
1503
1889
  }
1504
1890
 
1505
1891
  const port = Number(value);
1506
1892
 
1507
1893
  if (!Number.isInteger(port) || port <= 0 || port > 65535) {
1508
- throw new Error("Invalid port. Usage: agentkit dev --port <number>");
1894
+ throw new Error(`Invalid port. Usage: ${usage}`);
1509
1895
  }
1510
1896
 
1511
1897
  return port;
@@ -1557,6 +1943,90 @@ function parseOptionalStringFlag(value: string | boolean | undefined, name: stri
1557
1943
  return value.trim();
1558
1944
  }
1559
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
+
1560
2030
  function printBuildResult(result: Awaited<ReturnType<typeof buildAgentCapsule>>): void {
1561
2031
  console.log("AgentKit build complete");
1562
2032
  console.log("");
@@ -1614,6 +2084,14 @@ async function deployCloudflare(wranglerPath: string, buildDir: string): Promise
1614
2084
  }
1615
2085
  }
1616
2086
 
2087
+ async function installProjectDependencies(projectPath: string): Promise<void> {
2088
+ const exitCode = await runInheritedCommand("npm", ["install"], projectPath);
2089
+
2090
+ if (exitCode !== 0) {
2091
+ throw new Error(`Dependency install failed with exit code ${exitCode}. Run \`npm install\` in ${projectPath}.`);
2092
+ }
2093
+ }
2094
+
1617
2095
  function runInheritedCommand(command: string, args: string[], cwd: string): Promise<number> {
1618
2096
  return new Promise((resolvePromise, reject) => {
1619
2097
  const child = spawn(command, args, {
@@ -1669,6 +2147,378 @@ function parseJsonInput(source: string, label: string): unknown {
1669
2147
  }
1670
2148
  }
1671
2149
 
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);
2156
+
2157
+ if (!state?.url) {
2158
+ throw new Error("No hosted deploy URL found. Run agentkit deploy first.");
2159
+ }
2160
+
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
+ });
2221
+ }
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
+ }
2272
+
2273
+ return token.trim();
2274
+ }
2275
+
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
+ }
2287
+
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
+ }
2292
+
2293
+ function sendHtml(response: ServerResponse, body: string): void {
2294
+ response.writeHead(200, { "content-type": "text/html; charset=utf-8" });
2295
+ response.end(body);
2296
+ }
2297
+
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
+ }
2335
+
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
+ }
2477
+
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
+ }
2512
+
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;");
2520
+ }
2521
+
1672
2522
  async function waitForShutdown(stop: () => Promise<void>): Promise<void> {
1673
2523
  await new Promise<void>((resolve) => {
1674
2524
  let stopped = false;