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

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,14 +29,64 @@ 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 defaultAgentKitDevPort = 4123;
33
+ const defaultDeployChatAccessTokenName = "agentkit-chat-ui";
34
+ const defaultDeployChatAccessTokenPath = ".agentkit/chat-access-token.json";
31
35
 
32
36
  function renderHelp(): string {
33
37
  return `AgentKit
34
38
 
39
+ Create, run, inspect, and deploy Agent Capsules.
40
+
41
+ Quick start:
42
+ agentkit new support-agent --template support
43
+ cd support-agent
44
+ npm run dev
45
+ npm run chat -- --message "hello"
46
+
35
47
  Usage:
36
- agentkit new <name> [--template blank|support]
48
+ agentkit <command> [options]
49
+
50
+ Start:
51
+ agentkit new <name> [--template blank|support|dentista] [--no-install]
37
52
  agentkit dev
38
- agentkit chat --message <text>
53
+ agentkit open
54
+ agentkit chat-ui --deploy
55
+ agentkit chat --message <text> [--conversation-id <id>]
56
+ agentkit inspect
57
+
58
+ Build:
59
+ agentkit tool <name> [--input <path-or-json>]
60
+ agentkit db migrate
61
+ agentkit eval run
62
+ agentkit conversations list
63
+
64
+ Deploy:
65
+ agentkit login --token <token>
66
+ agentkit deploy doctor
67
+ agentkit secret set <NAME> --stdin
68
+ agentkit secret sync --from-local
69
+ agentkit deploy --smoke "hello"
70
+ agentkit deploy status
71
+
72
+ More:
73
+ agentkit channels ...
74
+ agentkit access token ...
75
+ agentkit env ...
76
+ agentkit docs full
77
+ agentkit help commands
78
+ `;
79
+ }
80
+
81
+ function renderCommandReference(): string {
82
+ return `AgentKit Command Reference
83
+
84
+ Usage:
85
+ agentkit new <name> [--template blank|support|dentista] [--no-install]
86
+ agentkit dev [--port <number>]
87
+ agentkit open
88
+ agentkit chat-ui --deploy [--port <number>] [--token-file <path>]
89
+ agentkit chat --message <text> [--conversation-id <id>]
39
90
  agentkit tool <name> [--input <path-or-json>]
40
91
  agentkit db migrate
41
92
  agentkit db reset --yes
@@ -52,32 +103,42 @@ Usage:
52
103
  agentkit channels deliveries list <name> [--api <url>]
53
104
  agentkit channels deliveries show <delivery-id> [--api <url>]
54
105
  agentkit inspect
55
- agentkit build [--target cloudflare]
106
+ agentkit build [--target cloudflare|container]
56
107
  agentkit login --token <token> [--api <url>]
57
108
  agentkit logout
58
- agentkit deploy [--target cloudflare|vps] [--host <host>] [--api <url>] [--dry-run] [--anonymous] [--local-wrangler]
109
+ agentkit deploy [--target cloudflare|vps] [--host <host>] [--api <url>] [--dry-run] [--anonymous] [--local-wrangler] [--smoke <message>]
59
110
  agentkit deploy doctor [--api <url>] [--anonymous]
111
+ agentkit deploy smoke [--message <text>] [--api <url>]
60
112
  agentkit deploy status [--api <url>]
61
113
  agentkit deploy pause [--api <url>]
62
114
  agentkit deploy resume [--api <url>]
63
115
  agentkit secret set <NAME> <VALUE> [--api <url>]
116
+ agentkit secret set <NAME> --stdin [--api <url>]
117
+ agentkit secret set <NAME> --from-env [ENV_NAME] [--api <url>]
118
+ agentkit secret set <NAME> --from-local-env [--api <url>]
119
+ agentkit secret sync --from-local [--api <url>]
64
120
  agentkit secret list [--api <url>]
65
121
  agentkit secret unset <NAME> [--api <url>]
66
- agentkit access token create <name> [--api <url>]
122
+ agentkit access token create <name> [--api <url>] [--out <path>]
67
123
  agentkit access token list [--api <url>]
68
124
  agentkit access token revoke <token-id> [--api <url>]
69
125
  agentkit env set <NAME> <VALUE>
126
+ agentkit env set <NAME> --stdin
127
+ agentkit env set <NAME> --from-env [ENV_NAME]
70
128
  agentkit env list
71
129
  agentkit env unset <NAME>
72
130
  agentkit docs path
73
131
  agentkit docs llms
74
132
  agentkit docs full
75
133
  agentkit handoff codex [goal]
134
+ agentkit help commands
76
135
  agentkit --help
77
136
 
78
137
  Commands:
79
138
  new Create a new Agent Capsule
80
139
  dev Run the Agent Capsule locally
140
+ open Open the local Agent Capsule UI if it is already running
141
+ chat-ui Run a local chat UI, optionally pointed at a hosted deploy
81
142
  chat Send one message to the local Agent Capsule
82
143
  tool Run one registered tool directly
83
144
  db Manage the local SQLite database
@@ -94,13 +155,17 @@ Commands:
94
155
  env Manage local .env values without opening code
95
156
  docs Print AgentKit docs locations
96
157
  handoff Print a ready-to-paste prompt for another coding agent
158
+ help Print focused help or the full command reference
97
159
 
98
160
  Examples:
99
161
  agentkit new support-agent --template support
100
162
  cd support-agent
101
163
  agentkit dev
164
+ agentkit open
102
165
  agentkit tool lookup_order --input '{"orderId":"A100"}'
103
166
  agentkit eval run
167
+
168
+ agentkit new clara-dentista --template dentista
104
169
  `;
105
170
  }
106
171
 
@@ -137,6 +202,33 @@ function parseArgs(argv: string[]): ParsedArgs {
137
202
  return { command, positional, flags };
138
203
  }
139
204
 
