@netlify/agent-runner-cli 1.164.0 → 1.165.0
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/bin-local.js +239 -206
- package/dist/bin.js +251 -218
- package/dist/index.js +218 -185
- package/dist/interactions-mcp-server.js +39 -13
- package/dist/skills/netlify-server@netlifyserver/SKILL.md +163 -0
- package/package.json +2 -1
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: netlify-server
|
|
3
|
+
description: Run a long-running HTTP server (Express, Hono, Fastify, or plain node:http) on Netlify. ONLY use when the prompt explicitly asks for a Netlify Server by name. Never select this primitive on your own initiative.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Netlify Server
|
|
7
|
+
|
|
8
|
+
A Netlify Server is one long-running Node.js process deployed alongside your site. Unlike Netlify Functions, which run per request, the server process is started once, reused across requests, scaled to zero when idle, and started again on the next request. It runs standard Node.js servers with no Netlify-specific APIs: if it listens on a port, it works.
|
|
9
|
+
|
|
10
|
+
## When to use this skill: explicit request only
|
|
11
|
+
|
|
12
|
+
Netlify Server is an experimental primitive. The rule for choosing it is strict:
|
|
13
|
+
|
|
14
|
+
- ONLY build with a Netlify Server when the prompt **explicitly requests a "Netlify Server"** by name (for example "build this as a Netlify Server" or "use the Netlify Server primitive").
|
|
15
|
+
- If the prompt does not name it, discard this skill entirely, even when the request mentions Express, Hono, WebSockets, servers in general, or would otherwise seem like a good fit. In those cases use **Netlify Functions** (see the `netlify-functions` skill) and the other platform primitives as usual.
|
|
16
|
+
- NEVER migrate an existing site's functions to a Netlify Server unless explicitly asked to.
|
|
17
|
+
|
|
18
|
+
Once a Netlify Server has been explicitly requested: implement all API routes inside the server app, and do NOT create files in `netlify/functions/` alongside it.
|
|
19
|
+
|
|
20
|
+
## Quick start
|
|
21
|
+
|
|
22
|
+
Create exactly one entrypoint at `netlify/server/index.mjs`. Any Node.js server works. It MUST listen on `process.env.PORT`.
|
|
23
|
+
|
|
24
|
+
Express:
|
|
25
|
+
|
|
26
|
+
```javascript
|
|
27
|
+
// netlify/server/index.mjs
|
|
28
|
+
import express from 'express'
|
|
29
|
+
|
|
30
|
+
const app = express()
|
|
31
|
+
|
|
32
|
+
app.get('/api/hello', (req, res) => {
|
|
33
|
+
res.json({ hello: 'world' })
|
|
34
|
+
})
|
|
35
|
+
|
|
36
|
+
app.listen(process.env.PORT)
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
Hono (no `listen` call; export the app as the default export):
|
|
40
|
+
|
|
41
|
+
```javascript
|
|
42
|
+
// netlify/server/index.mjs
|
|
43
|
+
import { Hono } from 'hono'
|
|
44
|
+
|
|
45
|
+
const app = new Hono()
|
|
46
|
+
|
|
47
|
+
app.get('/api/hello', (c) => c.json({ hello: 'world' }))
|
|
48
|
+
|
|
49
|
+
export default app
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
Declare dependencies in `package.json` as usual. Deploy is the normal site deploy; nothing extra to configure. The build log confirms detection with the line `Netlify Server detected at netlify/server/index.mjs`.
|
|
53
|
+
|
|
54
|
+
## Rules
|
|
55
|
+
|
|
56
|
+
- **One entrypoint only.** `netlify/server/` must contain exactly one of `index.mjs`, `index.js`, `index.ts`, or `index.mts`. Two entry files fail the build. Prefer `index.mjs` with ESM syntax; split the rest of the server into modules that the entrypoint imports.
|
|
57
|
+
- **Listen on `process.env.PORT`.** It is set before your code is imported. Do not hardcode a port.
|
|
58
|
+
- **Do not daemonize, fork, or spawn long-lived child processes.** One process serves the traffic.
|
|
59
|
+
- Environment variables work as usual via `process.env`.
|
|
60
|
+
- When adding npm modules, ensure `node_modules` is in `.gitignore`.
|
|
61
|
+
|
|
62
|
+
## Routing and static files
|
|
63
|
+
|
|
64
|
+
The server receives every request that is not served as a static file:
|
|
65
|
+
|
|
66
|
+
- Files in the publish directory win over the server for matching paths (`preferStatic`). Use the publish directory for assets like images, fonts, or a favicon.
|
|
67
|
+
- Do NOT put an `index.html` in the publish directory if the server should render `/`. A static `index.html` shadows the server's root route.
|
|
68
|
+
- There is no path prefix: the server sees `/`, `/api/anything`, and everything else, with the original paths.
|
|
69
|
+
|
|
70
|
+
## Lifecycle: scale to zero
|
|
71
|
+
|
|
72
|
+
This is the most important mental model for building correctly:
|
|
73
|
+
|
|
74
|
+
- After a period with no traffic, the server is shut down. The next request starts a fresh process (a cold start).
|
|
75
|
+
- **All process memory is lost on every scale-to-zero, deploy, and platform maintenance event.** In-memory caches are fine as caches; they are NEVER storage. Anything that must survive belongs in Netlify Blobs or Netlify Database (see the `general-database` skill to choose).
|
|
76
|
+
- In-flight requests are never cut off by the idle shutdown.
|
|
77
|
+
- Background work that outlives the request that triggered it (for example, responding early and then finishing a task) MUST be registered with `getContext().waitUntil(promise)`. Registered work keeps the process alive until the promise settles; unregistered work may be reclaimed the moment no requests are active.
|
|
78
|
+
- `waitUntil` extends the idle lifetime, but deploys and platform maintenance still stop the process with only a short grace window. Keep background tasks short (seconds, not minutes), and persist anything critical before responding. For genuinely long jobs, persist a job record and process it in resumable increments across requests.
|
|
79
|
+
|
|
80
|
+
Graceful shutdown hook (optional): export a `shutdown` function from the entrypoint; it is called before the process is stopped. Keep it fast (a few seconds), for example flushing a buffer to Blobs. For Express-style apps that call `listen`, you can also listen for `SIGTERM` directly.
|
|
81
|
+
|
|
82
|
+
```javascript
|
|
83
|
+
export const shutdown = async () => {
|
|
84
|
+
await flushPendingWrites()
|
|
85
|
+
}
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
## Request context: geolocation, cookies, site metadata
|
|
89
|
+
|
|
90
|
+
Inside request handling, get the Netlify request context with `getContext` from the `@netlify/functions` package (add it to `package.json`):
|
|
91
|
+
|
|
92
|
+
```javascript
|
|
93
|
+
import express from 'express'
|
|
94
|
+
import { getContext } from '@netlify/functions'
|
|
95
|
+
|
|
96
|
+
const app = express()
|
|
97
|
+
|
|
98
|
+
app.get('/api/where', (req, res) => {
|
|
99
|
+
const context = getContext()
|
|
100
|
+
res.json({ city: context.geo?.city, country: context.geo?.country?.name })
|
|
101
|
+
})
|
|
102
|
+
|
|
103
|
+
app.listen(process.env.PORT)
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
Rules:
|
|
107
|
+
|
|
108
|
+
- ONLY call `getContext()` while handling a request (inside a route handler, middleware, or code awaited from one). It throws outside a request.
|
|
109
|
+
- Works with every server form, including WebSocket upgrade handling.
|
|
110
|
+
|
|
111
|
+
## AI inference: Netlify AI Gateway
|
|
112
|
+
|
|
113
|
+
The AI Gateway works with zero configuration: provider credentials and base URLs (`OPENAI_API_KEY`, `OPENAI_BASE_URL`, and equivalents for other providers) are injected into `process.env` automatically. See the `netlify-ai-gateway` skill for supported models and usage.
|
|
114
|
+
|
|
115
|
+
One rule specific to servers: **construct AI SDK clients inside request handling, never at module scope.** Credentials are injected per request, so they do not exist when the module is first imported.
|
|
116
|
+
|
|
117
|
+
```javascript
|
|
118
|
+
import OpenAI from 'openai'
|
|
119
|
+
|
|
120
|
+
app.post('/api/chat', async (req, res) => {
|
|
121
|
+
const client = new OpenAI() // created per request: credentials are available here
|
|
122
|
+
// ...
|
|
123
|
+
})
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
## WebSockets
|
|
127
|
+
|
|
128
|
+
WebSockets work for servers that handle HTTP upgrades (the `listen` form). Attach the WebSocket server to the same HTTP server:
|
|
129
|
+
|
|
130
|
+
```javascript
|
|
131
|
+
// netlify/server/index.mjs
|
|
132
|
+
import express from 'express'
|
|
133
|
+
import { WebSocketServer } from 'ws'
|
|
134
|
+
|
|
135
|
+
const app = express()
|
|
136
|
+
const server = app.listen(process.env.PORT)
|
|
137
|
+
const wss = new WebSocketServer({ noServer: true })
|
|
138
|
+
|
|
139
|
+
server.on('upgrade', (req, socket, head) => {
|
|
140
|
+
if (req.url !== '/ws') {
|
|
141
|
+
socket.destroy()
|
|
142
|
+
return
|
|
143
|
+
}
|
|
144
|
+
wss.handleUpgrade(req, socket, head, (ws) => {
|
|
145
|
+
ws.send('connected')
|
|
146
|
+
})
|
|
147
|
+
})
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
Rules for WebSocket clients you generate:
|
|
151
|
+
|
|
152
|
+
- ALWAYS implement automatic reconnection with backoff in the browser client. The platform may terminate long-lived connections periodically; a reconnecting client makes this invisible to users.
|
|
153
|
+
- Do not rely on a socket staying open indefinitely, and do not keep client state only on the server side of a socket: rebuild the needed state on reconnect.
|
|
154
|
+
|
|
155
|
+
## Common mistakes
|
|
156
|
+
|
|
157
|
+
- Treating process memory as a database. It is wiped on every cold start. Use Blobs or Database.
|
|
158
|
+
- Hardcoding a port or defaulting to `3000` without reading `process.env.PORT` first.
|
|
159
|
+
- Adding an `index.html` to the publish directory that shadows the server's `/` route.
|
|
160
|
+
- Creating `netlify/functions/` endpoints alongside the server. Put API routes in the server app.
|
|
161
|
+
- Two entry files in `netlify/server/` (for example both `index.mjs` and `index.ts`). Keep one.
|
|
162
|
+
- Fire-and-forget background jobs without `getContext().waitUntil(promise)`. Unregistered work may be reclaimed mid-flight; registered work should still be short and non-critical.
|
|
163
|
+
- Constructing AI SDK clients (or calling `getContext()`) at module scope. Per-request state does not exist at import time.
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@netlify/agent-runner-cli",
|
|
3
3
|
"type": "module",
|
|
4
|
-
"version": "1.
|
|
4
|
+
"version": "1.165.0",
|
|
5
5
|
"description": "CLI tool for running Netlify agents",
|
|
6
6
|
"main": "./dist/index.js",
|
|
7
7
|
"types": "./dist/index.d.ts",
|
|
@@ -42,6 +42,7 @@
|
|
|
42
42
|
"test:integration:codex": "vitest run test/integration/codex.test.ts",
|
|
43
43
|
"test:integration:claude": "vitest run test/integration/claude.test.ts",
|
|
44
44
|
"test:integration:gemini": "vitest run test/integration/gemini.test.ts",
|
|
45
|
+
"test:integration:opencode": "vitest run test/integration/opencode.test.ts",
|
|
45
46
|
"test:integration:skill-invocation": "vitest run test/integration/skill-invocation.test.ts",
|
|
46
47
|
"check:types": "tsc --noEmit",
|
|
47
48
|
"postinstall": "node scripts/postinstall.js"
|