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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,385 @@
1
+ import { readFile } from "node:fs/promises";
2
+ import { createServer, type IncomingMessage, type ServerResponse } from "node:http";
3
+ import { join, relative, resolve } from "node:path";
4
+
5
+ import { findAgentCapsuleRoot } from "../runtime/config";
6
+ import { readLocalDeployStateIfExists } from "../runtime/core/deploy-state";
7
+ import { AgentKitError, isAgentKitError } from "../runtime/errors";
8
+ import { defaultDeployChatAccessTokenName, defaultDeployChatAccessTokenPath } from "./constants";
9
+
10
+ export async function startDeployChatUiServer(
11
+ cwd: string,
12
+ options: { port: number; tokenFile?: string },
13
+ ): Promise<{ url: string; deployUrl: string; tokenSource: string; stop(): Promise<void> }> {
14
+ const root = await findAgentCapsuleRoot(cwd);
15
+ const state = await readLocalDeployStateIfExists(root);
16
+
17
+ if (!state?.url) {
18
+ throw new Error("No hosted deploy URL found. Run agentkit deploy first.");
19
+ }
20
+
21
+ const tokenPath = options.tokenFile
22
+ ? resolve(root, options.tokenFile)
23
+ : join(root, defaultDeployChatAccessTokenPath);
24
+ const hostname = "localhost";
25
+ const deployUrl = state.url;
26
+ const server = createServer(async (request, response) => {
27
+ try {
28
+ const requestUrl = new URL(request.url ?? "/", `http://${request.headers.host ?? `${hostname}:${options.port}`}`);
29
+
30
+ if (request.method === "GET" && requestUrl.pathname === "/") {
31
+ sendHtml(response, renderDeployChatUi({ deployUrl }));
32
+ return;
33
+ }
34
+
35
+ if (request.method === "GET" && requestUrl.pathname === "/health") {
36
+ sendJson(response, 200, { status: "ok", target: "deploy-chat-ui", deployUrl });
37
+ return;
38
+ }
39
+
40
+ if (request.method === "GET" && requestUrl.pathname === "/_agentkit") {
41
+ sendJson(response, 200, {
42
+ deployUrl,
43
+ access: "proxied",
44
+ tokenSource: process.env.AGENTKIT_DEPLOY_ACCESS_TOKEN ? "AGENTKIT_DEPLOY_ACCESS_TOKEN" : relativePath(root, tokenPath),
45
+ });
46
+ return;
47
+ }
48
+
49
+ if (request.method === "POST" && (requestUrl.pathname === "/v1/chat" || requestUrl.pathname === "/chat")) {
50
+ const token = await readDeployAccessTokenForUi(tokenPath);
51
+ const body = await readIncomingBody(request);
52
+ const contentTypeHeader = request.headers["content-type"];
53
+ const contentType = Array.isArray(contentTypeHeader)
54
+ ? contentTypeHeader[0] ?? "application/json"
55
+ : contentTypeHeader ?? "application/json";
56
+ const upstream = await fetch(new URL("/v1/chat", deployUrl).href, {
57
+ method: "POST",
58
+ headers: {
59
+ "Authorization": `Bearer ${token}`,
60
+ "Content-Type": contentType,
61
+ },
62
+ body,
63
+ });
64
+ const text = await upstream.text();
65
+
66
+ response.writeHead(upstream.status, {
67
+ "content-type": upstream.headers.get("content-type") ?? "application/json",
68
+ });
69
+ response.end(text);
70
+ return;
71
+ }
72
+
73
+ sendJson(response, 404, { error: { code: "not_found", message: "Route not found." } });
74
+ } catch (error) {
75
+ sendJson(response, 500, {
76
+ error: {
77
+ code: isAgentKitError(error) ? error.code : "deploy_chat_ui_error",
78
+ message: error instanceof Error ? error.message : String(error),
79
+ },
80
+ });
81
+ }
82
+ });
83
+
84
+ await new Promise<void>((resolvePromise, reject) => {
85
+ server.once("error", reject);
86
+ server.listen(options.port, hostname, () => {
87
+ server.off("error", reject);
88
+ resolvePromise();
89
+ });
90
+ });
91
+
92
+ return {
93
+ url: `http://${hostname}:${options.port}`,
94
+ deployUrl,
95
+ tokenSource: process.env.AGENTKIT_DEPLOY_ACCESS_TOKEN ? "AGENTKIT_DEPLOY_ACCESS_TOKEN" : relativePath(root, tokenPath),
96
+ stop: () =>
97
+ new Promise<void>((resolvePromise, reject) => {
98
+ server.close((error) => (error ? reject(error) : resolvePromise()));
99
+ }),
100
+ };
101
+ }
102
+
103
+ export async function readDeployAccessTokenForUi(tokenPath: string): Promise<string> {
104
+ if (process.env.AGENTKIT_DEPLOY_ACCESS_TOKEN) {
105
+ return process.env.AGENTKIT_DEPLOY_ACCESS_TOKEN;
106
+ }
107
+
108
+ let payload: unknown;
109
+
110
+ try {
111
+ payload = JSON.parse(await readFile(tokenPath, "utf8"));
112
+ } catch {
113
+ throw new AgentKitError(
114
+ "deploy_access_token_missing",
115
+ `Could not read a clean deploy access token file at ${tokenPath}. Run: agentkit access token create ${defaultDeployChatAccessTokenName} --out ${defaultDeployChatAccessTokenPath}`,
116
+ );
117
+ }
118
+
119
+ const token =
120
+ payload && typeof payload === "object"
121
+ ? (payload as { access_token?: { token?: unknown }; token?: unknown; accessToken?: unknown }).access_token?.token ??
122
+ (payload as { token?: unknown }).token ??
123
+ (payload as { accessToken?: unknown }).accessToken
124
+ : undefined;
125
+
126
+ if (typeof token !== "string" || token.trim().length === 0) {
127
+ throw new AgentKitError(
128
+ "deploy_access_token_invalid",
129
+ `Deploy access token file ${tokenPath} does not contain access_token.token. Recreate it with: agentkit access token create ${defaultDeployChatAccessTokenName} --out ${defaultDeployChatAccessTokenPath}`,
130
+ );
131
+ }
132
+
133
+ return token.trim();
134
+ }
135
+
136
+ function readIncomingBody(request: IncomingMessage): Promise<string> {
137
+ return new Promise((resolvePromise, reject) => {
138
+ let body = "";
139
+ request.setEncoding("utf8");
140
+ request.on("data", (chunk) => {
141
+ body += chunk;
142
+ });
143
+ request.on("end", () => resolvePromise(body));
144
+ request.on("error", reject);
145
+ });
146
+ }
147
+
148
+ function sendJson(response: ServerResponse, status: number, body: unknown): void {
149
+ response.writeHead(status, { "content-type": "application/json; charset=utf-8" });
150
+ response.end(JSON.stringify(body, null, 2));
151
+ }
152
+
153
+ function sendHtml(response: ServerResponse, body: string): void {
154
+ response.writeHead(200, { "content-type": "text/html; charset=utf-8" });
155
+ response.end(body);
156
+ }
157
+
158
+ function renderDeployChatUi(input: { deployUrl: string }): string {
159
+ const deployUrl = escapeHtml(input.deployUrl);
160
+
161
+ return `<!doctype html>
162
+ <html lang="en">
163
+ <head>
164
+ <meta charset="utf-8">
165
+ <meta name="viewport" content="width=device-width, initial-scale=1">
166
+ <title>AgentKit Hosted Chat</title>
167
+ <style>
168
+ :root {
169
+ color-scheme: light;
170
+ --bg: #f6f7f5;
171
+ --panel: #ffffff;
172
+ --ink: #182022;
173
+ --muted: #667175;
174
+ --line: #d9ded9;
175
+ --accent: #176b5d;
176
+ --danger: #9f2d2d;
177
+ }
178
+
179
+ * { box-sizing: border-box; }
180
+
181
+ body {
182
+ margin: 0;
183
+ min-height: 100svh;
184
+ background: var(--bg);
185
+ color: var(--ink);
186
+ font-family: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
187
+ }
188
+
189
+ main {
190
+ min-height: 100svh;
191
+ display: grid;
192
+ grid-template-rows: auto 1fr auto;
193
+ background: var(--panel);
194
+ }
195
+
196
+ header {
197
+ border-bottom: 1px solid var(--line);
198
+ padding: 18px 22px;
199
+ }
200
+
201
+ h1 {
202
+ margin: 0 0 4px;
203
+ font-size: 20px;
204
+ line-height: 1.2;
205
+ }
206
+
207
+ code {
208
+ color: var(--muted);
209
+ font-size: 12px;
210
+ overflow-wrap: anywhere;
211
+ }
212
+
213
+ #messages {
214
+ padding: 22px;
215
+ overflow: auto;
216
+ display: flex;
217
+ flex-direction: column;
218
+ gap: 12px;
219
+ }
220
+
221
+ .message {
222
+ max-width: 780px;
223
+ display: grid;
224
+ gap: 4px;
225
+ }
226
+
227
+ .message.user {
228
+ align-self: end;
229
+ }
230
+
231
+ .role {
232
+ color: var(--muted);
233
+ font-size: 12px;
234
+ font-weight: 700;
235
+ text-transform: uppercase;
236
+ }
237
+
238
+ .bubble {
239
+ border: 1px solid var(--line);
240
+ border-radius: 8px;
241
+ padding: 12px 14px;
242
+ white-space: pre-wrap;
243
+ line-height: 1.45;
244
+ background: #fbfbfa;
245
+ }
246
+
247
+ .user .bubble {
248
+ border-color: #b8d8ce;
249
+ background: #e8f5f0;
250
+ }
251
+
252
+ .error .bubble {
253
+ border-color: #e1b1b1;
254
+ color: var(--danger);
255
+ background: #fff6f6;
256
+ }
257
+
258
+ form {
259
+ border-top: 1px solid var(--line);
260
+ padding: 14px;
261
+ display: grid;
262
+ grid-template-columns: 1fr auto;
263
+ gap: 10px;
264
+ }
265
+
266
+ textarea {
267
+ min-height: 52px;
268
+ max-height: 180px;
269
+ resize: vertical;
270
+ border: 1px solid var(--line);
271
+ border-radius: 8px;
272
+ padding: 12px;
273
+ color: var(--ink);
274
+ font: inherit;
275
+ }
276
+
277
+ button {
278
+ min-width: 96px;
279
+ border: 0;
280
+ border-radius: 8px;
281
+ padding: 0 18px;
282
+ color: white;
283
+ background: var(--accent);
284
+ font: inherit;
285
+ font-weight: 700;
286
+ cursor: pointer;
287
+ }
288
+
289
+ button:disabled {
290
+ cursor: wait;
291
+ opacity: 0.65;
292
+ }
293
+
294
+ @media (max-width: 640px) {
295
+ form {
296
+ grid-template-columns: 1fr;
297
+ }
298
+
299
+ button {
300
+ min-height: 44px;
301
+ }
302
+ }
303
+ </style>
304
+ </head>
305
+ <body>
306
+ <main>
307
+ <header>
308
+ <h1>AgentKit Hosted Chat</h1>
309
+ <code>${deployUrl}</code>
310
+ </header>
311
+ <section id="messages" aria-live="polite"></section>
312
+ <form id="form">
313
+ <textarea id="message" name="message" placeholder="Type a message" required autofocus></textarea>
314
+ <button id="send" type="submit">Send</button>
315
+ </form>
316
+ </main>
317
+ <script>
318
+ const form = document.querySelector("#form");
319
+ const textarea = document.querySelector("#message");
320
+ const send = document.querySelector("#send");
321
+ const messages = document.querySelector("#messages");
322
+ let conversationId = null;
323
+
324
+ function addMessage(role, content, className = "") {
325
+ const wrapper = document.createElement("article");
326
+ wrapper.className = ["message", role, className].filter(Boolean).join(" ");
327
+ const label = document.createElement("div");
328
+ label.className = "role";
329
+ label.textContent = role;
330
+ const bubble = document.createElement("div");
331
+ bubble.className = "bubble";
332
+ bubble.textContent = content;
333
+ wrapper.append(label, bubble);
334
+ messages.append(wrapper);
335
+ messages.scrollTop = messages.scrollHeight;
336
+ }
337
+
338
+ form.addEventListener("submit", async (event) => {
339
+ event.preventDefault();
340
+ const message = textarea.value.trim();
341
+ if (!message) return;
342
+
343
+ addMessage("user", message);
344
+ textarea.value = "";
345
+ send.disabled = true;
346
+
347
+ try {
348
+ const response = await fetch("/v1/chat", {
349
+ method: "POST",
350
+ headers: { "content-type": "application/json" },
351
+ body: JSON.stringify({ message, conversationId }),
352
+ });
353
+ const payload = await response.json();
354
+
355
+ if (!response.ok) {
356
+ throw new Error(payload.error?.message || "Request failed");
357
+ }
358
+
359
+ conversationId = payload.conversationId || conversationId;
360
+ addMessage("assistant", payload.message?.content || "");
361
+ } catch (error) {
362
+ addMessage("error", error instanceof Error ? error.message : String(error), "error");
363
+ } finally {
364
+ send.disabled = false;
365
+ textarea.focus();
366
+ }
367
+ });
368
+ </script>
369
+ </body>
370
+ </html>`;
371
+ }
372
+
373
+ function escapeHtml(value: string): string {
374
+ return value
375
+ .replace(/&/g, "&amp;")
376
+ .replace(/</g, "&lt;")
377
+ .replace(/>/g, "&gt;")
378
+ .replace(/"/g, "&quot;")
379
+ .replace(/'/g, "&#39;");
380
+ }
381
+
382
+ function relativePath(root: string, path: string): string {
383
+ const output = relative(root, path);
384
+ return output.startsWith("..") ? path : output || ".";
385
+ }
@@ -0,0 +1,329 @@
1
+ import { findAgentCapsuleRoot, loadAgentCapsule } from "../runtime/config";
2
+ import { readLocalDeployStateIfExists } from "../runtime/core/deploy-state";
3
+ import { loadDeployReadinessContext, stableCloudProjectId, type DeployReadinessContext } from "../runtime/deploy-readiness";
4
+ import { loadCapsuleEnv } from "../runtime/env";
5
+ import { isAgentKitError } from "../runtime/errors";
6
+ import {
7
+ cloudApiRequest,
8
+ formatCloudAuthSource,
9
+ readCloudAuth,
10
+ type CloudCapabilitiesResponse,
11
+ type CloudLimitsResponse,
12
+ type CloudMeResponse,
13
+ type CloudSecretsResponse,
14
+ } from "./cloud-client";
15
+
16
+ type DeployReadinessStatus = "pass" | "fail" | "warn" | "info";
17
+
18
+ type DeployReadinessCheck = {
19
+ status: DeployReadinessStatus;
20
+ title: string;
21
+ message: string;
22
+ action?: string;
23
+ };
24
+
25
+ type DeployReadinessReport = {
26
+ ok: boolean;
27
+ apiUrl: string;
28
+ context: DeployReadinessContext;
29
+ checks: DeployReadinessCheck[];
30
+ };
31
+
32
+ export async function checkDeployReadiness(options: {
33
+ apiUrl: string;
34
+ requireLogin: boolean;
35
+ }): Promise<DeployReadinessReport> {
36
+ const context = await loadDeployReadinessContext(process.cwd(), process.env);
37
+ const auth = await readCloudAuth();
38
+ const checks: DeployReadinessCheck[] = [];
39
+ let cloudAuthUsable = false;
40
+
41
+ if (context.runtime !== "edge") {
42
+ checks.push({
43
+ status: "fail",
44
+ title: "Target runtime",
45
+ message: 'Cloudflare deploy requires runtime: "edge" in agentkit.config.ts.',
46
+ action: "set runtime: \"edge\" before deploying to AgentKit Cloud",
47
+ });
48
+ } else {
49
+ checks.push({
50
+ status: "pass",
51
+ title: "Target runtime",
52
+ message: "Capsule is configured for edge deploy.",
53
+ });
54
+ }
55
+
56
+ if (!auth?.token) {
57
+ checks.push({
58
+ status: options.requireLogin ? "fail" : "warn",
59
+ title: "Login",
60
+ message: options.requireLogin
61
+ ? "No AgentKit Cloud token was found for hosted production deploy."
62
+ : "No AgentKit Cloud token was found; deploy will request anonymous mode.",
63
+ action: "agentkit login --token <token>",
64
+ });
65
+ } else {
66
+ try {
67
+ const me = (await cloudApiRequest(options.apiUrl, "/v1/me", { method: "GET" })) as CloudMeResponse;
68
+ const email = me.account?.email;
69
+ checks.push({
70
+ status: "pass",
71
+ title: "Login",
72
+ message: email ? `Authenticated as ${email}.` : "AgentKit Cloud token is accepted.",
73
+ });
74
+ cloudAuthUsable = true;
75
+ } catch (error) {
76
+ checks.push({
77
+ status: "fail",
78
+ title: "Login",
79
+ message: `AgentKit Cloud rejected the token from ${formatCloudAuthSource(auth)}. ${formatReadinessError(error)}`,
80
+ action: "agentkit login --token <token>",
81
+ });
82
+ }
83
+ }
84
+
85
+ if (cloudAuthUsable) {
86
+ try {
87
+ const payload = (await cloudApiRequest(options.apiUrl, "/v1/me/capabilities", {
88
+ method: "GET",
89
+ })) as CloudCapabilitiesResponse;
90
+ const capabilities = payload.capabilities ?? [];
91
+ const hasCloudflareAlpha = capabilities.some(
92
+ (capability) => capability.name === "cloudflare_deploy_alpha" && capability.status === "active",
93
+ );
94
+
95
+ checks.push({
96
+ status: hasCloudflareAlpha ? "pass" : "fail",
97
+ title: "Deploy entitlement",
98
+ message: hasCloudflareAlpha
99
+ ? "cloudflare_deploy_alpha is active."
100
+ : "This account is missing the cloudflare_deploy_alpha entitlement.",
101
+ ...(hasCloudflareAlpha ? {} : { action: "log in with an invited AgentKit Cloud alpha token" }),
102
+ });
103
+ } catch (error) {
104
+ checks.push({
105
+ status: "fail",
106
+ title: "Deploy entitlement",
107
+ message: `Could not verify AgentKit Cloud capabilities. ${formatReadinessError(error)}`,
108
+ });
109
+ }
110
+
111
+ try {
112
+ const payload = (await cloudApiRequest(options.apiUrl, "/v1/me/limits", {
113
+ method: "GET",
114
+ })) as CloudLimitsResponse;
115
+ const account = payload.online_agents?.account;
116
+ const global = payload.online_agents?.global;
117
+ const accountLimit = normalizeOptionalNumber(account?.limit);
118
+ const accountUsed = normalizeOptionalNumber(account?.used);
119
+ const globalLimit = normalizeOptionalNumber(global?.limit);
120
+ const globalUsed = normalizeOptionalNumber(global?.used);
121
+
122
+ if (accountLimit !== undefined && accountUsed !== undefined && accountUsed >= accountLimit) {
123
+ checks.push({
124
+ status: "fail",
125
+ title: "Online agent capacity",
126
+ message: `This account is already using ${accountUsed}/${accountLimit} online AgentKit deploys.`,
127
+ action: "agentkit deploy pause",
128
+ });
129
+ } else if (globalLimit !== undefined && globalUsed !== undefined && globalUsed >= globalLimit) {
130
+ checks.push({
131
+ status: "fail",
132
+ title: "Online agent capacity",
133
+ message: `AgentKit Cloud is already using ${globalUsed}/${globalLimit} global online deploy slots.`,
134
+ action: "agentkit deploy status",
135
+ });
136
+ } else if (accountLimit !== undefined && accountUsed !== undefined) {
137
+ checks.push({
138
+ status: "pass",
139
+ title: "Online agent capacity",
140
+ message: `Account has capacity: ${accountUsed}/${accountLimit} online deploys are in use.`,
141
+ });
142
+ } else if (globalLimit !== undefined && globalUsed !== undefined) {
143
+ checks.push({
144
+ status: "pass",
145
+ title: "Online agent capacity",
146
+ message: `Global capacity is available: ${globalUsed}/${globalLimit} online deploys are in use.`,
147
+ });
148
+ } else {
149
+ checks.push({
150
+ status: "info",
151
+ title: "Online agent capacity",
152
+ message: "No online deploy limit is configured for this account.",
153
+ });
154
+ }
155
+ } catch (error) {
156
+ checks.push({
157
+ status: "warn",
158
+ title: "Online agent capacity",
159
+ message: `Could not verify online deploy capacity. ${formatReadinessError(error)}`,
160
+ });
161
+ }
162
+ }
163
+
164
+ if (context.agentKitManagedSecrets.length > 0) {
165
+ checks.push({
166
+ status: "info",
167
+ title: "AgentKit-managed secrets",
168
+ message: `${context.agentKitManagedSecrets.join(", ")} ${
169
+ context.agentKitManagedSecrets.length === 1 ? "is" : "are"
170
+ } provisioned by AgentKit Cloud during deploy.`,
171
+ });
172
+ }
173
+
174
+ if (context.accessMode === "private") {
175
+ checks.push({
176
+ status: "info",
177
+ title: "Private access",
178
+ message: "AGENTKIT_PRIVATE_TOKEN is required at runtime; AgentKit Cloud generates it and saves it in .agentkit/deploy.json.",
179
+ });
180
+ }
181
+
182
+ if (context.userManagedSecrets.length === 0) {
183
+ checks.push({
184
+ status: "pass",
185
+ title: "Hosted secrets",
186
+ message: "No user-managed hosted secrets are required by this capsule.",
187
+ });
188
+ } else if (!cloudAuthUsable) {
189
+ checks.push({
190
+ status: options.requireLogin ? "fail" : "warn",
191
+ title: "Hosted secrets",
192
+ message: `Could not check hosted secrets without a valid AgentKit Cloud login. Required: ${context.userManagedSecrets.join(", ")}.`,
193
+ action: "agentkit secret list",
194
+ });
195
+ } else {
196
+ try {
197
+ const payload = (await cloudApiRequest(
198
+ options.apiUrl,
199
+ `/v1/projects/${encodeURIComponent(context.projectId)}/secrets`,
200
+ { method: "GET" },
201
+ )) as CloudSecretsResponse;
202
+ const hostedSecretNames = new Set((payload.secrets ?? []).map((secret) => secret.name).filter(isString));
203
+ const missing = context.userManagedSecrets.filter((name) => !hostedSecretNames.has(name));
204
+
205
+ if (missing.length === 0) {
206
+ checks.push({
207
+ status: "pass",
208
+ title: "Hosted secrets",
209
+ message: "All user-managed hosted secrets are set.",
210
+ });
211
+ } else {
212
+ for (const secret of missing) {
213
+ const localSource = context.localSecrets[secret];
214
+ checks.push({
215
+ status: "fail",
216
+ title: `Hosted secret ${secret}`,
217
+ message: localSource
218
+ ? `${secret} is present locally but has not been set in AgentKit Cloud.`
219
+ : `${secret} is required but missing from AgentKit Cloud.`,
220
+ action: secretSetCommand(secret, localSource),
221
+ });
222
+ }
223
+ }
224
+ } catch (error) {
225
+ checks.push({
226
+ status: "fail",
227
+ title: "Hosted secrets",
228
+ message: `Could not list hosted secrets for project ${context.projectId}. ${formatReadinessError(error)}`,
229
+ action: "agentkit secret list",
230
+ });
231
+ }
232
+ }
233
+
234
+ return {
235
+ ok: checks.every((check) => check.status !== "fail"),
236
+ apiUrl: options.apiUrl,
237
+ context,
238
+ checks,
239
+ };
240
+ }
241
+
242
+ export function printDeployReadinessReport(report: DeployReadinessReport): void {
243
+ console.log("AgentKit deploy readiness");
244
+ console.log(`Project: ${report.context.projectName}`);
245
+ console.log(`Project ID: ${report.context.projectId}`);
246
+ console.log(`Target: ${report.context.target}`);
247
+ console.log(`API: ${report.apiUrl}`);
248
+ console.log("");
249
+
250
+ for (const check of report.checks) {
251
+ console.log(`[${check.status}] ${check.title}: ${check.message}`);
252
+
253
+ if (check.action) {
254
+ console.log(` Run: ${check.action}`);
255
+ }
256
+ }
257
+ }
258
+
259
+ export async function syncHostedSecretsFromLocal(options: { apiUrl: string; projectId: string }): Promise<void> {
260
+ const context = await loadDeployReadinessContext(process.cwd(), process.env);
261
+ const localEnv = await loadCapsuleEnv(context.root, {});
262
+ let synced = 0;
263
+ let skipped = 0;
264
+
265
+ for (const secret of context.userManagedSecrets) {
266
+ const value = localEnv[secret];
267
+
268
+ if (value === undefined) {
269
+ skipped += 1;
270
+ console.log(`${secret}\tskipped_missing_local`);
271
+ continue;
272
+ }
273
+
274
+ await cloudApiRequest(options.apiUrl, `/v1/projects/${encodeURIComponent(options.projectId)}/secrets/${encodeURIComponent(secret)}`, {
275
+ method: "PUT",
276
+ body: JSON.stringify({ value }),
277
+ });
278
+ synced += 1;
279
+ console.log(`${secret}\tsynced`);
280
+ }
281
+
282
+ if (context.userManagedSecrets.length === 0) {
283
+ console.log("No user-managed hosted secrets are declared by this capsule.");
284
+ return;
285
+ }
286
+
287
+ console.log("");
288
+ console.log(`${synced} synced, ${skipped} skipped`);
289
+ }
290
+
291
+ export function formatReadinessError(error: unknown): string {
292
+ if (isAgentKitError(error)) {
293
+ return `${error.code}: ${error.message}`;
294
+ }
295
+
296
+ return error instanceof Error ? error.message : String(error);
297
+ }
298
+
299
+ export async function resolveProjectIdForSecretCommand(cwd: string): Promise<string> {
300
+ const state = await readLocalDeployStateIfExists(cwd);
301
+
302
+ if (state?.project_id) {
303
+ return state.project_id;
304
+ }
305
+
306
+ const root = await findAgentCapsuleRoot(cwd);
307
+ const capsule = await loadAgentCapsule(root);
308
+ return stableCloudProjectId(capsule.config.name);
309
+ }
310
+
311
+ function secretSetCommand(secret: string, source: DeployReadinessContext["localSecrets"][string] | undefined): string {
312
+ if (source?.dotenv) {
313
+ return `agentkit secret set ${secret} --from-local-env`;
314
+ }
315
+
316
+ if (source?.environment) {
317
+ return `agentkit secret set ${secret} --from-env`;
318
+ }
319
+
320
+ return `agentkit secret set ${secret} --stdin`;
321
+ }
322
+
323
+ function normalizeOptionalNumber(value: number | null | undefined): number | undefined {
324
+ return typeof value === "number" && Number.isFinite(value) ? value : undefined;
325
+ }
326
+
327
+ function isString(value: unknown): value is string {
328
+ return typeof value === "string";
329
+ }