205
+ function isHelpRequested(args: ParsedArgs): boolean {
206
+ return args.flags.help === true || args.flags.h === true || args.positional.includes("--help") || args.positional.includes("-h");
207
+ }
208
+
209
+ function renderCommandHelp(command: string): string {
210
+ const usage: Record<string, string> = {
211
+ new: "agentkit new <name> [--template blank|support|dentista] [--no-install]",
212
+ dev: "agentkit dev [--port <number>]",
213
+ open: "agentkit open",
214
+ "chat-ui": "agentkit chat-ui --deploy [--port <number>] [--token-file <path>]",
215
+ chat: 'agentkit chat --message "hello" [--conversation-id <id>]',
216
+ tool: "agentkit tool <name> [--input <path-or-json>]",
217
+ deploy:
218
+ 'agentkit deploy [--smoke "hello"] | agentkit deploy smoke [--message "hello"] | agentkit deploy doctor | status | pause | resume',
219
+ secret:
220
+ "agentkit secret set <NAME> --stdin | --from-env [ENV_NAME] | --from-local-env | agentkit secret sync --from-local | list | unset <NAME>",
221
+ access: "agentkit access token create <name> [--out <path>] | list | revoke <token-id>",
222
+ env: "agentkit env set <NAME> --stdin | --from-env [ENV_NAME] | list | unset <NAME>",
223
+ docs: "agentkit docs path | llms | full",
224
+ handoff: "agentkit handoff codex [goal] | agentkit handoff claude [goal]",
225
+ };
226
+
227
+ return `Usage: ${usage[command] ?? "agentkit <command> [options]"}
228
+
229
+ Run \`agentkit help commands\` for the full command reference.`;
230
+ }
231
+
140
232
  async function main() {
141
233
  const args = parseArgs(process.argv.slice(2));
142
234
 
@@ -145,38 +237,76 @@ async function main() {
145
237
  return;
146
238
  }
147
239
 
240
+ if (isHelpRequested(args)) {
241
+ console.log(renderCommandHelp(args.command));
242
+ return;
243
+ }
244
+
245
+ if (args.command === "help") {
246
+ const [topic] = args.positional;
247
+
248
+ if (!topic) {
249
+ console.log(renderHelp());
250
+ return;
251
+ }
252
+
253
+ if (topic === "commands") {
254
+ console.log(renderCommandReference());
255
+ return;
256
+ }
257
+
258
+ throw new Error("Usage: agentkit help [commands]");
259
+ }
260
+
148
261
  if (args.command === "new") {
149
262
  const [name] = args.positional;
150
263
  const template = String(args.flags.template ?? "blank");
264
+ const shouldInstall = args.flags["no-install"] !== true;
151
265
 
152
266
  if (!name) {
153
- throw new Error("Missing project name. Usage: agentkit new <name> [--template blank|support]");
267
+ throw new Error("Missing project name. Usage: agentkit new <name> [--template blank|support|dentista] [--no-install]");
154
268
  }
155
269
 
156
270
  const result = await createProject({ name, template, cwd: process.cwd() });
157
271
  console.log(`Created ${result.name} at ${result.path}`);
272
+
273
+ if (shouldInstall) {
274
+ console.log("");
275
+ console.log("Installing dependencies...");
276
+ await installProjectDependencies(result.path);
277
+ }
278
+
158
279
  console.log("");
159
280
  console.log("Next:");
160
281
  if (resolve(result.path) !== resolve(process.cwd())) {
161
282
  console.log(` cd ${result.name}`);
162
283
  }
163
- console.log(" npm install");
284
+ if (!shouldInstall) {
285
+ console.log(" npm install");
286
+ }
164
287
  console.log(" open this folder in Codex or Claude Code");
165
288
  console.log(' ask: "Develop an appointment and intake agent for an ophthalmology office"');
166
289
  console.log(' npm run chat -- --message "hello"');
167
290
  console.log(" npm run dev");
168
291
  console.log(" npm run agentkit -- deploy");
169
292
  console.log("");
170
- console.log("Optional: manage local secrets with `npm run agentkit -- env set <NAME> <VALUE>`.");
293
+ console.log("Optional: manage local secrets with `printf %s \"$VALUE\" | npm run agentkit -- env set <NAME> --stdin`.");
171
294
  return;
172
295
  }
173
296
 
174
297
  if (args.command === "dev") {
175
- const port = parsePort(args.flags.port);
176
- const devServer = await startAgentDevServer(process.cwd(), { port });
298
+ const port = parsePort(args.flags.port, "agentkit dev --port <number>");
299
+ const devServer = await startAgentDevServer(process.cwd(), { port, basePort: localDevBasePort() });
177
300
 
178
301
  console.log("Agent Capsule running");
179
302
  console.log("");
303
+ if (devServer.portConflict) {
304
+ const owner = devServer.portConflict.agent ? ` by ${devServer.portConflict.agent}` : "";
305
+ console.log(
306
+ `Port ${devServer.portConflict.port} is already used${owner}. Started ${devServer.capsule.config.name} at ${devServer.urls.chat}.`,
307
+ );
308
+ console.log("");
309
+ }
180
310
  console.log(`Chat: ${devServer.urls.chat}`);
181
311
  console.log(`API: ${devServer.urls.api}`);
182
312
  console.log(`Inspect: ${devServer.urls.inspect}`);
@@ -185,14 +315,55 @@ async function main() {
185
315
  return;
186
316
  }
187
317
 
318
+ if (args.command === "open") {
319
+ const server = await findRunningLocalDevServer(process.cwd());
320
+
321
+ if (!server) {
322
+ console.log("No running AgentKit dev server found for this capsule.");
323
+ console.log("Run: npm run dev");
324
+ return;
325
+ }
326
+
327
+ await openLocalUrl(server.url);
328
+ console.log(`Opened ${server.url}`);
329
+ return;
330
+ }
331
+
332
+ if (args.command === "chat-ui") {
333
+ if (!args.flags.deploy) {
334
+ throw new Error("Usage: agentkit chat-ui --deploy [--port <number>] [--token-file <path>]");
335
+ }
336
+
337
+ const port = parsePort(args.flags.port, "agentkit chat-ui --deploy --port <number>") ?? 4124;
338
+ const tokenFile = parseOptionalStringFlag(
339
+ args.flags["token-file"],
340
+ "token-file",
341
+ "agentkit chat-ui --deploy --token-file <path>",
342
+ );
343
+ const server = await startDeployChatUiServer(process.cwd(), { port, tokenFile });
344
+
345
+ console.log("AgentKit hosted chat UI running");
346
+ console.log("");
347
+ console.log(`Chat: ${server.url}`);
348
+ console.log(`Production: ${server.deployUrl}`);
349
+ console.log(`Token: ${server.tokenSource}`);
350
+ await waitForShutdown(server.stop);
351
+ return;
352
+ }
353
+
188
354
  if (args.command === "chat") {
189
355
  const message = args.flags.message;
356
+ const conversationId = parseOptionalStringFlag(
357
+ args.flags["conversation-id"] ?? args.flags.conversation,
358
+ "conversation id",
359
+ 'agentkit chat --message "hello" [--conversation-id <id>]',
360
+ );
190
361
 
191
362
  if (typeof message !== "string") {
192
- throw new Error('Missing message. Usage: agentkit chat --message "hello"');
363
+ throw new Error('Missing message. Usage: agentkit chat --message "hello" [--conversation-id <id>]');
193
364
  }
194
365
 
195
- const result = await runAgentMessageFromCwd(process.cwd(), { message });
366
+ const result = await runAgentMessageFromCwd(process.cwd(), { message, conversationId });
196
367
  console.log(result.message.content);
197
368
  return;
198
369
  }
@@ -351,8 +522,25 @@ async function main() {
351
522
 
352
523
  if (args.command === "login") {
353
524
  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.");
525
+ const apiUrl = parseCloudApiUrl(args.flags.api);
526
+ const verified = await verifyCloudLogin(apiUrl, token);
527
+ await writeCloudAuth({ apiUrl, token });
528
+ const email = verified.me.account?.email;
529
+ console.log(email ? `Logged in to AgentKit Cloud as ${email}.` : "AgentKit Cloud token verified and stored.");
530
+
531
+ const hasCloudflareAlpha = (verified.capabilities.capabilities ?? []).some(
532
+ (capability) => capability.name === "cloudflare_deploy_alpha" && capability.status === "active",
533
+ );
534
+ console.log(
535
+ hasCloudflareAlpha
536
+ ? "Entitlement: cloudflare_deploy_alpha active."
537
+ : "Warning: cloudflare_deploy_alpha entitlement was not found for this account.",
538
+ );
539
+
540
+ if (process.env.AGENTKIT_CLOUD_API_TOKEN && process.env.AGENTKIT_CLOUD_API_TOKEN !== token) {
541
+ console.log("Warning: AGENTKIT_CLOUD_API_TOKEN is set and will override this saved login in the current shell.");
542
+ }
543
+
356
544
  return;
357
545
  }
358
546
 
@@ -365,6 +553,20 @@ async function main() {
365
553
  if (args.command === "deploy") {
366
554
  const [deploySubcommand] = args.positional;
367
555
 
556
+ if (deploySubcommand === "smoke") {
557
+ const message =
558
+ parseOptionalStringFlag(args.flags.message ?? args.flags.smoke, "message", 'agentkit deploy smoke --message "hello"') ??
559
+ "hello";
560
+ const state = await requireLocalDeployState();
561
+ const token = await readDeployAccessTokenForSmoke(process.cwd(), state);
562
+ await runDeploySmoke({
563
+ deployUrl: requireDeployUrl(state),
564
+ message,
565
+ token,
566
+ });
567
+ return;
568
+ }
569
+
368
570
  if (deploySubcommand === "doctor") {
369
571
  const report = await checkDeployReadiness({
370
572
  apiUrl: await resolveCloudApiUrl(args.flags.api),
@@ -400,6 +602,7 @@ async function main() {
400
602
 
401
603
  const target = resolveDeployTarget(parseTargetFlag(args.flags.target, "deploy"));
402
604
  const anonymous = Boolean(args.flags.anonymous);
605
+ const smokeMessage = parseOptionalStringFlag(args.flags.smoke, "smoke", 'agentkit deploy --smoke "hello"');
403
606
 
404
607
  if (target.id === "vps") {
405
608
  const host = parseRequiredStringFlag(args.flags.host, "host", "agentkit deploy --target vps --host <host>");
@@ -450,6 +653,12 @@ async function main() {
450
653
  anonymous: anonymous || !auth?.token,
451
654
  });
452
655
  const statePath = await writeLocalDeployState(result.root, deploy);
656
+ const chatAccess = await ensureDeployChatAccessToken({
657
+ apiUrl,
658
+ root: result.root,
659
+ deployId: deploy.deploy.id,
660
+ skip: anonymous || !auth?.token || deploy.deploy.access.mode === "public",
661
+ });
453
662
 
454
663
  console.log("");
455
664
  console.log("Deploy ready");
@@ -457,6 +666,33 @@ async function main() {
457
666
  console.log(`Project: ${deploy.project.id}`);
458
667
  console.log(`Deploy: ${deploy.deploy.id}`);
459
668
  console.log(`State: ${statePath}`);
669
+
670
+ if (chatAccess?.status === "ready") {
671
+ console.log(`Chat auth: ${relativePath(process.cwd(), chatAccess.path)}`);
672
+ } else if (chatAccess?.status === "skipped") {
673
+ console.log("Chat auth: public or anonymous deploy; no local chat token was created.");
674
+ } else if (chatAccess?.status === "failed") {
675
+ console.log(`Chat auth: could not create local chat token. ${formatReadinessError(chatAccess.error)}`);
676
+ console.log(`Run: agentkit access token create ${defaultDeployChatAccessTokenName} --out ${defaultDeployChatAccessTokenPath}`);
677
+ }
678
+
679
+ if (smokeMessage) {
680
+ if (chatAccess?.status === "failed") {
681
+ throw new AgentKitError("deploy_smoke_failed", "Could not run deploy smoke because chat access token creation failed.");
682
+ }
683
+
684
+ await runDeploySmoke({
685
+ deployUrl: requireDeployUrl({
686
+ deploy_id: deploy.deploy.id,
687
+ url: deploy.deploy.url,
688
+ }),
689
+ message: smokeMessage,
690
+ token:
691
+ chatAccess?.status === "ready"
692
+ ? await readDeployAccessTokenForUi(chatAccess.path)
693
+ : undefined,
694
+ });
695
+ }
460
696
  return;
461
697
  }
462
698
 
@@ -474,10 +710,19 @@ async function main() {
474
710
  }
475
711
 
476
712
  if (subcommand === "set") {
477
- const value = valueParts.join(" ");
713
+ const value = await resolveSecretInputValue({
714
+ name,
715
+ valueParts,
716
+ stdinFlag: args.flags.stdin,
717
+ fromEnvFlag: args.flags["from-env"],
718
+ fromLocalEnvFlag: args.flags["from-local-env"],
719
+ localEnvRoot: args.flags["from-local-env"] !== undefined ? await findAgentCapsuleRoot(process.cwd()) : undefined,
720
+ 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>]",
721
+ inlineSensitiveHelp: "Prefer --stdin, --from-env, or --from-local-env so the value does not enter shell history.",
722
+ });
478
723
 
479
- if (!name || value.length === 0) {
480
- throw new Error("Usage: agentkit secret set <NAME> <VALUE> [--api <url>]");
724
+ if (!name) {
725
+ 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
726
  }
482
727
 
483
728
  const payload = await cloudApiRequest(apiUrl, `/v1/projects/${encodeURIComponent(projectId)}/secrets/${encodeURIComponent(name)}`, {
@@ -488,6 +733,15 @@ async function main() {
488
733
  return;
489
734
  }
490
735
 
736
+ if (subcommand === "sync") {
737
+ if (args.flags["from-local"] !== true) {
738
+ throw new Error("Usage: agentkit secret sync --from-local [--api <url>]");
739
+ }
740
+
741
+ await syncHostedSecretsFromLocal({ apiUrl, projectId });
742
+ return;
743
+ }
744
+
491
745
  if (subcommand === "unset") {
492
746
  if (!name) {
493
747
  throw new Error("Usage: agentkit secret unset <NAME> [--api <url>]");
@@ -500,14 +754,14 @@ async function main() {
500
754
  return;
501
755
  }
502
756
 
503
- throw new Error("Usage: agentkit secret set <NAME> <VALUE> | agentkit secret list | agentkit secret unset <NAME>");
757
+ 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>");
504
758
  }
505
759
 
506
760
  if (args.command === "access") {
507
761
  const [subcommand, noun, name] = args.positional;
508
762
 
509
763
  if (subcommand !== "token") {
510
- throw new Error("Usage: agentkit access token create <name> | list | revoke <token-id> [--api <url>]");
764
+ throw new Error("Usage: agentkit access token create <name> [--out <path>] | list | revoke <token-id> [--api <url>]");
511
765
  }
512
766
 
513
767
  const state = await requireLocalDeployState();
@@ -518,7 +772,7 @@ async function main() {
518
772
  method: "POST",
519
773
  body: JSON.stringify({ name }),
520
774
  });
521
- printJson(payload);
775
+ await printJsonOrWrite(payload, args.flags.out, "agentkit access token create <name> [--api <url>] [--out <path>]");
522
776
  return;
523
777
  }
524
778
 
@@ -540,7 +794,7 @@ async function main() {
540
794
  return;
541
795
  }
542
796
 
543
- throw new Error("Usage: agentkit access token create <name> | list | revoke <token-id> [--api <url>]");
797
+ throw new Error("Usage: agentkit access token create <name> [--out <path>] | list | revoke <token-id> [--api <url>]");
544
798
  }
545
799
 
546
800
  if (args.command === "channels") {
@@ -568,10 +822,18 @@ async function main() {
568
822
  }
569
823
 
570
824
  if (subcommand === "set") {
571
- const value = valueParts.join(" ");
825
+ const value = await resolveSecretInputValue({
826
+ name,
827
+ valueParts,
828
+ stdinFlag: args.flags.stdin,
829
+ fromEnvFlag: args.flags["from-env"],
830
+ fromLocalEnvFlag: undefined,
831
+ usage: "agentkit env set <NAME> <VALUE> | agentkit env set <NAME> --stdin | agentkit env set <NAME> --from-env [ENV_NAME]",
832
+ inlineSensitiveHelp: "Prefer --stdin or --from-env so the value does not enter shell history.",
833
+ });
572
834
 
573
- if (!name || value.length === 0) {
574
- throw new Error("Usage: agentkit env set <NAME> <VALUE>");
835
+ if (!name) {
836
+ throw new Error("Usage: agentkit env set <NAME> <VALUE> | agentkit env set <NAME> --stdin | agentkit env set <NAME> --from-env [ENV_NAME]");
575
837
  }
576
838
 
577
839
  await setCapsuleEnv(root, name, value);
@@ -589,7 +851,7 @@ async function main() {
589
851
  return;
590
852
  }
591
853
 
592
- throw new Error("Usage: agentkit env list | agentkit env set <NAME> <VALUE> | agentkit env unset <NAME>");
854
+ 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
855
  }
594
856
 
595
857
  if (args.command === "docs") {
@@ -653,9 +915,12 @@ Rules:
653
915
  - Follow the AgentKit contract in llms-full.txt.
654
916
  - Treat the owner's request as the brief.
655
917
  - Build or update the agent inside this capsule as far as you can without asking for obvious restatements.
918
+ - Do not wait for a wizard or recipe; implement directly in the capsule files.
656
919
  - Edit agent behavior in prompts/instructions.md.
657
920
  - Edit the agent contract in agentkit.config.ts.
658
921
  - Add TypeScript tools under tools/.
922
+ - Keep the first useful version runnable with test/fake unless the owner has already chosen a real provider.
923
+ - Before testing real conversation behavior, ask the owner to choose OpenRouter, OpenAI, Anthropic, or another supported provider. Do not choose for them.
659
924
  - Keep local secrets in .env only.
660
925
  - Do not commit or print secret values.
661
926
  - Do not edit .agentkit/ directly.
@@ -666,7 +931,6 @@ Verification:
666
931
  Run these before you finish:
667
932
 
668
933
  \`\`\`sh
669
- npm install
670
934
  npm run typecheck
671
935
  npm run agentkit -- inspect
672
936
  npm run chat -- --message "hello"
@@ -678,10 +942,16 @@ If you add a tool, also run a tool smoke test with the fake provider, for exampl
678
942
  npm run agentkit -- tool tool_name --input '{}'
679
943
  \`\`\`
680
944
 
945
+ UI testing:
946
+ - For local UI testing, run \`npm run dev\`, open the printed \`Chat:\` URL, and tell the owner the exact URL.
947
+ - 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.
948
+ - If the provider is still \`test/fake\`, say the UI was tested only with the deterministic fake provider.
949
+
681
950
  Final response:
682
951
  - Summarize what changed.
683
952
  - List files changed.
684
953
  - Paste the verification commands and results.
954
+ - State whether testing used \`test/fake\` or a real provider selected by the owner.
685
955
  - Call out any missing user details or blocked checks.
686
956
  `;
687
957
  }
@@ -716,6 +986,36 @@ type CloudCapabilitiesResponse = {
716
986
  }>;
717
987
  };
718
988
 
989
+ type CloudLimitsResponse = {
990
+ online_agents?: {
991
+ account?: {
992
+ used?: number | null;
993
+ limit?: number | null;
994
+ };
995
+ global?: {
996
+ used?: number | null;
997
+ limit?: number | null;
998
+ };
999
+ };
1000
+ };
1001
+
1002
+ type DeployAccessTokenCreateResponse = {
1003
+ access_token?: {
1004
+ id?: string;
1005
+ deploy_id?: string;
1006
+ name?: string;
1007
+ token?: string;
1008
+ };
1009
+ };
1010
+
1011
+ type DeployAccessTokenListResponse = {
1012
+ access_tokens?: Array<{
1013
+ id?: string;
1014
+ deploy_id?: string;
1015
+ name?: string;
1016
+ }>;
1017
+ };
1018
+
719
1019
  type CloudSecretsResponse = {
720
1020
  secrets?: Array<{
721
1021
  name?: string;
@@ -771,7 +1071,7 @@ async function checkDeployReadiness(options: {
771
1071
  checks.push({
772
1072
  status: "fail",
773
1073
  title: "Login",
774
- message: `AgentKit Cloud rejected the stored token. ${formatReadinessError(error)}`,
1074
+ message: `AgentKit Cloud rejected the token from ${formatCloudAuthSource(auth)}. ${formatReadinessError(error)}`,
775
1075
  action: "agentkit login --token <token>",
776
1076
  });
777
1077
  }
@@ -802,6 +1102,58 @@ async function checkDeployReadiness(options: {
802
1102
  message: `Could not verify AgentKit Cloud capabilities. ${formatReadinessError(error)}`,
803
1103
  });
804
1104
  }
1105
+
1106
+ try {
1107
+ const payload = (await cloudApiRequest(options.apiUrl, "/v1/me/limits", {
1108
+ method: "GET",
1109
+ })) as CloudLimitsResponse;
1110
+ const account = payload.online_agents?.account;
1111
+ const global = payload.online_agents?.global;
1112
+ const accountLimit = normalizeOptionalNumber(account?.limit);
1113
+ const accountUsed = normalizeOptionalNumber(account?.used);
1114
+ const globalLimit = normalizeOptionalNumber(global?.limit);
1115
+ const globalUsed = normalizeOptionalNumber(global?.used);
1116
+
1117
+ if (accountLimit !== undefined && accountUsed !== undefined && accountUsed >= accountLimit) {
1118
+ checks.push({
1119
+ status: "fail",
1120
+ title: "Online agent capacity",
1121
+ message: `This account is already using ${accountUsed}/${accountLimit} online AgentKit deploys.`,
1122
+ action: "agentkit deploy pause",
1123
+ });
1124
+ } else if (globalLimit !== undefined && globalUsed !== undefined && globalUsed >= globalLimit) {
1125
+ checks.push({
1126
+ status: "fail",
1127
+ title: "Online agent capacity",
1128
+ message: `AgentKit Cloud is already using ${globalUsed}/${globalLimit} global online deploy slots.`,
1129
+ action: "agentkit deploy status",
1130
+ });
1131
+ } else if (accountLimit !== undefined && accountUsed !== undefined) {
1132
+ checks.push({
1133
+ status: "pass",
1134
+ title: "Online agent capacity",
1135
+ message: `Account has capacity: ${accountUsed}/${accountLimit} online deploys are in use.`,
1136
+ });
1137
+ } else if (globalLimit !== undefined && globalUsed !== undefined) {
1138
+ checks.push({
1139
+ status: "pass",
1140
+ title: "Online agent capacity",
1141
+ message: `Global capacity is available: ${globalUsed}/${globalLimit} online deploys are in use.`,
1142
+ });
1143
+ } else {
1144
+ checks.push({
1145
+ status: "info",
1146
+ title: "Online agent capacity",
1147
+ message: "No online deploy limit is configured for this account.",
1148
+ });
1149
+ }
1150
+ } catch (error) {
1151
+ checks.push({
1152
+ status: "warn",
1153
+ title: "Online agent capacity",
1154
+ message: `Could not verify online deploy capacity. ${formatReadinessError(error)}`,
1155
+ });
1156
+ }
805
1157
  }
806
1158
 
807
1159
  if (context.agentKitManagedSecrets.length > 0) {
@@ -901,14 +1253,50 @@ function printDeployReadinessReport(report: DeployReadinessReport): void {
901
1253
 
902
1254
  function secretSetCommand(secret: string, source: DeployReadinessContext["localSecrets"][string] | undefined): string {
903
1255
  if (source?.dotenv) {
904
- return `agentkit secret set ${secret} <value-from-.env>`;
1256
+ return `agentkit secret set ${secret} --from-local-env`;
905
1257
  }
906
1258
 
907
1259
  if (source?.environment) {
908
- return `agentkit secret set ${secret} "$${secret}"`;
1260
+ return `agentkit secret set ${secret} --from-env`;
1261
+ }
1262
+
1263
+ return `agentkit secret set ${secret} --stdin`;
1264
+ }
1265
+
1266
+ async function syncHostedSecretsFromLocal(options: { apiUrl: string; projectId: string }): Promise<void> {
1267
+ const context = await loadDeployReadinessContext(process.cwd(), process.env);
1268
+ const localEnv = await loadCapsuleEnv(context.root, {});
1269
+ let synced = 0;
1270
+ let skipped = 0;
1271
+
1272
+ for (const secret of context.userManagedSecrets) {
1273
+ const value = localEnv[secret];
1274
+
1275
+ if (value === undefined) {
1276
+ skipped += 1;
1277
+ console.log(`${secret}\tskipped_missing_local`);
1278
+ continue;
1279
+ }
1280
+
1281
+ await cloudApiRequest(options.apiUrl, `/v1/projects/${encodeURIComponent(options.projectId)}/secrets/${encodeURIComponent(secret)}`, {
1282
+ method: "PUT",
1283
+ body: JSON.stringify({ value }),
1284
+ });
1285
+ synced += 1;
1286
+ console.log(`${secret}\tsynced`);
1287
+ }
1288
+
1289
+ if (context.userManagedSecrets.length === 0) {
1290
+ console.log("No user-managed hosted secrets are declared by this capsule.");
1291
+ return;
909
1292
  }
910
1293
 
911
- return `agentkit secret set ${secret} <value>`;
1294
+ console.log("");
1295
+ console.log(`${synced} synced, ${skipped} skipped`);
1296
+ }
1297
+
1298
+ function normalizeOptionalNumber(value: number | null | undefined): number | undefined {
1299
+ return typeof value === "number" && Number.isFinite(value) ? value : undefined;
912
1300
  }
913
1301
 
914
1302
  function formatReadinessError(error: unknown): string {
@@ -923,6 +1311,10 @@ function isString(value: unknown): value is string {
923
1311
  return typeof value === "string";
924
1312
  }
925
1313
 
1314
+ function isRecord(value: unknown): value is Record<string, unknown> {
1315
+ return typeof value === "object" && value !== null && !Array.isArray(value);
1316
+ }
1317
+
926
1318
  async function handleChannelsCommand(args: ParsedArgs): Promise<void> {
927
1319
  const [subcommand, first, second, third] = args.positional;
928
1320
 
@@ -1379,6 +1771,7 @@ function cloudFetch(apiUrl: string, path: string, init: RequestInit = {}): Promi
1379
1771
  type CloudAuthConfig = {
1380
1772
  apiUrl: string;
1381
1773
  token: string;
1774
+ source?: "environment" | "file";
1382
1775
  };
1383
1776
 
1384
1777
  function cloudAuthPath(): string {
@@ -1390,6 +1783,7 @@ async function readCloudAuth(): Promise<CloudAuthConfig | null> {
1390
1783
  return {
1391
1784
  apiUrl: process.env.AGENTKIT_CLOUD_API_URL ?? defaultAgentKitCloudApiUrl,
1392
1785
  token: process.env.AGENTKIT_CLOUD_API_TOKEN,
1786
+ source: "environment",
1393
1787
  };
1394
1788
  }
1395
1789
 
@@ -1400,6 +1794,7 @@ async function readCloudAuth(): Promise<CloudAuthConfig | null> {
1400
1794
  return {
1401
1795
  apiUrl: typeof parsed.apiUrl === "string" ? parsed.apiUrl : defaultAgentKitCloudApiUrl,
1402
1796
  token: parsed.token,
1797
+ source: "file",
1403
1798
  };
1404
1799
  }
1405
1800
  } catch {
@@ -1414,6 +1809,87 @@ async function writeCloudAuth(config: CloudAuthConfig): Promise<void> {
1414
1809
  await writeFile(cloudAuthPath(), `${JSON.stringify(config, null, 2)}\n`, { mode: 0o600 });
1415
1810
  }
1416
1811
 
1812
+ async function ensureDeployChatAccessToken(options: {
1813
+ apiUrl: string;
1814
+ root: string;
1815
+ deployId: string;
1816
+ skip: boolean;
1817
+ }): Promise<
1818
+ | { status: "ready"; path: string; tokenId: string; created: boolean }
1819
+ | { status: "skipped" }
1820
+ | { status: "failed"; error: unknown }
1821
+ > {
1822
+ if (options.skip) {
1823
+ return { status: "skipped" };
1824
+ }
1825
+
1826
+ try {
1827
+ const path = join(options.root, defaultDeployChatAccessTokenPath);
1828
+ const existing = await readDeployAccessTokenFile(path);
1829
+
1830
+ if (existing?.deploy_id === options.deployId && existing.id) {
1831
+ const listed = (await cloudApiRequest(
1832
+ options.apiUrl,
1833
+ `/v1/deploys/${encodeURIComponent(options.deployId)}/access-tokens`,
1834
+ { method: "GET" },
1835
+ )) as DeployAccessTokenListResponse;
1836
+ const stillExists = (listed.access_tokens ?? []).some((token) => token.id === existing.id);
1837
+
1838
+ if (stillExists) {
1839
+ return { status: "ready", path, tokenId: existing.id, created: false };
1840
+ }
1841
+ }
1842
+
1843
+ const created = (await cloudApiRequest(
1844
+ options.apiUrl,
1845
+ `/v1/deploys/${encodeURIComponent(options.deployId)}/access-tokens`,
1846
+ {
1847
+ method: "POST",
1848
+ body: JSON.stringify({ name: defaultDeployChatAccessTokenName }),
1849
+ },
1850
+ )) as DeployAccessTokenCreateResponse;
1851
+ const token = created.access_token;
1852
+
1853
+ if (!token?.id || token.deploy_id !== options.deployId || !token.token) {
1854
+ throw new AgentKitError("access_token_invalid", "AgentKit Cloud returned an invalid deploy access token payload.");
1855
+ }
1856
+
1857
+ await writeSecretJson(path, created);
1858
+ return { status: "ready", path, tokenId: token.id, created: true };
1859
+ } catch (error) {
1860
+ return { status: "failed", error };
1861
+ }
1862
+ }
1863
+
1864
+ async function readDeployAccessTokenFile(path: string): Promise<{ id?: string; deploy_id?: string } | null> {
1865
+ try {
1866
+ const parsed = JSON.parse(await readFile(path, "utf8")) as DeployAccessTokenCreateResponse;
1867
+ return parsed.access_token ?? null;
1868
+ } catch {
1869
+ return null;
1870
+ }
1871
+ }
1872
+
1873
+ async function writeSecretJson(path: string, value: unknown): Promise<void> {
1874
+ await mkdir(dirname(path), { recursive: true });
1875
+ await writeFile(path, `${JSON.stringify(value, null, 2)}\n`, { mode: 0o600 });
1876
+ }
1877
+
1878
+ async function verifyCloudLogin(
1879
+ apiUrl: string,
1880
+ token: string,
1881
+ ): Promise<{ me: CloudMeResponse; capabilities: CloudCapabilitiesResponse }> {
1882
+ const me = await cloudApiRequestWithToken<CloudMeResponse>(apiUrl, "/v1/me", token, { method: "GET" });
1883
+ const capabilities = await cloudApiRequestWithToken<CloudCapabilitiesResponse>(
1884
+ apiUrl,
1885
+ "/v1/me/capabilities",
1886
+ token,
1887
+ { method: "GET" },
1888
+ );
1889
+
1890
+ return { me, capabilities };
1891
+ }
1892
+
1417
1893
  async function clearCloudAuth(): Promise<void> {
1418
1894
  await rm(cloudAuthPath(), { force: true });
1419
1895
  }
@@ -1437,6 +1913,65 @@ async function requireLocalDeployState(): Promise<LocalDeployState> {
1437
1913
  return state;
1438
1914
  }
1439
1915
 
1916
+ function requireDeployUrl(state: Pick<LocalDeployState, "deploy_id" | "url">): string {
1917
+ if (!state.url) {
1918
+ throw new Error(`Deploy ${state.deploy_id} does not have a URL. Run agentkit deploy again.`);
1919
+ }
1920
+
1921
+ return state.url;
1922
+ }
1923
+
1924
+ async function readDeployAccessTokenForSmoke(cwd: string, state: LocalDeployState): Promise<string | undefined> {
1925
+ if (state.access?.mode === "public") {
1926
+ return undefined;
1927
+ }
1928
+
1929
+ const root = await findAgentCapsuleRoot(cwd);
1930
+ return readDeployAccessTokenForUi(join(root, defaultDeployChatAccessTokenPath));
1931
+ }
1932
+
1933
+ async function runDeploySmoke(input: { deployUrl: string; message: string; token?: string }): Promise<void> {
1934
+ const headers = new Headers({ "Content-Type": "application/json" });
1935
+
1936
+ if (input.token) {
1937
+ headers.set("Authorization", `Bearer ${input.token}`);
1938
+ }
1939
+
1940
+ let response: Response;
1941
+
1942
+ try {
1943
+ response = await fetch(new URL("/v1/chat", input.deployUrl).href, {
1944
+ method: "POST",
1945
+ headers,
1946
+ body: JSON.stringify({ message: input.message }),
1947
+ });
1948
+ } catch (error) {
1949
+ throw new AgentKitError(
1950
+ "deploy_smoke_failed",
1951
+ `Deploy smoke request failed before a response: ${error instanceof Error ? error.message : String(error)}`,
1952
+ );
1953
+ }
1954
+
1955
+ const payload = await response.json().catch(() => null);
1956
+
1957
+ if (!response.ok) {
1958
+ throw new AgentKitError("deploy_smoke_failed", readCloudError(payload, response.status));
1959
+ }
1960
+
1961
+ const content =
1962
+ payload && typeof payload === "object" && "message" in payload && isRecord((payload as { message?: unknown }).message)
1963
+ ? (payload as { message: { content?: unknown } }).message.content
1964
+ : undefined;
1965
+
1966
+ console.log("");
1967
+ console.log("Deploy smoke verified");
1968
+ console.log(`URL: ${input.deployUrl}`);
1969
+ console.log(`Message: ${input.message}`);
1970
+ if (typeof content === "string") {
1971
+ console.log(`Response: ${content}`);
1972
+ }
1973
+ }
1974
+
1440
1975
  async function resolveProjectIdForSecretCommand(cwd: string): Promise<string> {
1441
1976
  const state = await readLocalDeployStateIfExists(cwd);
1442
1977
 
@@ -1472,16 +2007,66 @@ async function cloudApiRequest(apiUrl: string, path: string, init: RequestInit):
1472
2007
  payload && typeof payload === "object" && "error" in payload
1473
2008
  ? String((payload as { error?: { code?: unknown } }).error?.code ?? "cloud_request_failed")
1474
2009
  : "cloud_request_failed";
1475
- throw new AgentKitError(code, readCloudError(payload, response.status));
2010
+ let message = readCloudError(payload, response.status);
2011
+
2012
+ if (code === "auth_token_invalid") {
2013
+ message = `${message} Token source: ${auth ? formatCloudAuthSource(auth) : "none"}. Run: agentkit login --token <token>`;
2014
+ }
2015
+
2016
+ throw new AgentKitError(code, message);
1476
2017
  }
1477
2018
 
1478
2019
  return payload;
1479
2020
  }
1480
2021
 
2022
+ async function cloudApiRequestWithToken<T>(
2023
+ apiUrl: string,
2024
+ path: string,
2025
+ token: string,
2026
+ init: RequestInit,
2027
+ ): Promise<T> {
2028
+ const headers = new Headers(init.headers);
2029
+
2030
+ if (!headers.has("Content-Type") && init.body !== undefined) {
2031
+ headers.set("Content-Type", "application/json");
2032
+ }
2033
+
2034
+ headers.set("Authorization", `Bearer ${token}`);
2035
+
2036
+ const response = await fetch(new URL(path, parseCloudApiUrl(apiUrl)).href, {
2037
+ ...init,
2038
+ headers,
2039
+ });
2040
+ const payload = await response.json().catch(() => null);
2041
+
2042
+ if (!response.ok) {
2043
+ const code =
2044
+ payload && typeof payload === "object" && "error" in payload
2045
+ ? String((payload as { error?: { code?: unknown } }).error?.code ?? "cloud_request_failed")
2046
+ : "cloud_request_failed";
2047
+ throw new AgentKitError(code, readCloudError(payload, response.status));
2048
+ }
2049
+
2050
+ return payload as T;
2051
+ }
2052
+
1481
2053
  function printJson(value: unknown): void {
1482
2054
  console.log(JSON.stringify(value, null, 2));
1483
2055
  }
1484
2056
 
2057
+ async function printJsonOrWrite(value: unknown, outFlag: string | boolean | undefined, usage: string): Promise<void> {
2058
+ const outPath = parseOptionalStringFlag(outFlag, "out", usage);
2059
+
2060
+ if (!outPath) {
2061
+ printJson(value);
2062
+ return;
2063
+ }
2064
+
2065
+ const absolutePath = resolve(process.cwd(), outPath);
2066
+ await writeSecretJson(absolutePath, value);
2067
+ console.log(`Deploy access token written to ${absolutePath}`);
2068
+ }
2069
+
1485
2070
  function readCloudError(payload: unknown, status: number): string {
1486
2071
  if (payload && typeof payload === "object" && "error" in payload) {
1487
2072
  const error = (payload as { error?: { message?: unknown } }).error;
@@ -1493,19 +2078,36 @@ function readCloudError(payload: unknown, status: number): string {
1493
2078
  return `AgentKit Cloud request failed with HTTP ${status}.`;
1494
2079
  }
1495
2080
 
1496
- function parsePort(value: string | boolean | undefined): number | undefined {
2081
+ function formatCloudAuthSource(auth: CloudAuthConfig): string {
2082
+ if (auth.source === "environment") {
2083
+ return "AGENTKIT_CLOUD_API_TOKEN";
2084
+ }
2085
+
2086
+ if (auth.source === "file") {
2087
+ return cloudAuthPath();
2088
+ }
2089
+
2090
+ return "AgentKit Cloud auth";
2091
+ }
2092
+
2093
+ function relativePath(root: string, path: string): string {
2094
+ const output = relative(root, path);
2095
+ return output.startsWith("..") ? path : output || ".";
2096
+ }
2097
+
2098
+ function parsePort(value: string | boolean | undefined, usage = "agentkit dev --port <number>"): number | undefined {
1497
2099
  if (value === undefined) {
1498
2100
  return undefined;
1499
2101
  }
1500
2102
 
1501
2103
  if (typeof value !== "string") {
1502
- throw new Error("Missing port. Usage: agentkit dev --port <number>");
2104
+ throw new Error(`Missing port. Usage: ${usage}`);
1503
2105
  }
1504
2106
 
1505
2107
  const port = Number(value);
1506
2108
 
1507
2109
  if (!Number.isInteger(port) || port <= 0 || port > 65535) {
1508
- throw new Error("Invalid port. Usage: agentkit dev --port <number>");
2110
+ throw new Error(`Invalid port. Usage: ${usage}`);
1509
2111
  }
1510
2112
 
1511
2113
  return port;
@@ -1557,6 +2159,100 @@ function parseOptionalStringFlag(value: string | boolean | undefined, name: stri
1557
2159
  return value.trim();
1558
2160
  }
1559
2161
 
2162
+ async function resolveSecretInputValue(options: {
2163
+ name: string | undefined;
2164
+ valueParts: string[];
2165
+ stdinFlag: string | boolean | undefined;
2166
+ fromEnvFlag: string | boolean | undefined;
2167
+ fromLocalEnvFlag: string | boolean | undefined;
2168
+ localEnvRoot?: string;
2169
+ usage: string;
2170
+ inlineSensitiveHelp?: string;
2171
+ }): Promise<string> {
2172
+ const { name, valueParts, stdinFlag, fromEnvFlag, fromLocalEnvFlag, localEnvRoot, usage, inlineSensitiveHelp } = options;
2173
+
2174
+ if (!name) {
2175
+ throw new Error(`Missing name. Usage: ${usage}`);
2176
+ }
2177
+
2178
+ const hasInlineValue = valueParts.length > 0;
2179
+ const hasStdin = stdinFlag !== undefined;
2180
+ const hasFromEnv = fromEnvFlag !== undefined;
2181
+ const hasFromLocalEnv = fromLocalEnvFlag !== undefined;
2182
+ const inputModes = [hasInlineValue, hasStdin, hasFromEnv, hasFromLocalEnv].filter(Boolean).length;
2183
+
2184
+ if (inputModes !== 1) {
2185
+ throw new Error(`Provide exactly one value source. Usage: ${usage}`);
2186
+ }
2187
+
2188
+ let value: string;
2189
+
2190
+ if (hasInlineValue) {
2191
+ value = valueParts.join(" ");
2192
+ if (looksLikeSecretName(name)) {
2193
+ console.error(
2194
+ `Warning: ${name} looks sensitive. ${inlineSensitiveHelp ?? "Prefer --stdin or --from-env so the value does not enter shell history."}`,
2195
+ );
2196
+ }
2197
+ } else if (hasStdin) {
2198
+ if (stdinFlag !== true) {
2199
+ throw new Error(`--stdin does not take a value. Usage: ${usage}`);
2200
+ }
2201
+
2202
+ value = stripOneTrailingNewline(await readStdinText());
2203
+ } else if (hasFromEnv) {
2204
+ const envName = fromEnvFlag === true ? name : parseRequiredStringFlag(fromEnvFlag, "env name", usage);
2205
+ const envValue = process.env[envName];
2206
+
2207
+ if (envValue === undefined) {
2208
+ throw new AgentKitError("secret_not_found", `Environment variable ${envName} is not set.`);
2209
+ }
2210
+
2211
+ value = envValue;
2212
+ } else {
2213
+ if (fromLocalEnvFlag !== true) {
2214
+ throw new Error(`--from-local-env does not take a value. Usage: ${usage}`);
2215
+ }
2216
+
2217
+ if (!localEnvRoot) {
2218
+ throw new Error(`--from-local-env is not available for this command. Usage: ${usage}`);
2219
+ }
2220
+
2221
+ const localEnv = await loadCapsuleEnv(localEnvRoot, {});
2222
+ const localValue = localEnv[name];
2223
+
2224
+ if (localValue === undefined) {
2225
+ throw new AgentKitError("secret_not_found", `Local .env does not contain ${name}.`);
2226
+ }
2227
+
2228
+ value = localValue;
2229
+ }
2230
+
2231
+ if (value.length === 0) {
2232
+ throw new Error(`Secret value cannot be empty. Usage: ${usage}`);
2233
+ }
2234
+
2235
+ return value;
2236
+ }
2237
+
2238
+ function looksLikeSecretName(name: string): boolean {
2239
+ return /(?:SECRET|TOKEN|KEY|PASSWORD|CREDENTIAL|PRIVATE)/i.test(name);
2240
+ }
2241
+
2242
+ async function readStdinText(): Promise<string> {
2243
+ const chunks: Buffer[] = [];
2244
+
2245
+ for await (const chunk of process.stdin) {
2246
+ chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
2247
+ }
2248
+
2249
+ return Buffer.concat(chunks).toString("utf8");
2250
+ }
2251
+
2252
+ function stripOneTrailingNewline(value: string): string {
2253
+ return value.replace(/\r?\n$/, "");
2254
+ }
2255
+
1560
2256
  function printBuildResult(result: Awaited<ReturnType<typeof buildAgentCapsule>>): void {
1561
2257
  console.log("AgentKit build complete");
1562
2258
  console.log("");
@@ -1614,6 +2310,14 @@ async function deployCloudflare(wranglerPath: string, buildDir: string): Promise
1614
2310
  }
1615
2311
  }
1616
2312
 
2313
+ async function installProjectDependencies(projectPath: string): Promise<void> {
2314
+ const exitCode = await runInheritedCommand("npm", ["install"], projectPath);
2315
+
2316
+ if (exitCode !== 0) {
2317
+ throw new Error(`Dependency install failed with exit code ${exitCode}. Run \`npm install\` in ${projectPath}.`);
2318
+ }
2319
+ }
2320
+
1617
2321
  function runInheritedCommand(command: string, args: string[], cwd: string): Promise<number> {
1618
2322
  return new Promise((resolvePromise, reject) => {
1619
2323
  const child = spawn(command, args, {
@@ -1669,6 +2373,456 @@ function parseJsonInput(source: string, label: string): unknown {
1669
2373
  }
1670
2374
  }
1671
2375
 
2376
+ async function findRunningLocalDevServer(cwd: string): Promise<{ url: string; agent: string } | null> {
2377
+ const capsule = await loadAgentCapsule(cwd);
2378
+ const basePort = localDevBasePort();
2379
+
2380
+ for (let port = basePort; port <= basePort + 50; port += 1) {
2381
+ const state = await fetchLocalAgentKitState(`http://localhost:${port}/_agentkit`);
2382
+
2383
+ if (state?.agent === capsule.config.name) {
2384
+ return {
2385
+ url: `http://localhost:${port}`,
2386
+ agent: state.agent,
2387
+ };
2388
+ }
2389
+ }
2390
+
2391
+ return null;
2392
+ }
2393
+
2394
+ function localDevBasePort(): number {
2395
+ const value = process.env.AGENTKIT_DEV_BASE_PORT;
2396
+
2397
+ if (!value) {
2398
+ return defaultAgentKitDevPort;
2399
+ }
2400
+
2401
+ const parsed = Number(value);
2402
+ return Number.isInteger(parsed) && parsed > 0 && parsed <= 65535 ? parsed : defaultAgentKitDevPort;
2403
+ }
2404
+
2405
+ async function fetchLocalAgentKitState(url: string): Promise<{ agent?: string } | null> {
2406
+ const controller = new AbortController();
2407
+ const timeout = setTimeout(() => controller.abort(), 300);
2408
+
2409
+ try {
2410
+ const response = await fetch(url, { signal: controller.signal });
2411
+ const payload = await response.json().catch(() => null);
2412
+
2413
+ if (payload && typeof payload === "object") {
2414
+ return {
2415
+ agent: typeof (payload as { agent?: unknown }).agent === "string" ? (payload as { agent: string }).agent : undefined,
2416
+ };
2417
+ }
2418
+ } catch {
2419
+ return null;
2420
+ } finally {
2421
+ clearTimeout(timeout);
2422
+ }
2423
+
2424
+ return null;
2425
+ }
2426
+
2427
+ async function openLocalUrl(url: string): Promise<void> {
2428
+ if (process.env.AGENTKIT_OPEN_COMMAND === "none") {
2429
+ return;
2430
+ }
2431
+
2432
+ const command =
2433
+ process.env.AGENTKIT_OPEN_COMMAND ??
2434
+ (process.platform === "darwin" ? "open" : process.platform === "win32" ? "cmd" : "xdg-open");
2435
+ const args = process.env.AGENTKIT_OPEN_COMMAND
2436
+ ? [url]
2437
+ : process.platform === "win32"
2438
+ ? ["/c", "start", "", url]
2439
+ : [url];
2440
+
2441
+ await new Promise<void>((resolvePromise, reject) => {
2442
+ const child = spawn(command, args, {
2443
+ detached: true,
2444
+ stdio: "ignore",
2445
+ });
2446
+ child.once("error", reject);
2447
+ child.once("spawn", () => {
2448
+ child.unref();
2449
+ resolvePromise();
2450
+ });
2451
+ });
2452
+ }
2453
+
2454
+ async function startDeployChatUiServer(
2455
+ cwd: string,
2456
+ options: { port: number; tokenFile?: string },
2457
+ ): Promise<{ url: string; deployUrl: string; tokenSource: string; stop(): Promise<void> }> {
2458
+ const root = await findAgentCapsuleRoot(cwd);
2459
+ const state = await readLocalDeployStateIfExists(root);
2460
+
2461
+ if (!state?.url) {
2462
+ throw new Error("No hosted deploy URL found. Run agentkit deploy first.");
2463
+ }
2464
+
2465
+ const tokenPath = options.tokenFile
2466
+ ? resolve(root, options.tokenFile)
2467
+ : join(root, defaultDeployChatAccessTokenPath);
2468
+ const hostname = "localhost";
2469
+ const deployUrl = state.url;
2470
+ const server = createServer(async (request, response) => {
2471
+ try {
2472
+ const requestUrl = new URL(request.url ?? "/", `http://${request.headers.host ?? `${hostname}:${options.port}`}`);
2473
+
2474
+ if (request.method === "GET" && requestUrl.pathname === "/") {
2475
+ sendHtml(response, renderDeployChatUi({ deployUrl }));
2476
+ return;
2477
+ }
2478
+
2479
+ if (request.method === "GET" && requestUrl.pathname === "/health") {
2480
+ sendJson(response, 200, { status: "ok", target: "deploy-chat-ui", deployUrl });
2481
+ return;
2482
+ }
2483
+
2484
+ if (request.method === "GET" && requestUrl.pathname === "/_agentkit") {
2485
+ sendJson(response, 200, {
2486
+ deployUrl,
2487
+ access: "proxied",
2488
+ tokenSource: process.env.AGENTKIT_DEPLOY_ACCESS_TOKEN ? "AGENTKIT_DEPLOY_ACCESS_TOKEN" : relativePath(root, tokenPath),
2489
+ });
2490
+ return;
2491
+ }
2492
+
2493
+ if (request.method === "POST" && (requestUrl.pathname === "/v1/chat" || requestUrl.pathname === "/chat")) {
2494
+ const token = await readDeployAccessTokenForUi(tokenPath);
2495
+ const body = await readIncomingBody(request);
2496
+ const contentTypeHeader = request.headers["content-type"];
2497
+ const contentType = Array.isArray(contentTypeHeader)
2498
+ ? contentTypeHeader[0] ?? "application/json"
2499
+ : contentTypeHeader ?? "application/json";
2500
+ const upstream = await fetch(new URL("/v1/chat", deployUrl).href, {
2501
+ method: "POST",
2502
+ headers: {
2503
+ "Authorization": `Bearer ${token}`,
2504
+ "Content-Type": contentType,
2505
+ },
2506
+ body,
2507
+ });
2508
+ const text = await upstream.text();
2509
+
2510
+ response.writeHead(upstream.status, {
2511
+ "content-type": upstream.headers.get("content-type") ?? "application/json",
2512
+ });
2513
+ response.end(text);
2514
+ return;
2515
+ }
2516
+
2517
+ sendJson(response, 404, { error: { code: "not_found", message: "Route not found." } });
2518
+ } catch (error) {
2519
+ sendJson(response, 500, {
2520
+ error: {
2521
+ code: isAgentKitError(error) ? error.code : "deploy_chat_ui_error",
2522
+ message: error instanceof Error ? error.message : String(error),
2523
+ },
2524
+ });
2525
+ }
2526
+ });
2527
+
2528
+ await new Promise<void>((resolvePromise, reject) => {
2529
+ server.once("error", reject);
2530
+ server.listen(options.port, hostname, () => {
2531
+ server.off("error", reject);
2532
+ resolvePromise();
2533
+ });
2534
+ });
2535
+
2536
+ return {
2537
+ url: `http://${hostname}:${options.port}`,
2538
+ deployUrl,
2539
+ tokenSource: process.env.AGENTKIT_DEPLOY_ACCESS_TOKEN ? "AGENTKIT_DEPLOY_ACCESS_TOKEN" : relativePath(root, tokenPath),
2540
+ stop: () =>
2541
+ new Promise<void>((resolvePromise, reject) => {
2542
+ server.close((error) => (error ? reject(error) : resolvePromise()));
2543
+ }),
2544
+ };
2545
+ }
2546
+
2547
+ async function readDeployAccessTokenForUi(tokenPath: string): Promise<string> {
2548
+ if (process.env.AGENTKIT_DEPLOY_ACCESS_TOKEN) {
2549
+ return process.env.AGENTKIT_DEPLOY_ACCESS_TOKEN;
2550
+ }
2551
+
2552
+ let payload: unknown;
2553
+
2554
+ try {
2555
+ payload = JSON.parse(await readFile(tokenPath, "utf8"));
2556
+ } catch (error) {
2557
+ throw new AgentKitError(
2558
+ "deploy_access_token_missing",
2559
+ `Could not read a clean deploy access token file at ${tokenPath}. Run: agentkit access token create ${defaultDeployChatAccessTokenName} --out ${defaultDeployChatAccessTokenPath}`,
2560
+ );
2561
+ }
2562
+
2563
+ const token =
2564
+ payload && typeof payload === "object"
2565
+ ? (payload as { access_token?: { token?: unknown }; token?: unknown; accessToken?: unknown }).access_token?.token ??
2566
+ (payload as { token?: unknown }).token ??
2567
+ (payload as { accessToken?: unknown }).accessToken
2568
+ : undefined;
2569
+
2570
+ if (typeof token !== "string" || token.trim().length === 0) {
2571
+ throw new AgentKitError(
2572
+ "deploy_access_token_invalid",
2573
+ `Deploy access token file ${tokenPath} does not contain access_token.token. Recreate it with: agentkit access token create ${defaultDeployChatAccessTokenName} --out ${defaultDeployChatAccessTokenPath}`,
2574
+ );
2575
+ }
2576
+
2577
+ return token.trim();
2578
+ }
2579
+
2580
+ function readIncomingBody(request: IncomingMessage): Promise<string> {
2581
+ return new Promise((resolvePromise, reject) => {
2582
+ let body = "";
2583
+ request.setEncoding("utf8");
2584
+ request.on("data", (chunk) => {
2585
+ body += chunk;
2586
+ });
2587
+ request.on("end", () => resolvePromise(body));
2588
+ request.on("error", reject);
2589
+ });
2590
+ }
2591
+
2592
+ function sendJson(response: ServerResponse, status: number, body: unknown): void {
2593
+ response.writeHead(status, { "content-type": "application/json; charset=utf-8" });
2594
+ response.end(JSON.stringify(body, null, 2));
2595
+ }
2596
+
2597
+ function sendHtml(response: ServerResponse, body: string): void {
2598
+ response.writeHead(200, { "content-type": "text/html; charset=utf-8" });
2599
+ response.end(body);
2600
+ }
2601
+
2602
+ function renderDeployChatUi(input: { deployUrl: string }): string {
2603
+ const deployUrl = escapeHtml(input.deployUrl);
2604
+
2605
+ return `<!doctype html>
2606
+ <html lang="en">
2607
+ <head>
2608
+ <meta charset="utf-8">
2609
+ <meta name="viewport" content="width=device-width, initial-scale=1">
2610
+ <title>AgentKit Hosted Chat</title>
2611
+ <style>
2612
+ :root {
2613
+ color-scheme: light;
2614
+ --bg: #f6f7f5;
2615
+ --panel: #ffffff;
2616
+ --ink: #182022;
2617
+ --muted: #667175;
2618
+ --line: #d9ded9;
2619
+ --accent: #176b5d;
2620
+ --danger: #9f2d2d;
2621
+ }
2622
+
2623
+ * { box-sizing: border-box; }
2624
+
2625
+ body {
2626
+ margin: 0;
2627
+ min-height: 100svh;
2628
+ background: var(--bg);
2629
+ color: var(--ink);
2630
+ font-family: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
2631
+ }
2632
+
2633
+ main {
2634
+ min-height: 100svh;
2635
+ display: grid;
2636
+ grid-template-rows: auto 1fr auto;
2637
+ background: var(--panel);
2638
+ }
2639
+
2640
+ header {
2641
+ border-bottom: 1px solid var(--line);
2642
+ padding: 18px 22px;
2643
+ }
2644
+
2645
+ h1 {
2646
+ margin: 0 0 4px;
2647
+ font-size: 20px;
2648
+ line-height: 1.2;
2649
+ }
2650
+
2651
+ code {
2652
+ color: var(--muted);
2653
+ font-size: 12px;
2654
+ overflow-wrap: anywhere;
2655
+ }
2656
+
2657
+ #messages {
2658
+ padding: 22px;
2659
+ overflow: auto;
2660
+ display: flex;
2661
+ flex-direction: column;
2662
+ gap: 12px;
2663
+ }
2664
+
2665
+ .message {
2666
+ max-width: 780px;
2667
+ display: grid;
2668
+ gap: 4px;
2669
+ }
2670
+
2671
+ .message.user {
2672
+ align-self: end;
2673
+ }
2674
+
2675
+ .role {
2676
+ color: var(--muted);
2677
+ font-size: 12px;
2678
+ font-weight: 700;
2679
+ text-transform: uppercase;
2680
+ }
2681
+
2682
+ .bubble {
2683
+ border: 1px solid var(--line);
2684
+ border-radius: 8px;
2685
+ padding: 12px 14px;
2686
+ white-space: pre-wrap;
2687
+ line-height: 1.45;
2688
+ background: #fbfbfa;
2689
+ }
2690
+
2691
+ .user .bubble {
2692
+ border-color: #b8d8ce;
2693
+ background: #e8f5f0;
2694
+ }
2695
+
2696
+ .error .bubble {
2697
+ border-color: #e1b1b1;
2698
+ color: var(--danger);
2699
+ background: #fff6f6;
2700
+ }
2701
+
2702
+ form {
2703
+ border-top: 1px solid var(--line);
2704
+ padding: 14px;
2705
+ display: grid;
2706
+ grid-template-columns: 1fr auto;
2707
+ gap: 10px;
2708
+ }
2709
+
2710
+ textarea {
2711
+ min-height: 52px;
2712
+ max-height: 180px;
2713
+ resize: vertical;
2714
+ border: 1px solid var(--line);
2715
+ border-radius: 8px;
2716
+ padding: 12px;
2717
+ color: var(--ink);
2718
+ font: inherit;
2719
+ }
2720
+
2721
+ button {
2722
+ min-width: 96px;
2723
+ border: 0;
2724
+ border-radius: 8px;
2725
+ padding: 0 18px;
2726
+ color: white;
2727
+ background: var(--accent);
2728
+ font: inherit;
2729
+ font-weight: 700;
2730
+ cursor: pointer;
2731
+ }
2732
+
2733
+ button:disabled {
2734
+ cursor: wait;
2735
+ opacity: 0.65;
2736
+ }
2737
+
2738
+ @media (max-width: 640px) {
2739
+ form {
2740
+ grid-template-columns: 1fr;
2741
+ }
2742
+
2743
+ button {
2744
+ min-height: 44px;
2745
+ }
2746
+ }
2747
+ </style>
2748
+ </head>
2749
+ <body>
2750
+ <main>
2751
+ <header>
2752
+ <h1>AgentKit Hosted Chat</h1>
2753
+ <code>${deployUrl}</code>
2754
+ </header>
2755
+ <section id="messages" aria-live="polite"></section>
2756
+ <form id="form">
2757
+ <textarea id="message" name="message" placeholder="Type a message" required autofocus></textarea>
2758
+ <button id="send" type="submit">Send</button>
2759
+ </form>
2760
+ </main>
2761
+ <script>
2762
+ const form = document.querySelector("#form");
2763
+ const textarea = document.querySelector("#message");
2764
+ const send = document.querySelector("#send");
2765
+ const messages = document.querySelector("#messages");
2766
+ let conversationId = null;
2767
+
2768
+ function addMessage(role, content, className = "") {
2769
+ const wrapper = document.createElement("article");
2770
+ wrapper.className = ["message", role, className].filter(Boolean).join(" ");
2771
+ const label = document.createElement("div");
2772
+ label.className = "role";
2773
+ label.textContent = role;
2774
+ const bubble = document.createElement("div");
2775
+ bubble.className = "bubble";
2776
+ bubble.textContent = content;
2777
+ wrapper.append(label, bubble);
2778
+ messages.append(wrapper);
2779
+ messages.scrollTop = messages.scrollHeight;
2780
+ }
2781
+
2782
+ form.addEventListener("submit", async (event) => {
2783
+ event.preventDefault();
2784
+ const message = textarea.value.trim();
2785
+ if (!message) return;
2786
+
2787
+ addMessage("user", message);
2788
+ textarea.value = "";
2789
+ send.disabled = true;
2790
+
2791
+ try {
2792
+ const response = await fetch("/v1/chat", {
2793
+ method: "POST",
2794
+ headers: { "content-type": "application/json" },
2795
+ body: JSON.stringify({ message, conversationId }),
2796
+ });
2797
+ const payload = await response.json();
2798
+
2799
+ if (!response.ok) {
2800
+ throw new Error(payload.error?.message || "Request failed");
2801
+ }
2802
+
2803
+ conversationId = payload.conversationId || conversationId;
2804
+ addMessage("assistant", payload.message?.content || "");
2805
+ } catch (error) {
2806
+ addMessage("error", error instanceof Error ? error.message : String(error), "error");
2807
+ } finally {
2808
+ send.disabled = false;
2809
+ textarea.focus();
2810
+ }
2811
+ });
2812
+ </script>
2813
+ </body>
2814
+ </html>`;
2815
+ }
2816
+
2817
+ function escapeHtml(value: string): string {
2818
+ return value
2819
+ .replace(/&/g, "&amp;")
2820
+ .replace(/</g, "&lt;")
2821
+ .replace(/>/g, "&gt;")
2822
+ .replace(/"/g, "&quot;")
2823
+ .replace(/'/g, "&#39;");
2824
+ }
2825
+
1672
2826
  async function waitForShutdown(stop: () => Promise<void>): Promise<void> {
1673
2827
  await new Promise<void>((resolve) => {
1674
2828
  let stopped = false;