@runtypelabs/cli 2.59.1 → 2.59.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/dist/chunk-0x7UKC1U.js +17 -0
- package/dist/chunk-2ySxXtcg.js +418 -0
- package/dist/chunk-3An_BRHv.js +501 -0
- package/dist/chunk-6uTxgdiY.js +16218 -0
- package/dist/chunk-87nRnNRG.js +1363 -0
- package/dist/chunk-B3wo8uRx.js +322 -0
- package/dist/chunk-BGp3ZdBy.js +560 -0
- package/dist/chunk-BJQv9Elp.js +107 -0
- package/dist/chunk-BOpO6xE-.js +6053 -0
- package/dist/chunk-BQXXCL8B.js +102 -0
- package/dist/chunk-BV-8isOd.js +34 -0
- package/dist/chunk-BVMhjGVe.js +826 -0
- package/dist/chunk-BW2fg46T.js +141 -0
- package/dist/chunk-BWJdY5Ag.js +36 -0
- package/dist/chunk-BYJOrLhh.js +302 -0
- package/dist/chunk-BYab8EVL.js +127 -0
- package/dist/chunk-BfEKvp7h.js +82 -0
- package/dist/chunk-BhgLSfH6.js +1123 -0
- package/dist/chunk-BirUxTtH.js +296 -0
- package/dist/chunk-BmB6UVZ9.js +1559 -0
- package/dist/chunk-BnWhIqW1.js +109 -0
- package/dist/chunk-Bpfnfk0t.js +123 -0
- package/dist/chunk-BrhgdnM3.js +108 -0
- package/dist/chunk-Bt0XAgX5.js +327 -0
- package/dist/chunk-Bu0FJr6S.js +84 -0
- package/dist/chunk-BvEoKpN3.js +255 -0
- package/dist/chunk-BxNJgYbE.js +28 -0
- package/dist/chunk-C1OFhd7F.js +6131 -0
- package/dist/chunk-C7e7WwbU.js +331 -0
- package/dist/chunk-CDTYP6bN.js +558 -0
- package/dist/chunk-CGDQoqDC.js +24 -0
- package/dist/chunk-CJraEBVd.js +127 -0
- package/dist/chunk-CQrNhOtv.js +40 -0
- package/dist/chunk-CW6AzBH6.js +128 -0
- package/dist/chunk-CXgL4w-9.js +317 -0
- package/dist/chunk-CZrqoPyf.js +233 -0
- package/dist/chunk-CcFCyqbB.js +83 -0
- package/dist/chunk-CdaIvO-y.js +485 -0
- package/dist/chunk-CehSXtgK.js +624 -0
- package/dist/chunk-Ciqkop65.js +218 -0
- package/dist/chunk-CoAWdSiE.js +43 -0
- package/dist/chunk-Crb4y7t4.js +830 -0
- package/dist/chunk-CrlqIfmb.js +337 -0
- package/dist/chunk-Cs4pwCQ-.js +324 -0
- package/dist/chunk-CwM_9y01.js +519 -0
- package/dist/chunk-CyRbIJg8.js +440 -0
- package/dist/chunk-D0OHsyMy.js +212 -0
- package/dist/chunk-D3y2bURs.js +1347 -0
- package/dist/chunk-D4_mJk03.js +2338 -0
- package/dist/chunk-D6C8rned.js +578 -0
- package/dist/chunk-D9ZbUnks.js +11 -0
- package/dist/chunk-DEPmLshX.js +834 -0
- package/dist/chunk-DFj1UPB2.js +330 -0
- package/dist/chunk-DGdFdZVf.js +197 -0
- package/dist/chunk-DGv7X9S_.js +63 -0
- package/dist/chunk-DJQ47WYZ.js +664 -0
- package/dist/chunk-DJw3O61j.js +1974 -0
- package/dist/chunk-DK4FbWVK.js +100 -0
- package/dist/chunk-DPuofXMp.js +3907 -0
- package/dist/chunk-DSRYhUus.js +993 -0
- package/dist/chunk-DajZ9Eg4.js +299 -0
- package/dist/chunk-Df4dunvU.js +195 -0
- package/dist/chunk-DjNTPhfO.js +481 -0
- package/dist/chunk-Dn8eaJuu.js +1042 -0
- package/dist/chunk-DwDWqSxM.js +637 -0
- package/dist/chunk-Dz7cEtxl.js +84 -0
- package/dist/chunk-E_0bgjrS.js +359 -0
- package/dist/chunk-KPYN2vUp.js +776 -0
- package/dist/chunk-KcqPQJBi.js +86 -0
- package/dist/chunk-N9H0JnoG.js +349 -0
- package/dist/chunk-NONFcXkn.js +728 -0
- package/dist/chunk-PrFjcpmH.js +256 -0
- package/dist/chunk-QSfNYiw9.js +11 -0
- package/dist/chunk-R265bzj9.js +226 -0
- package/dist/chunk-X9V_r6jM.js +166 -0
- package/dist/chunk-ZBtPUtyA.js +55 -0
- package/dist/chunk-ZCTfmOs7.js +4939 -0
- package/dist/chunk-h6oavj0t.js +97 -0
- package/dist/chunk-i7DwkvvD.js +42 -0
- package/dist/chunk-mG-4eZ6S.js +203 -0
- package/dist/chunk-o0ygUbCf.js +313 -0
- package/dist/chunk-ucu5lrDZ.js +5715 -0
- package/dist/index.js +11367 -73789
- package/package.json +5 -5
|
@@ -0,0 +1,1363 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { I as __exportAll } from "./index.js";
|
|
3
|
+
import { i as extractSecretReferencesFromAnyValue } from "./chunk-BVMhjGVe.js";
|
|
4
|
+
import { n as resolveModelSecretNames, t as resolveModelOptionalSettings } from "./chunk-Bpfnfk0t.js";
|
|
5
|
+
import { r as createCliClient } from "./chunk-BW2fg46T.js";
|
|
6
|
+
import { n as ensureAuth } from "./chunk-CJraEBVd.js";
|
|
7
|
+
import { Command } from "commander";
|
|
8
|
+
import chalk from "chalk";
|
|
9
|
+
import * as fs$1 from "node:fs";
|
|
10
|
+
import * as path$1 from "node:path";
|
|
11
|
+
import { RuntypeApiError } from "@runtypelabs/sdk";
|
|
12
|
+
// src/commands/deploy.ts
|
|
13
|
+
var deploy_exports = /* @__PURE__ */ __exportAll({
|
|
14
|
+
BYOC_PLAN_REQUIRED_CODE: () => BYOC_PLAN_REQUIRED_CODE,
|
|
15
|
+
RETIRED_DEPLOY_FLOW_MESSAGE: () => RETIRED_DEPLOY_FLOW_MESSAGE,
|
|
16
|
+
TELEMETRY_ENV_TABLE: () => TELEMETRY_ENV_TABLE,
|
|
17
|
+
agentReleaseSelectorParams: () => agentReleaseSelectorParams,
|
|
18
|
+
collectDeploymentSecretInventory: () => collectDeploymentSecretInventory,
|
|
19
|
+
deployCommand: () => deployCommand,
|
|
20
|
+
describeAgentExportFailure: () => describeAgentExportFailure,
|
|
21
|
+
formatOptionalProviderSettings: () => formatOptionalProviderSettings,
|
|
22
|
+
readme: () => readme,
|
|
23
|
+
serverTs: () => serverTs,
|
|
24
|
+
setupSh: () => setupSh,
|
|
25
|
+
vercelReadme: () => vercelReadme,
|
|
26
|
+
vercelRouteTs: () => vercelRouteTs,
|
|
27
|
+
vercelSetupSh: () => vercelSetupSh,
|
|
28
|
+
workerIndexTs: () => workerIndexTs,
|
|
29
|
+
workerReadme: () => workerReadme,
|
|
30
|
+
workerSetupSh: () => workerSetupSh,
|
|
31
|
+
wranglerToml: () => wranglerToml
|
|
32
|
+
});
|
|
33
|
+
function bashSingleQuote(s) {
|
|
34
|
+
return "'" + s.replace(/'/g, "'\\''") + "'";
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Renders the `telemetry` block every scaffold passes to `createRuntime`.
|
|
38
|
+
* Reporting turns on as soon as an API key is present, and each switch is a
|
|
39
|
+
* plain environment variable so an operator can flip it without editing code.
|
|
40
|
+
* `envAccess` is `process.env` on Node hosts and `env` inside a Worker.
|
|
41
|
+
*/
|
|
42
|
+
function telemetryConfigBlock(envAccess, indent) {
|
|
43
|
+
const inner = `${indent} `;
|
|
44
|
+
return [
|
|
45
|
+
`${indent}telemetry: {`,
|
|
46
|
+
`${inner}enabled: ${envAccess}.RUNTYPE_TELEMETRY !== 'off' && Boolean(${envAccess}.RUNTYPE_API_KEY),`,
|
|
47
|
+
`${inner}apiKey: ${envAccess}.RUNTYPE_API_KEY,`,
|
|
48
|
+
`${inner}baseUrl: ${envAccess}.RUNTYPE_API_URL,`,
|
|
49
|
+
`${inner}content: ${envAccess}.RUNTYPE_TELEMETRY_CONTENT !== 'off',`,
|
|
50
|
+
`${indent}},`
|
|
51
|
+
].join("\n");
|
|
52
|
+
}
|
|
53
|
+
/** The four telemetry environment variables, documented once for every README. */
|
|
54
|
+
const TELEMETRY_ENV_TABLE = `| Variable | Effect |
|
|
55
|
+
| --- | --- |
|
|
56
|
+
| \`RUNTYPE_API_KEY\` | Turns reporting on. Use an API key with the \`TELEMETRY:WRITE\` scope. |
|
|
57
|
+
| \`RUNTYPE_TELEMETRY=off\` | Stops reporting even when the key is set. |
|
|
58
|
+
| \`RUNTYPE_TELEMETRY_CONTENT=off\` | Reports runs without messages, tool arguments, or tool results. |
|
|
59
|
+
| \`RUNTYPE_API_URL\` | Overrides the Runtype API origin. Defaults to \`https://api.runtype.com\`. |`;
|
|
60
|
+
/**
|
|
61
|
+
* README section describing the telemetry lane. `setCommand` is the host's own
|
|
62
|
+
* way of setting the key, so the reader never has to translate the instruction.
|
|
63
|
+
*/
|
|
64
|
+
function telemetryReadmeSection(setCommand) {
|
|
65
|
+
return `## Telemetry
|
|
66
|
+
|
|
67
|
+
Set \`RUNTYPE_API_KEY\` and this deployment reports every run to Runtype over
|
|
68
|
+
OpenTelemetry, so each run appears in the dashboard with its trace tree,
|
|
69
|
+
transcript, tool calls, token usage, and cost estimate.
|
|
70
|
+
|
|
71
|
+
\`\`\`bash
|
|
72
|
+
${setCommand}
|
|
73
|
+
\`\`\`
|
|
74
|
+
|
|
75
|
+
${TELEMETRY_ENV_TABLE}
|
|
76
|
+
`;
|
|
77
|
+
}
|
|
78
|
+
function serverTs() {
|
|
79
|
+
return `/**
|
|
80
|
+
* Generated by \`runtype deploy\`.
|
|
81
|
+
*
|
|
82
|
+
* Boots a Hono server that loads every agent JSON file under ./agents/ and
|
|
83
|
+
* registers a streaming POST /agent/:name endpoint for each.
|
|
84
|
+
*
|
|
85
|
+
* See README.md for startup and deploy instructions.
|
|
86
|
+
*/
|
|
87
|
+
import { readFile, readdir } from 'node:fs/promises'
|
|
88
|
+
import { resolve } from 'node:path'
|
|
89
|
+
import { Hono } from 'hono'
|
|
90
|
+
import { cors } from 'hono/cors'
|
|
91
|
+
import { serve } from '@hono/node-server'
|
|
92
|
+
import {
|
|
93
|
+
CloudRunAdapter,
|
|
94
|
+
createRuntime,
|
|
95
|
+
exportedAgentSchema,
|
|
96
|
+
type ExportedAgent,
|
|
97
|
+
} from '@runtypelabs/runtime'
|
|
98
|
+
|
|
99
|
+
async function loadAgent(filePath: string): Promise<ExportedAgent> {
|
|
100
|
+
const raw = await readFile(filePath, 'utf-8')
|
|
101
|
+
return exportedAgentSchema.parse(JSON.parse(raw))
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
async function loadAgentsDir(dir: string): Promise<Record<string, ExportedAgent>> {
|
|
105
|
+
let entries: string[]
|
|
106
|
+
try {
|
|
107
|
+
entries = await readdir(dir)
|
|
108
|
+
} catch (err) {
|
|
109
|
+
throw new Error(
|
|
110
|
+
\`[runtype] could not read agents directory at \${dir}: \${err instanceof Error ? err.message : String(err)}\`
|
|
111
|
+
)
|
|
112
|
+
}
|
|
113
|
+
const jsonFiles = entries.filter((f) => f.endsWith('.json')).sort()
|
|
114
|
+
if (jsonFiles.length === 0) {
|
|
115
|
+
throw new Error(\`[runtype] no agents found in \${dir} (expected one or more *.json files)\`)
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
const agents: Record<string, ExportedAgent> = {}
|
|
119
|
+
for (const file of jsonFiles) {
|
|
120
|
+
const agent = await loadAgent(resolve(dir, file))
|
|
121
|
+
if (agents[agent.name]) {
|
|
122
|
+
throw new Error(
|
|
123
|
+
\`[runtype] duplicate agent name "\${agent.name}" in \${file} — already defined by an earlier file\`
|
|
124
|
+
)
|
|
125
|
+
}
|
|
126
|
+
agents[agent.name] = agent
|
|
127
|
+
console.log(\`[runtype] loaded agent id=\${agent.id} name=\${agent.name}\`)
|
|
128
|
+
}
|
|
129
|
+
return agents
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
async function main(): Promise<void> {
|
|
133
|
+
const agentsDir = resolve(process.cwd(), 'agents')
|
|
134
|
+
const agents = await loadAgentsDir(agentsDir)
|
|
135
|
+
|
|
136
|
+
const runtime = createRuntime({
|
|
137
|
+
agents,
|
|
138
|
+
keys: { mode: 'own' },
|
|
139
|
+
adapter: new CloudRunAdapter(),
|
|
140
|
+
${telemetryConfigBlock("process.env", " ")}
|
|
141
|
+
})
|
|
142
|
+
|
|
143
|
+
const app = new Hono()
|
|
144
|
+
|
|
145
|
+
const allowedOriginsEnv = process.env.ALLOWED_ORIGINS?.trim()
|
|
146
|
+
const allowedOrigins = allowedOriginsEnv
|
|
147
|
+
? allowedOriginsEnv.split(',').map((o) => o.trim()).filter(Boolean)
|
|
148
|
+
: '*'
|
|
149
|
+
app.use(
|
|
150
|
+
'*',
|
|
151
|
+
cors({
|
|
152
|
+
origin: allowedOrigins,
|
|
153
|
+
allowMethods: ['GET', 'POST', 'OPTIONS'],
|
|
154
|
+
allowHeaders: ['Content-Type', 'Authorization'],
|
|
155
|
+
credentials: false,
|
|
156
|
+
maxAge: 86400,
|
|
157
|
+
})
|
|
158
|
+
)
|
|
159
|
+
|
|
160
|
+
app.get('/health', (c) => c.json({ status: 'ok', agents: Object.keys(agents) }))
|
|
161
|
+
|
|
162
|
+
app.post('/agent/:name', async (c) => {
|
|
163
|
+
const name = c.req.param('name')
|
|
164
|
+
if (!runtime.getAgent(name)) {
|
|
165
|
+
return c.json({ error: \`unknown agent: \${name}\` }, 404)
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
let body: { messages?: Array<{ role: string; content: string }> } = {}
|
|
169
|
+
try {
|
|
170
|
+
body = (await c.req.json()) as typeof body
|
|
171
|
+
} catch {
|
|
172
|
+
return c.json({ error: 'request body must be JSON' }, 400)
|
|
173
|
+
}
|
|
174
|
+
const messages = Array.isArray(body.messages) ? body.messages : []
|
|
175
|
+
|
|
176
|
+
try {
|
|
177
|
+
const response = await runtime.executeAgent(name, {
|
|
178
|
+
messages: messages as Array<{ role: 'system' | 'user' | 'assistant'; content: string }>,
|
|
179
|
+
signal: c.req.raw.signal,
|
|
180
|
+
})
|
|
181
|
+
return response
|
|
182
|
+
} catch (err) {
|
|
183
|
+
return c.json({ error: err instanceof Error ? err.message : 'unknown error' }, 500)
|
|
184
|
+
}
|
|
185
|
+
})
|
|
186
|
+
|
|
187
|
+
const port = Number(process.env.PORT ?? 8080)
|
|
188
|
+
serve({ fetch: app.fetch, port, hostname: '0.0.0.0' })
|
|
189
|
+
console.log(\`[runtype] listening on 0.0.0.0:\${port} — agents: \${Object.keys(agents).join(', ')}\`)
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
main().catch((err) => {
|
|
193
|
+
console.error('[runtype] fatal: failed to start server', err)
|
|
194
|
+
process.exit(1)
|
|
195
|
+
})
|
|
196
|
+
`;
|
|
197
|
+
}
|
|
198
|
+
function dockerfile() {
|
|
199
|
+
return `FROM node:22-bookworm-slim AS builder
|
|
200
|
+
|
|
201
|
+
WORKDIR /app
|
|
202
|
+
|
|
203
|
+
COPY package.json ./
|
|
204
|
+
COPY packages ./packages
|
|
205
|
+
RUN npm install --omit=dev --no-audit --no-fund
|
|
206
|
+
|
|
207
|
+
COPY tsconfig.json server.ts ./
|
|
208
|
+
COPY agents ./agents
|
|
209
|
+
RUN npm install --no-save --no-audit --no-fund typescript @types/node \\
|
|
210
|
+
&& npx tsc -p tsconfig.json
|
|
211
|
+
|
|
212
|
+
FROM node:22-bookworm-slim AS runner
|
|
213
|
+
|
|
214
|
+
WORKDIR /app
|
|
215
|
+
ENV NODE_ENV=production
|
|
216
|
+
ENV PORT=8080
|
|
217
|
+
EXPOSE 8080
|
|
218
|
+
|
|
219
|
+
COPY --from=builder /app/node_modules ./node_modules
|
|
220
|
+
COPY --from=builder /app/dist ./dist
|
|
221
|
+
COPY --from=builder /app/agents ./agents
|
|
222
|
+
COPY --from=builder /app/package.json ./package.json
|
|
223
|
+
|
|
224
|
+
CMD ["node", "dist/server.js"]
|
|
225
|
+
`;
|
|
226
|
+
}
|
|
227
|
+
function dockerignore() {
|
|
228
|
+
return `node_modules
|
|
229
|
+
dist
|
|
230
|
+
*.log
|
|
231
|
+
`;
|
|
232
|
+
}
|
|
233
|
+
function tsconfigJson() {
|
|
234
|
+
return JSON.stringify({
|
|
235
|
+
compilerOptions: {
|
|
236
|
+
target: "ES2022",
|
|
237
|
+
module: "ESNext",
|
|
238
|
+
moduleResolution: "bundler",
|
|
239
|
+
lib: ["ES2022", "DOM"],
|
|
240
|
+
strict: true,
|
|
241
|
+
esModuleInterop: true,
|
|
242
|
+
forceConsistentCasingInFileNames: true,
|
|
243
|
+
resolveJsonModule: true,
|
|
244
|
+
skipLibCheck: true,
|
|
245
|
+
outDir: "dist",
|
|
246
|
+
declaration: false,
|
|
247
|
+
sourceMap: true,
|
|
248
|
+
noEmit: false
|
|
249
|
+
},
|
|
250
|
+
include: ["*.ts"],
|
|
251
|
+
exclude: ["node_modules", "dist"]
|
|
252
|
+
}, null, 2);
|
|
253
|
+
}
|
|
254
|
+
function packageJson(name, runtimeDep) {
|
|
255
|
+
return JSON.stringify({
|
|
256
|
+
name,
|
|
257
|
+
version: "0.0.0",
|
|
258
|
+
private: true,
|
|
259
|
+
type: "module",
|
|
260
|
+
scripts: {
|
|
261
|
+
start: "node dist/server.js",
|
|
262
|
+
dev: "tsx server.ts",
|
|
263
|
+
build: "tsc -p tsconfig.json",
|
|
264
|
+
typecheck: "tsc -p tsconfig.json --noEmit"
|
|
265
|
+
},
|
|
266
|
+
dependencies: {
|
|
267
|
+
"@hono/node-server": "^2.0.0",
|
|
268
|
+
"@runtypelabs/runtime": runtimeDep,
|
|
269
|
+
hono: "^4.12.16"
|
|
270
|
+
},
|
|
271
|
+
devDependencies: {
|
|
272
|
+
"@types/node": "^22.10.5",
|
|
273
|
+
tsx: "^4.19.2",
|
|
274
|
+
typescript: "^5.3.3"
|
|
275
|
+
},
|
|
276
|
+
engines: { node: ">=22.0.0" }
|
|
277
|
+
}, null, 2);
|
|
278
|
+
}
|
|
279
|
+
function setupSh(monorepoRoot) {
|
|
280
|
+
return `#!/usr/bin/env bash
|
|
281
|
+
# Generated by \`runtype deploy\`.
|
|
282
|
+
#
|
|
283
|
+
# Builds and packs @runtypelabs/runtime from your local monorepo, then
|
|
284
|
+
# installs all dependencies so you can run \`npm start\` or deploy to
|
|
285
|
+
# Cloud Run with \`gcloud run deploy --source .\`.
|
|
286
|
+
#
|
|
287
|
+
# Usage (from this directory):
|
|
288
|
+
# ./setup.sh
|
|
289
|
+
# # then: npm run dev OR gcloud run deploy …
|
|
290
|
+
|
|
291
|
+
set -euo pipefail
|
|
292
|
+
|
|
293
|
+
DEPLOY_DIR="$(cd "$(dirname "\${BASH_SOURCE[0]}")" && pwd)"
|
|
294
|
+
MONOREPO_ROOT=${bashSingleQuote(monorepoRoot)}
|
|
295
|
+
TARBALL_DIR="\${DEPLOY_DIR}/packages"
|
|
296
|
+
|
|
297
|
+
echo "[setup] building @runtypelabs/runtime..."
|
|
298
|
+
pnpm --dir "\${MONOREPO_ROOT}" --filter @runtypelabs/runtime build
|
|
299
|
+
|
|
300
|
+
mkdir -p "\${TARBALL_DIR}"
|
|
301
|
+
|
|
302
|
+
echo "[setup] packing @runtypelabs/runtime tarball..."
|
|
303
|
+
pnpm --dir "\${MONOREPO_ROOT}/packages/runtime" pack \\
|
|
304
|
+
--pack-destination "\${TARBALL_DIR}"
|
|
305
|
+
|
|
306
|
+
RUNTIME_TGZ="$(ls "\${TARBALL_DIR}"/runtypelabs-runtime-*.tgz | head -n 1)"
|
|
307
|
+
if [ -z "\${RUNTIME_TGZ}" ]; then
|
|
308
|
+
echo "[setup] error: no tarball produced by pnpm pack" >&2
|
|
309
|
+
exit 1
|
|
310
|
+
fi
|
|
311
|
+
RUNTIME_TGZ_NAME="$(basename "\${RUNTIME_TGZ}")"
|
|
312
|
+
|
|
313
|
+
echo "[setup] updating package.json with file: dependency..."
|
|
314
|
+
node -e "
|
|
315
|
+
const fs = require('fs');
|
|
316
|
+
const pkg = JSON.parse(fs.readFileSync('\${DEPLOY_DIR}/package.json', 'utf8'));
|
|
317
|
+
pkg.dependencies['@runtypelabs/runtime'] = 'file:./packages/\${RUNTIME_TGZ_NAME}';
|
|
318
|
+
fs.writeFileSync('\${DEPLOY_DIR}/package.json', JSON.stringify(pkg, null, 2) + '\\\\n');
|
|
319
|
+
"
|
|
320
|
+
|
|
321
|
+
echo "[setup] installing dependencies..."
|
|
322
|
+
npm install --no-audit --no-fund
|
|
323
|
+
|
|
324
|
+
echo ""
|
|
325
|
+
echo "[setup] Done! Next steps:"
|
|
326
|
+
echo " npm run dev — start locally"
|
|
327
|
+
echo " npm run build — compile TypeScript"
|
|
328
|
+
echo " gcloud run deploy --source . --region us-central1 --allow-unauthenticated --port 8080"
|
|
329
|
+
echo ""
|
|
330
|
+
echo "[setup] Telemetry: export RUNTYPE_API_KEY=<key with TELEMETRY:WRITE> to report runs"
|
|
331
|
+
echo " RUNTYPE_TELEMETRY=off disable reporting"
|
|
332
|
+
echo " RUNTYPE_TELEMETRY_CONTENT=off report without messages, tool arguments, or results"
|
|
333
|
+
echo " RUNTYPE_API_URL=<origin> override https://api.runtype.com"
|
|
334
|
+
`;
|
|
335
|
+
}
|
|
336
|
+
function formatOptionalProviderSettings(settings) {
|
|
337
|
+
return settings.map((setting) => `- \`${setting.name}\` (default: \`${setting.defaultValue}\`)`).join("\n");
|
|
338
|
+
}
|
|
339
|
+
function optionalProviderSettingsSection(settings) {
|
|
340
|
+
if (settings.length === 0) return "";
|
|
341
|
+
return `
|
|
342
|
+
## Optional provider settings
|
|
343
|
+
|
|
344
|
+
These environment variables override runtime defaults and are not required:
|
|
345
|
+
|
|
346
|
+
${formatOptionalProviderSettings(settings)}
|
|
347
|
+
`;
|
|
348
|
+
}
|
|
349
|
+
function readme(agentNames, secretNames, optionalSettings) {
|
|
350
|
+
const agentList = agentNames.map((n) => ` - ${n}`).join("\n");
|
|
351
|
+
const secretList = secretNames.length > 0 ? secretNames.map((s) => ` gcloud secrets create ${s} --data-file=-`).join("\n") : " (none detected)";
|
|
352
|
+
return `# Runtype Deployment
|
|
353
|
+
|
|
354
|
+
Generated by \`runtype deploy\`. Contains the following agents:
|
|
355
|
+
|
|
356
|
+
${agentList}
|
|
357
|
+
|
|
358
|
+
## Setup
|
|
359
|
+
|
|
360
|
+
Run \`./setup.sh\` to build the runtime and install dependencies:
|
|
361
|
+
|
|
362
|
+
\`\`\`bash
|
|
363
|
+
chmod +x setup.sh
|
|
364
|
+
./setup.sh
|
|
365
|
+
\`\`\`
|
|
366
|
+
|
|
367
|
+
## Local development
|
|
368
|
+
|
|
369
|
+
\`\`\`bash
|
|
370
|
+
npm run dev
|
|
371
|
+
\`\`\`
|
|
372
|
+
|
|
373
|
+
Test with:
|
|
374
|
+
\`\`\`bash
|
|
375
|
+
curl -X POST http://localhost:8080/agent/${agentNames[0] ?? "my-agent"} \\
|
|
376
|
+
-H "Content-Type: application/json" \\
|
|
377
|
+
-d '{"messages":[{"role":"user","content":"Hello!"}]}'
|
|
378
|
+
\`\`\`
|
|
379
|
+
|
|
380
|
+
## Required secrets
|
|
381
|
+
|
|
382
|
+
Set these before deploying to Cloud Run:
|
|
383
|
+
|
|
384
|
+
${secretList}
|
|
385
|
+
|
|
386
|
+
${optionalProviderSettingsSection(optionalSettings)}
|
|
387
|
+
|
|
388
|
+
${telemetryReadmeSection("gcloud run services update runtype-deploy --update-secrets RUNTYPE_API_KEY=runtype-api-key:latest")}
|
|
389
|
+
|
|
390
|
+
## Deploy to Cloud Run
|
|
391
|
+
|
|
392
|
+
\`\`\`bash
|
|
393
|
+
gcloud run deploy runtype-deploy \\
|
|
394
|
+
--source . \\
|
|
395
|
+
--region us-central1 \\
|
|
396
|
+
--platform managed \\
|
|
397
|
+
--allow-unauthenticated \\
|
|
398
|
+
--port 8080
|
|
399
|
+
\`\`\`
|
|
400
|
+
|
|
401
|
+
## Health check
|
|
402
|
+
|
|
403
|
+
\`GET /health\` returns \`{"status":"ok","agents":[...]}\`.
|
|
404
|
+
`;
|
|
405
|
+
}
|
|
406
|
+
/**
|
|
407
|
+
* Generates the Cloudflare Workers entry point.
|
|
408
|
+
* Agents are statically imported (no fs access in Workers); each deployed
|
|
409
|
+
* agent needs an explicit import statement so Wrangler bundles the JSON.
|
|
410
|
+
*/
|
|
411
|
+
function workerIndexTs(agentSlugs) {
|
|
412
|
+
return `/**
|
|
413
|
+
* Generated by \`runtype deploy --target cloudflare\`.
|
|
414
|
+
*
|
|
415
|
+
* Routes:
|
|
416
|
+
* GET /health — liveness probe (lists registered agent names)
|
|
417
|
+
* POST /agent/:name — SSE stream of the agent event schema
|
|
418
|
+
*
|
|
419
|
+
* Agent definitions are statically imported so Wrangler bundles them.
|
|
420
|
+
* To add a new agent:
|
|
421
|
+
* 1. Export it with \`runtype deploy --agent <id> --target cloudflare\`
|
|
422
|
+
* 2. Or drop a JSON file under agents/ and add an import + entry below
|
|
423
|
+
* 3. Run \`wrangler deploy\`
|
|
424
|
+
*/
|
|
425
|
+
import {
|
|
426
|
+
CloudflareAdapter,
|
|
427
|
+
createRuntime,
|
|
428
|
+
exportedAgentSchema,
|
|
429
|
+
type ExportedAgent,
|
|
430
|
+
} from '@runtypelabs/runtime'
|
|
431
|
+
|
|
432
|
+
// ---------------------------------------------------------------------------
|
|
433
|
+
// Agent registry — static imports bundled by Wrangler at deploy time
|
|
434
|
+
// ---------------------------------------------------------------------------
|
|
435
|
+
|
|
436
|
+
${agentSlugs.map((slug, i) => `import agent${i}Json from '../agents/${slug}.json'`).join("\n")}
|
|
437
|
+
|
|
438
|
+
function parseAgent(json: unknown, filename: string): ExportedAgent {
|
|
439
|
+
try {
|
|
440
|
+
return exportedAgentSchema.parse(json)
|
|
441
|
+
} catch (err) {
|
|
442
|
+
throw new Error(
|
|
443
|
+
\`[runtype] invalid agent JSON in \${filename}: \${
|
|
444
|
+
err instanceof Error ? err.message : String(err)
|
|
445
|
+
}\`
|
|
446
|
+
)
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
const agentFiles: Array<[string, unknown]> = [
|
|
451
|
+
${agentSlugs.map((slug, i) => ` ['${slug}.json', agent${i}Json],`).join("\n")}
|
|
452
|
+
]
|
|
453
|
+
|
|
454
|
+
const agentRegistry: Record<string, ExportedAgent> = {}
|
|
455
|
+
for (const [filename, json] of agentFiles) {
|
|
456
|
+
const agent = parseAgent(json, filename)
|
|
457
|
+
if (agentRegistry[agent.name]) {
|
|
458
|
+
throw new Error(\`[runtype] duplicate agent name "\${agent.name}" in \${filename}\`)
|
|
459
|
+
}
|
|
460
|
+
agentRegistry[agent.name] = agent
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
// ---------------------------------------------------------------------------
|
|
464
|
+
// Env — Wrangler injects secrets + bindings here
|
|
465
|
+
// ---------------------------------------------------------------------------
|
|
466
|
+
|
|
467
|
+
export interface Env {
|
|
468
|
+
// Secrets — set via \`wrangler secret put <NAME>\`
|
|
469
|
+
ANTHROPIC_API_KEY?: string
|
|
470
|
+
OPENAI_API_KEY?: string
|
|
471
|
+
GOOGLE_API_KEY?: string
|
|
472
|
+
MIXLAYER_API_KEY?: string
|
|
473
|
+
// Optional: narrow CORS from * to specific origins (comma-separated)
|
|
474
|
+
ALLOWED_ORIGINS?: string
|
|
475
|
+
// Optional: KV namespace binding for durable cross-request caching
|
|
476
|
+
CACHE_KV?: KVNamespace
|
|
477
|
+
// Telemetry: set RUNTYPE_API_KEY (scope TELEMETRY:WRITE) to report runs to Runtype
|
|
478
|
+
RUNTYPE_API_KEY?: string
|
|
479
|
+
// Optional: override the Runtype API origin (default https://api.runtype.com)
|
|
480
|
+
RUNTYPE_API_URL?: string
|
|
481
|
+
// Optional: set to "off" to stop reporting, or to report without conversation content
|
|
482
|
+
RUNTYPE_TELEMETRY?: string
|
|
483
|
+
RUNTYPE_TELEMETRY_CONTENT?: string
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
// ---------------------------------------------------------------------------
|
|
487
|
+
// Worker
|
|
488
|
+
// ---------------------------------------------------------------------------
|
|
489
|
+
|
|
490
|
+
export default {
|
|
491
|
+
async fetch(request: Request, env: Env, ctx: ExecutionContext): Promise<Response> {
|
|
492
|
+
const url = new URL(request.url)
|
|
493
|
+
const cors = corsHeaders(request, env.ALLOWED_ORIGINS)
|
|
494
|
+
|
|
495
|
+
if (request.method === 'OPTIONS') {
|
|
496
|
+
return new Response(null, { status: 204, headers: cors })
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
if (request.method === 'GET' && url.pathname === '/health') {
|
|
500
|
+
return json({ status: 'ok', agents: Object.keys(agentRegistry) }, 200, cors)
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
const agentMatch = url.pathname.match(/^\\/agent\\/([^/]+)$/)
|
|
504
|
+
if (request.method === 'POST' && agentMatch) {
|
|
505
|
+
const name = decodeURIComponent(agentMatch[1])
|
|
506
|
+
if (!agentRegistry[name]) {
|
|
507
|
+
return json({ error: \`unknown agent: \${name}\` }, 404, cors)
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
const runtime = createRuntime({
|
|
511
|
+
agents: agentRegistry,
|
|
512
|
+
keys: { mode: 'own' },
|
|
513
|
+
adapter: new CloudflareAdapter(env, {
|
|
514
|
+
kvNamespace: env.CACHE_KV,
|
|
515
|
+
waitUntil: ctx.waitUntil.bind(ctx),
|
|
516
|
+
}),
|
|
517
|
+
${telemetryConfigBlock("env", " ")}
|
|
518
|
+
})
|
|
519
|
+
|
|
520
|
+
let body: { messages?: Array<{ role: string; content: string }> } = {}
|
|
521
|
+
try {
|
|
522
|
+
body = (await request.json()) as typeof body
|
|
523
|
+
} catch {
|
|
524
|
+
return json({ error: 'request body must be JSON' }, 400, cors)
|
|
525
|
+
}
|
|
526
|
+
const messages = Array.isArray(body.messages) ? body.messages : []
|
|
527
|
+
|
|
528
|
+
try {
|
|
529
|
+
const response = await runtime.executeAgent(name, {
|
|
530
|
+
messages: messages as Array<{
|
|
531
|
+
role: 'system' | 'user' | 'assistant'
|
|
532
|
+
content: string
|
|
533
|
+
}>,
|
|
534
|
+
signal: request.signal,
|
|
535
|
+
})
|
|
536
|
+
const headers = new Headers(response.headers)
|
|
537
|
+
for (const [k, v] of Object.entries(cors)) headers.set(k, v)
|
|
538
|
+
return new Response(response.body, { status: response.status, headers })
|
|
539
|
+
} catch (err) {
|
|
540
|
+
return json({ error: err instanceof Error ? err.message : 'unknown error' }, 500, cors)
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
return json({ error: 'not found' }, 404, cors)
|
|
545
|
+
},
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
// ---------------------------------------------------------------------------
|
|
549
|
+
// Helpers
|
|
550
|
+
// ---------------------------------------------------------------------------
|
|
551
|
+
|
|
552
|
+
function json(body: unknown, status: number, extra: Record<string, string>): Response {
|
|
553
|
+
return new Response(JSON.stringify(body), {
|
|
554
|
+
status,
|
|
555
|
+
headers: { 'Content-Type': 'application/json', ...extra },
|
|
556
|
+
})
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
function corsHeaders(request: Request, allowedOriginsEnv?: string): Record<string, string> {
|
|
560
|
+
const origin = request.headers.get('Origin') ?? ''
|
|
561
|
+
let allow = '*'
|
|
562
|
+
if (allowedOriginsEnv) {
|
|
563
|
+
const list = allowedOriginsEnv.split(',').map((o) => o.trim()).filter(Boolean)
|
|
564
|
+
allow = list.includes(origin) ? origin : (list[0] ?? '*')
|
|
565
|
+
}
|
|
566
|
+
const headers: Record<string, string> = {
|
|
567
|
+
'Access-Control-Allow-Origin': allow,
|
|
568
|
+
'Access-Control-Allow-Methods': 'GET, POST, OPTIONS',
|
|
569
|
+
'Access-Control-Allow-Headers': 'Content-Type, Authorization',
|
|
570
|
+
'Access-Control-Max-Age': '86400',
|
|
571
|
+
}
|
|
572
|
+
if (allow !== '*') headers['Vary'] = 'Origin'
|
|
573
|
+
return headers
|
|
574
|
+
}
|
|
575
|
+
`;
|
|
576
|
+
}
|
|
577
|
+
function wranglerToml(projectName) {
|
|
578
|
+
return `name = "${projectName}"
|
|
579
|
+
main = "src/index.ts"
|
|
580
|
+
compatibility_date = "2025-01-01"
|
|
581
|
+
|
|
582
|
+
# Secrets — set via \`wrangler secret put <NAME>\`
|
|
583
|
+
# Run this for each API key your agents need:
|
|
584
|
+
# wrangler secret put ANTHROPIC_API_KEY
|
|
585
|
+
# wrangler secret put OPENAI_API_KEY
|
|
586
|
+
# wrangler secret put GOOGLE_API_KEY
|
|
587
|
+
# wrangler secret put MIXLAYER_API_KEY
|
|
588
|
+
|
|
589
|
+
# Telemetry: set the Runtype API key (scope \`TELEMETRY:WRITE\`) to report every
|
|
590
|
+
# run to Runtype over OpenTelemetry:
|
|
591
|
+
# wrangler secret put RUNTYPE_API_KEY
|
|
592
|
+
|
|
593
|
+
# Optional: KV namespace for durable cross-request caching.
|
|
594
|
+
# Without this the adapter uses a per-isolate in-memory Map (ephemeral).
|
|
595
|
+
# To enable:
|
|
596
|
+
# 1. wrangler kv namespace create CACHE_KV
|
|
597
|
+
# 2. Uncomment the block below and paste the generated id.
|
|
598
|
+
# [[kv_namespaces]]
|
|
599
|
+
# binding = "CACHE_KV"
|
|
600
|
+
# id = "<paste your KV namespace id here>"
|
|
601
|
+
|
|
602
|
+
# Optional: Workers AI binding (for @cf/* and workers-ai/* model ids).
|
|
603
|
+
# [ai]
|
|
604
|
+
# binding = "AI"
|
|
605
|
+
|
|
606
|
+
# CORS: narrow from * to specific origins via ALLOWED_ORIGINS env var.
|
|
607
|
+
# Telemetry switches: RUNTYPE_TELEMETRY = "off" stops reporting,
|
|
608
|
+
# RUNTYPE_TELEMETRY_CONTENT = "off" reports without messages, tool arguments or
|
|
609
|
+
# results, and RUNTYPE_API_URL overrides https://api.runtype.com.
|
|
610
|
+
# [vars]
|
|
611
|
+
# ALLOWED_ORIGINS = "https://yourapp.com,https://persona-chat.dev"
|
|
612
|
+
# RUNTYPE_TELEMETRY = "off"
|
|
613
|
+
# RUNTYPE_TELEMETRY_CONTENT = "off"
|
|
614
|
+
# RUNTYPE_API_URL = "https://api.runtype.com"
|
|
615
|
+
`;
|
|
616
|
+
}
|
|
617
|
+
function workerPackageJson(name, runtimeDep) {
|
|
618
|
+
return JSON.stringify({
|
|
619
|
+
name,
|
|
620
|
+
version: "0.0.0",
|
|
621
|
+
private: true,
|
|
622
|
+
scripts: {
|
|
623
|
+
dev: "wrangler dev",
|
|
624
|
+
deploy: "wrangler deploy",
|
|
625
|
+
typecheck: "tsc --noEmit"
|
|
626
|
+
},
|
|
627
|
+
dependencies: { "@runtypelabs/runtime": runtimeDep },
|
|
628
|
+
devDependencies: {
|
|
629
|
+
"@cloudflare/workers-types": "^5.0.0",
|
|
630
|
+
typescript: "^5.3.3",
|
|
631
|
+
wrangler: "^4.0.0"
|
|
632
|
+
}
|
|
633
|
+
}, null, 2);
|
|
634
|
+
}
|
|
635
|
+
function workerTsconfigJson() {
|
|
636
|
+
return JSON.stringify({
|
|
637
|
+
compilerOptions: {
|
|
638
|
+
target: "ES2022",
|
|
639
|
+
module: "ES2022",
|
|
640
|
+
moduleResolution: "bundler",
|
|
641
|
+
strict: true,
|
|
642
|
+
resolveJsonModule: true,
|
|
643
|
+
types: ["@cloudflare/workers-types"]
|
|
644
|
+
},
|
|
645
|
+
include: ["src"]
|
|
646
|
+
}, null, 2);
|
|
647
|
+
}
|
|
648
|
+
function workerSetupSh(monorepoRoot) {
|
|
649
|
+
return `#!/usr/bin/env bash
|
|
650
|
+
# Generated by \`runtype deploy --target cloudflare\`.
|
|
651
|
+
#
|
|
652
|
+
# Builds and packs @runtypelabs/runtime from your local monorepo, then
|
|
653
|
+
# installs all dependencies so you can run \`wrangler dev\` or deploy with
|
|
654
|
+
# \`wrangler deploy\`.
|
|
655
|
+
#
|
|
656
|
+
# Usage (from this directory):
|
|
657
|
+
# ./setup.sh
|
|
658
|
+
|
|
659
|
+
set -euo pipefail
|
|
660
|
+
|
|
661
|
+
DEPLOY_DIR="$(cd "$(dirname "\${BASH_SOURCE[0]}")" && pwd)"
|
|
662
|
+
MONOREPO_ROOT=${bashSingleQuote(monorepoRoot)}
|
|
663
|
+
TARBALL_DIR="\${DEPLOY_DIR}/packages"
|
|
664
|
+
|
|
665
|
+
echo "[setup] building @runtypelabs/runtime..."
|
|
666
|
+
pnpm --dir "\${MONOREPO_ROOT}" --filter @runtypelabs/runtime build
|
|
667
|
+
|
|
668
|
+
mkdir -p "\${TARBALL_DIR}"
|
|
669
|
+
|
|
670
|
+
echo "[setup] packing @runtypelabs/runtime tarball..."
|
|
671
|
+
pnpm --dir "\${MONOREPO_ROOT}/packages/runtime" pack \\
|
|
672
|
+
--pack-destination "\${TARBALL_DIR}"
|
|
673
|
+
|
|
674
|
+
RUNTIME_TGZ="$(ls "\${TARBALL_DIR}"/runtypelabs-runtime-*.tgz | head -n 1)"
|
|
675
|
+
if [ -z "\${RUNTIME_TGZ}" ]; then
|
|
676
|
+
echo "[setup] error: no tarball produced by pnpm pack" >&2
|
|
677
|
+
exit 1
|
|
678
|
+
fi
|
|
679
|
+
RUNTIME_TGZ_NAME="$(basename "\${RUNTIME_TGZ}")"
|
|
680
|
+
|
|
681
|
+
echo "[setup] updating package.json with file: dependency..."
|
|
682
|
+
node -e "
|
|
683
|
+
const fs = require('fs');
|
|
684
|
+
const pkg = JSON.parse(fs.readFileSync('\${DEPLOY_DIR}/package.json', 'utf8'));
|
|
685
|
+
pkg.dependencies['@runtypelabs/runtime'] = 'file:./packages/\${RUNTIME_TGZ_NAME}';
|
|
686
|
+
fs.writeFileSync('\${DEPLOY_DIR}/package.json', JSON.stringify(pkg, null, 2) + '\\\\n');
|
|
687
|
+
"
|
|
688
|
+
|
|
689
|
+
echo "[setup] installing dependencies..."
|
|
690
|
+
npm install --no-audit --no-fund
|
|
691
|
+
|
|
692
|
+
echo ""
|
|
693
|
+
echo "[setup] Done! Next steps:"
|
|
694
|
+
echo " wrangler dev — preview locally"
|
|
695
|
+
echo " wrangler deploy — deploy to Cloudflare Workers"
|
|
696
|
+
echo ""
|
|
697
|
+
echo "[setup] Telemetry: wrangler secret put RUNTYPE_API_KEY (scope TELEMETRY:WRITE) to report runs"
|
|
698
|
+
echo " RUNTYPE_TELEMETRY=off disable reporting"
|
|
699
|
+
echo " RUNTYPE_TELEMETRY_CONTENT=off report without messages, tool arguments, or results"
|
|
700
|
+
echo " RUNTYPE_API_URL=<origin> override https://api.runtype.com"
|
|
701
|
+
`;
|
|
702
|
+
}
|
|
703
|
+
function workerReadme(agentNames, secretNames, optionalSettings) {
|
|
704
|
+
return `# Runtype Cloudflare Workers Deployment
|
|
705
|
+
|
|
706
|
+
Generated by \`runtype deploy --target cloudflare\`. Contains the following agents:
|
|
707
|
+
|
|
708
|
+
${agentNames.map((n) => ` - ${n}`).join("\n")}
|
|
709
|
+
|
|
710
|
+
## Setup
|
|
711
|
+
|
|
712
|
+
\`\`\`bash
|
|
713
|
+
chmod +x setup.sh && ./setup.sh # build runtime + install deps
|
|
714
|
+
\`\`\`
|
|
715
|
+
|
|
716
|
+
## Secrets
|
|
717
|
+
|
|
718
|
+
Set each API key your agents use:
|
|
719
|
+
|
|
720
|
+
\`\`\`bash
|
|
721
|
+
${secretNames.length > 0 ? secretNames.map((s) => ` wrangler secret put ${s}`).join("\n") : " (none detected)"}
|
|
722
|
+
\`\`\`
|
|
723
|
+
|
|
724
|
+
${optionalProviderSettingsSection(optionalSettings)}
|
|
725
|
+
|
|
726
|
+
${telemetryReadmeSection("wrangler secret put RUNTYPE_API_KEY")}
|
|
727
|
+
|
|
728
|
+
## Run locally
|
|
729
|
+
|
|
730
|
+
\`\`\`bash
|
|
731
|
+
wrangler dev
|
|
732
|
+
\`\`\`
|
|
733
|
+
|
|
734
|
+
## Deploy
|
|
735
|
+
|
|
736
|
+
\`\`\`bash
|
|
737
|
+
wrangler deploy
|
|
738
|
+
\`\`\`
|
|
739
|
+
|
|
740
|
+
## Health check
|
|
741
|
+
|
|
742
|
+
\`GET /health\` returns \`{"status":"ok","agents":[...]}\`.
|
|
743
|
+
|
|
744
|
+
## Call an agent
|
|
745
|
+
|
|
746
|
+
\`\`\`bash
|
|
747
|
+
curl https://<worker-subdomain>.workers.dev/agent/<name> \\
|
|
748
|
+
-H 'Content-Type: application/json' \\
|
|
749
|
+
-d '{"messages":[{"role":"user","content":"Hello"}]}'
|
|
750
|
+
\`\`\`
|
|
751
|
+
`;
|
|
752
|
+
}
|
|
753
|
+
/**
|
|
754
|
+
* Generates the Vercel catch-all API route.
|
|
755
|
+
* Uses Hono for routing. Agent JSONs are statically imported so Vercel
|
|
756
|
+
* bundles them into the function package at build time.
|
|
757
|
+
*/
|
|
758
|
+
function vercelRouteTs(agentSlugs) {
|
|
759
|
+
return `/**
|
|
760
|
+
* Generated by \`runtype deploy --target vercel\`.
|
|
761
|
+
*
|
|
762
|
+
* Catch-all Vercel Function. All traffic is rewritten here by vercel.json.
|
|
763
|
+
* Routes:
|
|
764
|
+
* GET /health — liveness probe (lists registered agent names)
|
|
765
|
+
* POST /agent/:name — SSE stream of the agent event schema
|
|
766
|
+
*
|
|
767
|
+
* Agent definitions are statically imported and bundled by Vercel.
|
|
768
|
+
* To add a new agent:
|
|
769
|
+
* 1. Export it with \`runtype deploy --agent <id> --target vercel\`
|
|
770
|
+
* 2. Or drop a JSON file under agents/ and add an import + entry below
|
|
771
|
+
* 3. Run \`vercel deploy\`
|
|
772
|
+
*/
|
|
773
|
+
import { Hono } from 'hono'
|
|
774
|
+
import { handle } from 'hono/vercel'
|
|
775
|
+
import { cors } from 'hono/cors'
|
|
776
|
+
import {
|
|
777
|
+
VercelAdapter,
|
|
778
|
+
createRuntime,
|
|
779
|
+
exportedAgentSchema,
|
|
780
|
+
type ExportedAgent,
|
|
781
|
+
} from '@runtypelabs/runtime'
|
|
782
|
+
|
|
783
|
+
// ---------------------------------------------------------------------------
|
|
784
|
+
// Agent registry — built at module load, cached across warm invocations
|
|
785
|
+
// ---------------------------------------------------------------------------
|
|
786
|
+
|
|
787
|
+
${agentSlugs.map((slug, i) => `import agent${i}Json from '../agents/${slug}.json'`).join("\n")}
|
|
788
|
+
|
|
789
|
+
function parseAgent(json: unknown, filename: string): ExportedAgent {
|
|
790
|
+
try {
|
|
791
|
+
return exportedAgentSchema.parse(json)
|
|
792
|
+
} catch (err) {
|
|
793
|
+
throw new Error(
|
|
794
|
+
\`[runtype] invalid agent JSON in \${filename}: \${
|
|
795
|
+
err instanceof Error ? err.message : String(err)
|
|
796
|
+
}\`
|
|
797
|
+
)
|
|
798
|
+
}
|
|
799
|
+
}
|
|
800
|
+
|
|
801
|
+
const agentFiles: Array<[string, unknown]> = [
|
|
802
|
+
${agentSlugs.map((slug, i) => ` ['${slug}.json', agent${i}Json],`).join("\n")}
|
|
803
|
+
]
|
|
804
|
+
|
|
805
|
+
const agentRegistry: Record<string, ExportedAgent> = {}
|
|
806
|
+
for (const [filename, json] of agentFiles) {
|
|
807
|
+
const agent = parseAgent(json, filename)
|
|
808
|
+
if (agentRegistry[agent.name]) {
|
|
809
|
+
throw new Error(\`[runtype] duplicate agent name "\${agent.name}" in \${filename}\`)
|
|
810
|
+
}
|
|
811
|
+
agentRegistry[agent.name] = agent
|
|
812
|
+
}
|
|
813
|
+
|
|
814
|
+
// ---------------------------------------------------------------------------
|
|
815
|
+
// Runtime — constructed at module scope (warm across invocations)
|
|
816
|
+
// ---------------------------------------------------------------------------
|
|
817
|
+
|
|
818
|
+
const runtime = createRuntime({
|
|
819
|
+
agents: agentRegistry,
|
|
820
|
+
keys: { mode: 'own' },
|
|
821
|
+
adapter: new VercelAdapter(),
|
|
822
|
+
${telemetryConfigBlock("process.env", " ")}
|
|
823
|
+
})
|
|
824
|
+
|
|
825
|
+
// ---------------------------------------------------------------------------
|
|
826
|
+
// Hono app
|
|
827
|
+
// ---------------------------------------------------------------------------
|
|
828
|
+
|
|
829
|
+
const app = new Hono()
|
|
830
|
+
|
|
831
|
+
const allowedOriginsEnv = process.env.ALLOWED_ORIGINS?.trim()
|
|
832
|
+
const allowedOrigins = allowedOriginsEnv
|
|
833
|
+
? allowedOriginsEnv.split(',').map((o) => o.trim()).filter(Boolean)
|
|
834
|
+
: '*'
|
|
835
|
+
|
|
836
|
+
app.use(
|
|
837
|
+
'*',
|
|
838
|
+
cors({
|
|
839
|
+
origin: allowedOrigins,
|
|
840
|
+
allowMethods: ['GET', 'POST', 'OPTIONS'],
|
|
841
|
+
allowHeaders: ['Content-Type', 'Authorization'],
|
|
842
|
+
maxAge: 86400,
|
|
843
|
+
})
|
|
844
|
+
)
|
|
845
|
+
|
|
846
|
+
app.get('/health', (c) => c.json({ status: 'ok', agents: Object.keys(agentRegistry) }))
|
|
847
|
+
|
|
848
|
+
app.post('/agent/:name', async (c) => {
|
|
849
|
+
const name = c.req.param('name')
|
|
850
|
+
if (!runtime.getAgent(name)) {
|
|
851
|
+
return c.json({ error: \`unknown agent: \${name}\` }, 404)
|
|
852
|
+
}
|
|
853
|
+
|
|
854
|
+
let body: { messages?: Array<{ role: string; content: string }> } = {}
|
|
855
|
+
try {
|
|
856
|
+
body = (await c.req.json()) as typeof body
|
|
857
|
+
} catch {
|
|
858
|
+
return c.json({ error: 'request body must be JSON' }, 400)
|
|
859
|
+
}
|
|
860
|
+
const messages = Array.isArray(body.messages) ? body.messages : []
|
|
861
|
+
|
|
862
|
+
try {
|
|
863
|
+
const response = await runtime.executeAgent(name, {
|
|
864
|
+
messages: messages as Array<{
|
|
865
|
+
role: 'system' | 'user' | 'assistant'
|
|
866
|
+
content: string
|
|
867
|
+
}>,
|
|
868
|
+
signal: c.req.raw.signal,
|
|
869
|
+
})
|
|
870
|
+
return response
|
|
871
|
+
} catch (err) {
|
|
872
|
+
return c.json({ error: err instanceof Error ? err.message : 'unknown error' }, 500)
|
|
873
|
+
}
|
|
874
|
+
})
|
|
875
|
+
|
|
876
|
+
export const GET = handle(app)
|
|
877
|
+
export const POST = handle(app)
|
|
878
|
+
`;
|
|
879
|
+
}
|
|
880
|
+
function vercelJson() {
|
|
881
|
+
return JSON.stringify({
|
|
882
|
+
rewrites: [{
|
|
883
|
+
source: "/(.*)",
|
|
884
|
+
destination: "/api/[[...route]]"
|
|
885
|
+
}],
|
|
886
|
+
functions: { "api/[[...route]].ts": { maxDuration: 60 } }
|
|
887
|
+
}, null, 2);
|
|
888
|
+
}
|
|
889
|
+
function vercelPackageJson(name, runtimeDep) {
|
|
890
|
+
return JSON.stringify({
|
|
891
|
+
name,
|
|
892
|
+
version: "0.0.0",
|
|
893
|
+
private: true,
|
|
894
|
+
scripts: {
|
|
895
|
+
dev: "vercel dev",
|
|
896
|
+
deploy: "vercel deploy",
|
|
897
|
+
typecheck: "tsc --noEmit"
|
|
898
|
+
},
|
|
899
|
+
dependencies: {
|
|
900
|
+
"@runtypelabs/runtime": runtimeDep,
|
|
901
|
+
hono: "^4.12.16"
|
|
902
|
+
},
|
|
903
|
+
devDependencies: {
|
|
904
|
+
"@types/node": "^22.0.0",
|
|
905
|
+
"@vercel/node": "^5.0.0",
|
|
906
|
+
typescript: "^5.3.3",
|
|
907
|
+
vercel: "^44.0.0"
|
|
908
|
+
}
|
|
909
|
+
}, null, 2);
|
|
910
|
+
}
|
|
911
|
+
function vercelTsconfigJson() {
|
|
912
|
+
return JSON.stringify({
|
|
913
|
+
compilerOptions: {
|
|
914
|
+
target: "ES2022",
|
|
915
|
+
module: "ESNext",
|
|
916
|
+
moduleResolution: "bundler",
|
|
917
|
+
strict: true,
|
|
918
|
+
resolveJsonModule: true,
|
|
919
|
+
esModuleInterop: true,
|
|
920
|
+
skipLibCheck: true
|
|
921
|
+
},
|
|
922
|
+
include: ["api"]
|
|
923
|
+
}, null, 2);
|
|
924
|
+
}
|
|
925
|
+
function vercelSetupSh(monorepoRoot) {
|
|
926
|
+
return `#!/usr/bin/env bash
|
|
927
|
+
# Generated by \`runtype deploy --target vercel\`.
|
|
928
|
+
#
|
|
929
|
+
# Builds and packs @runtypelabs/runtime from your local monorepo, then
|
|
930
|
+
# installs all dependencies so you can run \`vercel dev\` or deploy with
|
|
931
|
+
# \`vercel deploy\`.
|
|
932
|
+
#
|
|
933
|
+
# Usage (from this directory):
|
|
934
|
+
# ./setup.sh
|
|
935
|
+
|
|
936
|
+
set -euo pipefail
|
|
937
|
+
|
|
938
|
+
DEPLOY_DIR="$(cd "$(dirname "\${BASH_SOURCE[0]}")" && pwd)"
|
|
939
|
+
MONOREPO_ROOT=${bashSingleQuote(monorepoRoot)}
|
|
940
|
+
TARBALL_DIR="\${DEPLOY_DIR}/packages"
|
|
941
|
+
|
|
942
|
+
echo "[setup] building @runtypelabs/runtime..."
|
|
943
|
+
pnpm --dir "\${MONOREPO_ROOT}" --filter @runtypelabs/runtime build
|
|
944
|
+
|
|
945
|
+
mkdir -p "\${TARBALL_DIR}"
|
|
946
|
+
|
|
947
|
+
echo "[setup] packing @runtypelabs/runtime tarball..."
|
|
948
|
+
pnpm --dir "\${MONOREPO_ROOT}/packages/runtime" pack \\
|
|
949
|
+
--pack-destination "\${TARBALL_DIR}"
|
|
950
|
+
|
|
951
|
+
RUNTIME_TGZ="$(ls "\${TARBALL_DIR}"/runtypelabs-runtime-*.tgz | head -n 1)"
|
|
952
|
+
if [ -z "\${RUNTIME_TGZ}" ]; then
|
|
953
|
+
echo "[setup] error: no tarball produced by pnpm pack" >&2
|
|
954
|
+
exit 1
|
|
955
|
+
fi
|
|
956
|
+
RUNTIME_TGZ_NAME="$(basename "\${RUNTIME_TGZ}")"
|
|
957
|
+
|
|
958
|
+
echo "[setup] updating package.json with file: dependency..."
|
|
959
|
+
node -e "
|
|
960
|
+
const fs = require('fs');
|
|
961
|
+
const pkg = JSON.parse(fs.readFileSync('\${DEPLOY_DIR}/package.json', 'utf8'));
|
|
962
|
+
pkg.dependencies['@runtypelabs/runtime'] = 'file:./packages/\${RUNTIME_TGZ_NAME}';
|
|
963
|
+
fs.writeFileSync('\${DEPLOY_DIR}/package.json', JSON.stringify(pkg, null, 2) + '\\\\n');
|
|
964
|
+
"
|
|
965
|
+
|
|
966
|
+
echo "[setup] installing dependencies..."
|
|
967
|
+
npm install --no-audit --no-fund
|
|
968
|
+
|
|
969
|
+
echo ""
|
|
970
|
+
echo "[setup] Done! Next steps:"
|
|
971
|
+
echo " vercel dev — preview locally"
|
|
972
|
+
echo " vercel deploy — deploy to Vercel Functions"
|
|
973
|
+
echo ""
|
|
974
|
+
echo "[setup] Telemetry: vercel env add RUNTYPE_API_KEY (scope TELEMETRY:WRITE) to report runs"
|
|
975
|
+
echo " RUNTYPE_TELEMETRY=off disable reporting"
|
|
976
|
+
echo " RUNTYPE_TELEMETRY_CONTENT=off report without messages, tool arguments, or results"
|
|
977
|
+
echo " RUNTYPE_API_URL=<origin> override https://api.runtype.com"
|
|
978
|
+
`;
|
|
979
|
+
}
|
|
980
|
+
function vercelReadme(agentNames, secretNames, optionalSettings) {
|
|
981
|
+
return `# Runtype Vercel Functions Deployment
|
|
982
|
+
|
|
983
|
+
Generated by \`runtype deploy --target vercel\`. Contains the following agents:
|
|
984
|
+
|
|
985
|
+
${agentNames.map((n) => ` - ${n}`).join("\n")}
|
|
986
|
+
|
|
987
|
+
## Setup
|
|
988
|
+
|
|
989
|
+
\`\`\`bash
|
|
990
|
+
chmod +x setup.sh && ./setup.sh # build runtime + install deps
|
|
991
|
+
\`\`\`
|
|
992
|
+
|
|
993
|
+
## Secrets
|
|
994
|
+
|
|
995
|
+
Set each API key your agents use via the Vercel dashboard or CLI:
|
|
996
|
+
|
|
997
|
+
\`\`\`bash
|
|
998
|
+
${secretNames.length > 0 ? secretNames.map((s) => ` vercel env add ${s}`).join("\n") : " (none detected)"}
|
|
999
|
+
\`\`\`
|
|
1000
|
+
|
|
1001
|
+
${optionalProviderSettingsSection(optionalSettings)}
|
|
1002
|
+
|
|
1003
|
+
${telemetryReadmeSection("vercel env add RUNTYPE_API_KEY")}
|
|
1004
|
+
|
|
1005
|
+
## Run locally
|
|
1006
|
+
|
|
1007
|
+
\`\`\`bash
|
|
1008
|
+
vercel dev
|
|
1009
|
+
\`\`\`
|
|
1010
|
+
|
|
1011
|
+
## Deploy
|
|
1012
|
+
|
|
1013
|
+
\`\`\`bash
|
|
1014
|
+
vercel deploy
|
|
1015
|
+
\`\`\`
|
|
1016
|
+
|
|
1017
|
+
## Health check
|
|
1018
|
+
|
|
1019
|
+
\`GET /health\` returns \`{"status":"ok","agents":[...]}\`.
|
|
1020
|
+
|
|
1021
|
+
## Call an agent
|
|
1022
|
+
|
|
1023
|
+
\`\`\`bash
|
|
1024
|
+
curl https://<your-project>.vercel.app/agent/<name> \\
|
|
1025
|
+
-H 'Content-Type: application/json' \\
|
|
1026
|
+
-d '{"messages":[{"role":"user","content":"Hello"}]}'
|
|
1027
|
+
\`\`\`
|
|
1028
|
+
`;
|
|
1029
|
+
}
|
|
1030
|
+
function collectExportedExecutionInventory(definition) {
|
|
1031
|
+
const models = /* @__PURE__ */ new Set();
|
|
1032
|
+
const namedSecrets = /* @__PURE__ */ new Set();
|
|
1033
|
+
const visited = /* @__PURE__ */ new WeakSet();
|
|
1034
|
+
const isObject = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
1035
|
+
const addModel = (value) => {
|
|
1036
|
+
if (typeof value === "string" && value) models.add(value);
|
|
1037
|
+
};
|
|
1038
|
+
const walkMcpServers = (servers) => {
|
|
1039
|
+
if (!Array.isArray(servers)) return;
|
|
1040
|
+
for (const server of servers) {
|
|
1041
|
+
if (!isObject(server) || !isObject(server.auth)) continue;
|
|
1042
|
+
for (const key of [
|
|
1043
|
+
"tokenSecretName",
|
|
1044
|
+
"usernameSecretName",
|
|
1045
|
+
"passwordSecretName"
|
|
1046
|
+
]) {
|
|
1047
|
+
const value = server.auth[key];
|
|
1048
|
+
if (typeof value === "string" && value) namedSecrets.add(value);
|
|
1049
|
+
}
|
|
1050
|
+
}
|
|
1051
|
+
};
|
|
1052
|
+
function walkRuntimeTools(runtimeTools) {
|
|
1053
|
+
if (!Array.isArray(runtimeTools)) return;
|
|
1054
|
+
for (const tool of runtimeTools) {
|
|
1055
|
+
if (!isObject(tool) || !isObject(tool.config)) continue;
|
|
1056
|
+
if (isObject(tool.config.flow)) walkFlow(tool.config.flow);
|
|
1057
|
+
if (isObject(tool.config.agent)) walkAgent(tool.config.agent);
|
|
1058
|
+
if (isObject(tool.config.skill)) walkRuntimeTools(tool.config.skill.resolvedTools);
|
|
1059
|
+
}
|
|
1060
|
+
}
|
|
1061
|
+
function walkTools(tools) {
|
|
1062
|
+
if (!isObject(tools)) return;
|
|
1063
|
+
walkMcpServers(tools.mcpServers);
|
|
1064
|
+
walkRuntimeTools(tools.runtimeTools);
|
|
1065
|
+
}
|
|
1066
|
+
function walkFallbackModels(config) {
|
|
1067
|
+
if (!isObject(config.errorHandling) || !Array.isArray(config.errorHandling.fallbacks)) return;
|
|
1068
|
+
for (const fallback of config.errorHandling.fallbacks) if (isObject(fallback) && fallback.type === "model") addModel(fallback.model);
|
|
1069
|
+
}
|
|
1070
|
+
function walkSteps(steps) {
|
|
1071
|
+
if (!Array.isArray(steps)) return;
|
|
1072
|
+
for (const step of steps) {
|
|
1073
|
+
if (!isObject(step) || !isObject(step.config)) continue;
|
|
1074
|
+
if (step.type === "prompt") addModel(step.config.model);
|
|
1075
|
+
walkFallbackModels(step.config);
|
|
1076
|
+
walkTools(step.config.tools);
|
|
1077
|
+
if (step.type !== "conditional") continue;
|
|
1078
|
+
for (const key of [
|
|
1079
|
+
"trueSteps",
|
|
1080
|
+
"falseSteps",
|
|
1081
|
+
"otherwiseSteps"
|
|
1082
|
+
]) walkSteps(step.config[key]);
|
|
1083
|
+
if (Array.isArray(step.config.branches)) {
|
|
1084
|
+
for (const branch of step.config.branches) if (isObject(branch)) walkSteps(branch.steps);
|
|
1085
|
+
}
|
|
1086
|
+
}
|
|
1087
|
+
}
|
|
1088
|
+
function walkFlow(flow) {
|
|
1089
|
+
if (visited.has(flow)) return;
|
|
1090
|
+
visited.add(flow);
|
|
1091
|
+
walkSteps(flow.steps);
|
|
1092
|
+
if (isObject(flow.agents)) {
|
|
1093
|
+
for (const agent of Object.values(flow.agents)) if (isObject(agent)) walkAgent(agent);
|
|
1094
|
+
}
|
|
1095
|
+
}
|
|
1096
|
+
function walkAgent(agent) {
|
|
1097
|
+
if (visited.has(agent)) return;
|
|
1098
|
+
visited.add(agent);
|
|
1099
|
+
if (isObject(agent.config)) {
|
|
1100
|
+
addModel(agent.config.model);
|
|
1101
|
+
walkTools(agent.config.tools);
|
|
1102
|
+
}
|
|
1103
|
+
if (isObject(agent.claudeManagedConfig)) addModel(agent.claudeManagedConfig.model);
|
|
1104
|
+
if (Array.isArray(agent.skills)) {
|
|
1105
|
+
for (const skill of agent.skills) if (isObject(skill)) walkRuntimeTools(skill.resolvedTools);
|
|
1106
|
+
}
|
|
1107
|
+
if (Array.isArray(agent.capabilities)) for (const capability of agent.capabilities) {
|
|
1108
|
+
if (!isObject(capability)) continue;
|
|
1109
|
+
if (isObject(capability.flow)) walkFlow(capability.flow);
|
|
1110
|
+
if (isObject(capability.subAgent)) walkAgent(capability.subAgent);
|
|
1111
|
+
}
|
|
1112
|
+
}
|
|
1113
|
+
if (isObject(definition)) {
|
|
1114
|
+
if (Array.isArray(definition.steps) && !isObject(definition.config)) walkFlow(definition);
|
|
1115
|
+
else walkAgent(definition);
|
|
1116
|
+
}
|
|
1117
|
+
return {
|
|
1118
|
+
models: Array.from(models),
|
|
1119
|
+
namedSecrets: Array.from(namedSecrets)
|
|
1120
|
+
};
|
|
1121
|
+
}
|
|
1122
|
+
function collectDeploymentSecretInventory(agentDef) {
|
|
1123
|
+
const requiredNames = /* @__PURE__ */ new Set();
|
|
1124
|
+
const optionalSettings = /* @__PURE__ */ new Map();
|
|
1125
|
+
const executionInventory = collectExportedExecutionInventory(agentDef);
|
|
1126
|
+
for (const model of executionInventory.models) {
|
|
1127
|
+
for (const name of resolveModelSecretNames(model)) requiredNames.add(name);
|
|
1128
|
+
for (const setting of resolveModelOptionalSettings(model)) optionalSettings.set(setting.name, setting);
|
|
1129
|
+
}
|
|
1130
|
+
for (const name of executionInventory.namedSecrets) requiredNames.add(name);
|
|
1131
|
+
for (const name of extractSecretReferencesFromAnyValue(agentDef, 512)) requiredNames.add(name);
|
|
1132
|
+
for (const name of requiredNames) optionalSettings.delete(name);
|
|
1133
|
+
return {
|
|
1134
|
+
requiredNames: Array.from(requiredNames),
|
|
1135
|
+
optionalSettings: Array.from(optionalSettings.values())
|
|
1136
|
+
};
|
|
1137
|
+
}
|
|
1138
|
+
function slugify(s) {
|
|
1139
|
+
return s.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 63);
|
|
1140
|
+
}
|
|
1141
|
+
/**
|
|
1142
|
+
* Why `runtype deploy --flow` no longer scaffolds, and the two supported ways to
|
|
1143
|
+
* deploy a flow instead. Exported so a test pins that both stay named.
|
|
1144
|
+
*/
|
|
1145
|
+
const RETIRED_DEPLOY_FLOW_MESSAGE = [
|
|
1146
|
+
"Error: `runtype deploy --flow` is retired (ADR 0024).",
|
|
1147
|
+
" The scaffold wrapped an exported flow in a primary-flow agent, and that agent execution mode is gone.",
|
|
1148
|
+
" Two ways to deploy a flow instead:",
|
|
1149
|
+
" 1. Attach the flow to an agent as a runtime tool ({ toolType: 'flow', flowId }), then deploy that agent:",
|
|
1150
|
+
" runtype deploy --agent <agentId>",
|
|
1151
|
+
" 2. Run the flow on the hosted API without scaffolding anything:",
|
|
1152
|
+
" runtype dispatch --flow <flowId>"
|
|
1153
|
+
];
|
|
1154
|
+
const BYOC_PLAN_REQUIRED_CODE = "BYOC_PLAN_REQUIRED";
|
|
1155
|
+
/**
|
|
1156
|
+
* Hints printed under a failed `GET /agents/{id}/export-runtime`, keyed on the
|
|
1157
|
+
* API's own denial contract: a 403 is the plan or scope gate, a 404 is the
|
|
1158
|
+
* agent itself. Exported so a test pins that a 404 is never read as an
|
|
1159
|
+
* environment restriction again.
|
|
1160
|
+
*/
|
|
1161
|
+
function describeAgentExportFailure(err) {
|
|
1162
|
+
if (!(err instanceof RuntypeApiError)) return [];
|
|
1163
|
+
if (err.statusCode === 403) {
|
|
1164
|
+
if ((err.data && typeof err.data === "object" && "code" in err.data ? err.data.code : void 0) === "BYOC_PLAN_REQUIRED") return ["Self-hosting (BYOC) requires an Enterprise plan. Contact support to enable it for your account.", "The API key also needs the RUNTIME:EXPORT scope."];
|
|
1165
|
+
return ["The API key needs the RUNTIME:EXPORT scope, and self-hosting (BYOC) requires an Enterprise plan."];
|
|
1166
|
+
}
|
|
1167
|
+
if (err.statusCode === 404) {
|
|
1168
|
+
const code = err.data && typeof err.data === "object" && "code" in err.data ? err.data.code : void 0;
|
|
1169
|
+
if (typeof code === "string" && RELEASE_SELECTOR_NOT_FOUND_CODES.has(code)) return ["The requested --alias or --version-id does not exist on this agent, or the alias is archived or expired."];
|
|
1170
|
+
return ["Agent not found, or not owned by this account."];
|
|
1171
|
+
}
|
|
1172
|
+
return [];
|
|
1173
|
+
}
|
|
1174
|
+
const RELEASE_SELECTOR_NOT_FOUND_CODES = /* @__PURE__ */ new Set([
|
|
1175
|
+
"alias_not_found",
|
|
1176
|
+
"alias_archived",
|
|
1177
|
+
"alias_expired",
|
|
1178
|
+
"version_not_found"
|
|
1179
|
+
]);
|
|
1180
|
+
/** The export-runtime query for a release selector; errors when both selectors are given. */
|
|
1181
|
+
function agentReleaseSelectorParams(options, agentCount) {
|
|
1182
|
+
if (options.alias !== void 0 && options.versionId !== void 0) return { error: "Pass either --alias or --version-id, not both." };
|
|
1183
|
+
if (options.versionId !== void 0 && agentCount !== 1) return { error: "--version-id names one agent version, so pass exactly one --agent with it." };
|
|
1184
|
+
return { params: {
|
|
1185
|
+
...options.alias !== void 0 ? { alias: options.alias } : {},
|
|
1186
|
+
...options.versionId !== void 0 ? { versionId: options.versionId } : {}
|
|
1187
|
+
} };
|
|
1188
|
+
}
|
|
1189
|
+
function describeExportedRelease(def) {
|
|
1190
|
+
const release = def.release;
|
|
1191
|
+
if (!release?.versionId) return "";
|
|
1192
|
+
const via = release.alias ? ` via ${release.alias}${release.aliasRevision !== void 0 ? ` r${release.aliasRevision}` : ""}` : "";
|
|
1193
|
+
return chalk.dim(` (${release.versionId}${via})`);
|
|
1194
|
+
}
|
|
1195
|
+
const deployCommand = new Command("deploy").description("Export an agent and scaffold a deployment (cloud-run, cloudflare, or vercel)").option("--agent <id>", "Agent ID to export (may be repeated)", (v, acc) => {
|
|
1196
|
+
acc.push(v);
|
|
1197
|
+
return acc;
|
|
1198
|
+
}, []).option("--flow <id>", "Removed (ADR 0024): export the agent that runs the flow instead", (v, acc) => {
|
|
1199
|
+
acc.push(v);
|
|
1200
|
+
return acc;
|
|
1201
|
+
}, []).option("--output <dir>", "Output directory for the scaffold (default: ./runtype-deploy)", "./runtype-deploy").option("--name <name>", "Project name used in package.json (default: derived from output dir)").option("--target <target>", "Deployment target: cloud-run, cloudflare, or vercel (default: cloud-run)", "cloud-run").option("--alias <name>", "Export the version each agent's release alias points at (e.g. staging, live, pr-42)").option("--version-id <id>", "Export this published agent version (requires exactly one --agent)").action(async (options) => {
|
|
1202
|
+
const agentIds = options.agent;
|
|
1203
|
+
if (options.flow.length > 0) {
|
|
1204
|
+
for (const line of RETIRED_DEPLOY_FLOW_MESSAGE) console.error(chalk.yellow(line));
|
|
1205
|
+
process.exit(1);
|
|
1206
|
+
}
|
|
1207
|
+
if (agentIds.length === 0) {
|
|
1208
|
+
console.error(chalk.red("Error: provide at least one --agent <id>"));
|
|
1209
|
+
process.exit(1);
|
|
1210
|
+
}
|
|
1211
|
+
const selector = agentReleaseSelectorParams(options, agentIds.length);
|
|
1212
|
+
if ("error" in selector) {
|
|
1213
|
+
console.error(chalk.red(`Error: ${selector.error}`));
|
|
1214
|
+
process.exit(1);
|
|
1215
|
+
}
|
|
1216
|
+
const target = options.target;
|
|
1217
|
+
if (target !== "cloud-run" && target !== "cloudflare" && target !== "vercel") {
|
|
1218
|
+
console.error(chalk.red(`Error: unknown --target "${target}". Must be one of: cloud-run, cloudflare, vercel`));
|
|
1219
|
+
process.exit(1);
|
|
1220
|
+
}
|
|
1221
|
+
const apiKey = await ensureAuth();
|
|
1222
|
+
if (!apiKey) return;
|
|
1223
|
+
const client = createCliClient(apiKey);
|
|
1224
|
+
const outDir = path$1.resolve(options.output);
|
|
1225
|
+
const projectName = options.name ?? slugify(path$1.basename(outDir));
|
|
1226
|
+
console.log(chalk.cyan(`\nScaffolding ${target} deployment to ${outDir}\n`));
|
|
1227
|
+
const agentDefs = [];
|
|
1228
|
+
for (const id of agentIds) {
|
|
1229
|
+
process.stdout.write(` Exporting agent ${chalk.green(id)}... `);
|
|
1230
|
+
try {
|
|
1231
|
+
const def = await client.get(`/agents/${id}/export-runtime`, selector.params);
|
|
1232
|
+
agentDefs.push({
|
|
1233
|
+
id,
|
|
1234
|
+
name: String(def.name ?? id),
|
|
1235
|
+
def
|
|
1236
|
+
});
|
|
1237
|
+
process.stdout.write(chalk.green("✓") + describeExportedRelease(def) + "\n");
|
|
1238
|
+
} catch (err) {
|
|
1239
|
+
const msg = err instanceof RuntypeApiError ? err.message : String(err);
|
|
1240
|
+
process.stdout.write(chalk.red(`✗\n`));
|
|
1241
|
+
console.error(chalk.red(` Failed to export agent ${id}: ${msg}`));
|
|
1242
|
+
for (const hint of describeAgentExportFailure(err)) console.error(chalk.yellow(` ${hint}`));
|
|
1243
|
+
process.exit(1);
|
|
1244
|
+
}
|
|
1245
|
+
}
|
|
1246
|
+
const agentSlugs = agentDefs.map(({ name }) => slugify(name));
|
|
1247
|
+
const slugSet = /* @__PURE__ */ new Set();
|
|
1248
|
+
for (let i = 0; i < agentSlugs.length; i++) {
|
|
1249
|
+
const slug = agentSlugs[i];
|
|
1250
|
+
if (slugSet.has(slug)) {
|
|
1251
|
+
const colliders = agentDefs.filter(({ name }) => slugify(name) === slug).map(({ name }) => `"${name}"`).join(" and ");
|
|
1252
|
+
console.error(chalk.red(`Error: agents ${colliders} produce the same slug "${slug}".`));
|
|
1253
|
+
console.error(chalk.yellow(" Rename one of them in the Runtype dashboard to make slugs unique."));
|
|
1254
|
+
process.exit(1);
|
|
1255
|
+
}
|
|
1256
|
+
slugSet.add(slug);
|
|
1257
|
+
}
|
|
1258
|
+
fs$1.mkdirSync(path$1.join(outDir, "agents"), { recursive: true });
|
|
1259
|
+
fs$1.mkdirSync(path$1.join(outDir, "packages"), { recursive: true });
|
|
1260
|
+
for (const { name, def } of agentDefs) {
|
|
1261
|
+
const filename = `${slugify(name)}.json`;
|
|
1262
|
+
fs$1.writeFileSync(path$1.join(outDir, "agents", filename), JSON.stringify(def, null, 2));
|
|
1263
|
+
console.log(` Wrote agents/${filename}`);
|
|
1264
|
+
}
|
|
1265
|
+
const agentNames = agentDefs.map((a) => a.name);
|
|
1266
|
+
let monorepoRoot = process.cwd();
|
|
1267
|
+
for (let i = 0; i < 8; i++) {
|
|
1268
|
+
if (fs$1.existsSync(path$1.join(monorepoRoot, "pnpm-workspace.yaml"))) break;
|
|
1269
|
+
const parent = path$1.dirname(monorepoRoot);
|
|
1270
|
+
if (parent === monorepoRoot) break;
|
|
1271
|
+
monorepoRoot = parent;
|
|
1272
|
+
}
|
|
1273
|
+
const allSecrets = /* @__PURE__ */ new Set();
|
|
1274
|
+
const allOptionalSettings = /* @__PURE__ */ new Map();
|
|
1275
|
+
for (const { def } of agentDefs) {
|
|
1276
|
+
const inventory = collectDeploymentSecretInventory(def);
|
|
1277
|
+
for (const name of inventory.requiredNames) allSecrets.add(name);
|
|
1278
|
+
for (const setting of inventory.optionalSettings) allOptionalSettings.set(setting.name, setting);
|
|
1279
|
+
}
|
|
1280
|
+
for (const name of allSecrets) allOptionalSettings.delete(name);
|
|
1281
|
+
const secretNames = Array.from(allSecrets);
|
|
1282
|
+
const optionalSettings = Array.from(allOptionalSettings.values());
|
|
1283
|
+
if (target === "cloudflare") {
|
|
1284
|
+
fs$1.mkdirSync(path$1.join(outDir, "src"), { recursive: true });
|
|
1285
|
+
fs$1.writeFileSync(path$1.join(outDir, "src", "index.ts"), workerIndexTs(agentSlugs));
|
|
1286
|
+
fs$1.writeFileSync(path$1.join(outDir, "wrangler.toml"), wranglerToml(projectName));
|
|
1287
|
+
fs$1.writeFileSync(path$1.join(outDir, "package.json"), workerPackageJson(projectName, "workspace:*"));
|
|
1288
|
+
fs$1.writeFileSync(path$1.join(outDir, "tsconfig.json"), workerTsconfigJson());
|
|
1289
|
+
const setupPath = path$1.join(outDir, "setup.sh");
|
|
1290
|
+
fs$1.writeFileSync(setupPath, workerSetupSh(monorepoRoot));
|
|
1291
|
+
fs$1.chmodSync(setupPath, 493);
|
|
1292
|
+
fs$1.writeFileSync(path$1.join(outDir, "README.md"), workerReadme(agentNames, secretNames, optionalSettings));
|
|
1293
|
+
} else if (target === "vercel") {
|
|
1294
|
+
fs$1.mkdirSync(path$1.join(outDir, "api"), { recursive: true });
|
|
1295
|
+
fs$1.writeFileSync(path$1.join(outDir, "api", "[[...route]].ts"), vercelRouteTs(agentSlugs));
|
|
1296
|
+
fs$1.writeFileSync(path$1.join(outDir, "vercel.json"), vercelJson());
|
|
1297
|
+
fs$1.writeFileSync(path$1.join(outDir, "package.json"), vercelPackageJson(projectName, "workspace:*"));
|
|
1298
|
+
fs$1.writeFileSync(path$1.join(outDir, "tsconfig.json"), vercelTsconfigJson());
|
|
1299
|
+
const setupPath = path$1.join(outDir, "setup.sh");
|
|
1300
|
+
fs$1.writeFileSync(setupPath, vercelSetupSh(monorepoRoot));
|
|
1301
|
+
fs$1.chmodSync(setupPath, 493);
|
|
1302
|
+
fs$1.writeFileSync(path$1.join(outDir, "README.md"), vercelReadme(agentNames, secretNames, optionalSettings));
|
|
1303
|
+
} else {
|
|
1304
|
+
fs$1.writeFileSync(path$1.join(outDir, "server.ts"), serverTs());
|
|
1305
|
+
fs$1.writeFileSync(path$1.join(outDir, "Dockerfile"), dockerfile());
|
|
1306
|
+
fs$1.writeFileSync(path$1.join(outDir, ".dockerignore"), dockerignore());
|
|
1307
|
+
fs$1.writeFileSync(path$1.join(outDir, "tsconfig.json"), tsconfigJson());
|
|
1308
|
+
fs$1.writeFileSync(path$1.join(outDir, "package.json"), packageJson(projectName, "workspace:*"));
|
|
1309
|
+
const setupPath = path$1.join(outDir, "setup.sh");
|
|
1310
|
+
fs$1.writeFileSync(setupPath, setupSh(monorepoRoot));
|
|
1311
|
+
fs$1.chmodSync(setupPath, 493);
|
|
1312
|
+
fs$1.writeFileSync(path$1.join(outDir, "README.md"), readme(agentNames, secretNames, optionalSettings));
|
|
1313
|
+
}
|
|
1314
|
+
console.log("");
|
|
1315
|
+
console.log(chalk.green(`✓ Scaffold written to ${outDir}`));
|
|
1316
|
+
console.log("");
|
|
1317
|
+
console.log(chalk.bold("Next steps:"));
|
|
1318
|
+
console.log(` cd ${options.output}`);
|
|
1319
|
+
console.log(" chmod +x setup.sh && ./setup.sh # pack runtime + install deps");
|
|
1320
|
+
if (optionalSettings.length > 0) {
|
|
1321
|
+
console.log("");
|
|
1322
|
+
console.log(chalk.yellow("Optional provider settings (defaults used when unset):"));
|
|
1323
|
+
for (const setting of optionalSettings) console.log(` ${chalk.cyan(setting.name)} (default: ${chalk.cyan(setting.defaultValue)})`);
|
|
1324
|
+
}
|
|
1325
|
+
if (target === "cloudflare") {
|
|
1326
|
+
console.log(" wrangler dev # test locally");
|
|
1327
|
+
console.log("");
|
|
1328
|
+
if (secretNames.length > 0) {
|
|
1329
|
+
console.log(chalk.yellow("Required secrets (set via wrangler):"));
|
|
1330
|
+
for (const s of secretNames) console.log(` wrangler secret put ${chalk.cyan(s)}`);
|
|
1331
|
+
console.log("");
|
|
1332
|
+
}
|
|
1333
|
+
console.log("Deploy to Cloudflare Workers:");
|
|
1334
|
+
console.log(" wrangler deploy");
|
|
1335
|
+
} else if (target === "vercel") {
|
|
1336
|
+
console.log(" vercel dev # test locally");
|
|
1337
|
+
console.log("");
|
|
1338
|
+
if (secretNames.length > 0) {
|
|
1339
|
+
console.log(chalk.yellow("Required secrets (set via Vercel):"));
|
|
1340
|
+
for (const s of secretNames) console.log(` vercel env add ${chalk.cyan(s)}`);
|
|
1341
|
+
console.log("");
|
|
1342
|
+
}
|
|
1343
|
+
console.log("Deploy to Vercel Functions:");
|
|
1344
|
+
console.log(" vercel deploy");
|
|
1345
|
+
} else {
|
|
1346
|
+
console.log(" npm run dev # test locally");
|
|
1347
|
+
console.log("");
|
|
1348
|
+
if (secretNames.length > 0) {
|
|
1349
|
+
console.log(chalk.yellow("Required secrets (set before deploying):"));
|
|
1350
|
+
for (const s of secretNames) console.log(` ${chalk.cyan(s)}`);
|
|
1351
|
+
console.log("");
|
|
1352
|
+
console.log(" Set via Cloud Run: gcloud run deploy … --set-secrets KEY=NAME:latest");
|
|
1353
|
+
console.log(" Or export locally: export KEY=<value>");
|
|
1354
|
+
console.log("");
|
|
1355
|
+
}
|
|
1356
|
+
console.log("Deploy to Cloud Run:");
|
|
1357
|
+
console.log(` gcloud run deploy ${projectName} --source ${options.output} \\`);
|
|
1358
|
+
console.log(" --region us-central1 --platform managed \\");
|
|
1359
|
+
console.log(" --allow-unauthenticated --port 8080");
|
|
1360
|
+
}
|
|
1361
|
+
});
|
|
1362
|
+
export { deploy_exports as n, describeAgentExportFailure as r, agentReleaseSelectorParams as t };
|
|
1363
|
+
|