@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/README.md +45 -6
- package/docs/guides/add-tool.md +1 -1
- package/docs/guides/channels-implementation-map.md +243 -0
- package/docs/guides/create-agent.md +23 -4
- package/docs/guides/prepare-deploy.md +18 -6
- package/docs/guides/run-evals.md +28 -2
- package/docs/guides/security-rules.md +1 -1
- package/docs/guides/use-provider.md +16 -2
- package/docs/llms-full.txt +70 -38
- package/docs/llms.txt +15 -4
- package/package.json +1 -2
- package/src/cli/index.ts +1190 -36
- package/src/cloud/artifact.ts +48 -0
- package/src/cloud/client.ts +79 -0
- package/src/cloud/contracts.ts +47 -0
- package/src/cloud/index.ts +3 -0
- package/src/index.ts +1 -1
- package/src/providers/test.ts +51 -0
- package/src/runtime/chat.ts +59 -4
- package/src/runtime/deploy.ts +1 -1
- package/src/runtime/dev-server.ts +154 -16
- package/src/runtime/runtime-contract.ts +16 -2
- package/src/runtime/targets/cloudflare/build.ts +83 -4
- package/src/storage/sqlite.ts +2 -2
- package/src/templates/blank.ts +52 -9
- package/src/templates/dentista.ts +988 -0
- package/src/templates/index.ts +2 -0
- package/src/templates/support.ts +50 -9
- package/src/runtime/targets/cloudflare/deploy.ts +0 -5475
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
|
|
48
|
+
agentkit <command> [options]
|
|
49
|
+
|
|
50
|
+
Start:
|
|
51
|
+
agentkit new <name> [--template blank|support|dentista] [--no-install]
|
|
37
52
|
agentkit dev
|
|
38
|
-
agentkit
|
|
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
|
-
|
|
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>
|
|
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
|
-
|
|
355
|
-
|
|
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 =
|
|
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
|
|
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
|
-
|
|
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 =
|
|
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
|
|
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
|
|
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}
|
|
1256
|
+
return `agentkit secret set ${secret} --from-local-env`;
|
|
905
1257
|
}
|
|
906
1258
|
|
|
907
1259
|
if (source?.environment) {
|
|
908
|
-
return `agentkit secret set ${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
|
-
|
|
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
|
-
|
|
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
|
|
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(
|
|
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(
|
|
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, "&")
|
|
2820
|
+
.replace(/</g, "<")
|
|
2821
|
+
.replace(/>/g, ">")
|
|
2822
|
+
.replace(/"/g, """)
|
|
2823
|
+
.replace(/'/g, "'");
|
|
2824
|
+
}
|
|
2825
|
+
|
|
1672
2826
|
async function waitForShutdown(stop: () => Promise<void>): Promise<void> {
|
|
1673
2827
|
await new Promise<void>((resolve) => {
|
|
1674
2828
|
let stopped = false;
|