@tomorrowos/sdk 0.9.35 → 0.9.41

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/VERCEL_SETUP.md CHANGED
@@ -5,11 +5,11 @@
5
5
  >
6
6
  > **On Vercel, this file is the setup questionnaire** (not `REPLIT_SETUP.md`, not `LLM_PROMPT.md`).
7
7
  >
8
- > Protocol id: `vercel-setup/1.9` pairs with `@tomorrowos/sdk` **0.9.35+** (`templates/cms-starter-v0`, Fluid Functions WebSockets).
8
+ > Protocol id: `vercel-setup/1.9` �?pairs with `@tomorrowos/sdk` **0.9.41+** (`templates/cms-starter-v0`, Fluid Functions WebSockets).
9
9
  >
10
- > **Questions source of truth:** `VERCEL_QUESTIONS.md` Agents must **open that file and paste each Q block verbatim**. Do not paraphrase.
11
- > **Copy questions verbatim from `VERCEL_QUESTIONS.md`.** Do not invent alternate Q2 options (no “Supabase Storage”, no S3, no “disable media”, no making Blob the recommended option).
12
- > **`TOMORROWOS_STORE` and `DATABASE_SSL` are agent-only** set them automatically after Q1; never put them in a user-facing Env form for the customer to type.
10
+ > **Questions source of truth:** `VERCEL_QUESTIONS.md` �?Agents must **open that file and paste each Q block verbatim**. Do not paraphrase.
11
+ > **Copy questions verbatim from `VERCEL_QUESTIONS.md`.** Do not invent alternate Q2 options (no “Supabase Storage�? no S3, no “disable media�? no making Blob the recommended option).
12
+ > **`TOMORROWOS_STORE` and `DATABASE_SSL` are agent-only** �?set them automatically after Q1; never put them in a user-facing Env form for the customer to type.
13
13
 
14
14
  ## NON-NEGOTIABLE VERCEL RUNTIME CONTRACT
15
15
 
@@ -18,7 +18,7 @@ The following rules are **mandatory acceptance criteria**, not suggestions. The
18
18
  1. **One TomorrowOS instance only.** Use the shared instance exported through `api/index.ts`. **Never create a second `TomorrowOS` instance in `api/ws.ts`** or any other route.
19
19
  2. **Never use `public/` as the Vercel Output Directory.** The dashboard Output Directory must be **empty / unset**. Serve the Control Panel at runtime from `cms-panel/` through TomorrowOS `staticRoot`.
20
20
  3. **Preview may use Next only as an isolated reverse-proxy shell under `preview/`.** Preview must not replace TomorrowOS or determine the Production runtime.
21
- 4. **Publish must use the Fluid Function.** Production must run `api/index.ts` the shared exported `http.Server`, with Fluid compute enabled. It must not use `next start`, a static export, or a bare `(req, res)` handler.
21
+ 4. **Publish must use the Fluid Function.** Production must run `api/index.ts` �?the shared exported `http.Server`, with Fluid compute enabled. It must not use `next start`, a static export, or a bare `(req, res)` handler.
22
22
  5. **After every Production deployment, verify all three gates before saying setup is complete:**
23
23
  - `GET /status` returns TomorrowOS **JSON**, not HTML.
24
24
  - `GET /` returns the TomorrowOS **Control Panel**.
@@ -35,16 +35,16 @@ When a human pastes a prompt like this, **read this file and start Question 1 im
35
35
  ```text
36
36
  Follow @tomorrowos/sdk VERCEL_SETUP.md and set up my TomorrowOS CMS.
37
37
  First open VERCEL_QUESTIONS.md. For Q1, Q2, Q3: paste each question block VERBATIM (character-for-character). Do not rephrase options.
38
- Q2 must show Cloudinary (recommended) as option 1 never Blob-as-recommended, never S3, never Supabase Storage.
39
- After database choice, YOU set TOMORROWOS_STORE and DATABASE_SSL do not ask me to fill those.
38
+ Q2 must show Cloudinary (recommended) as option 1 �?never Blob-as-recommended, never S3, never Supabase Storage.
39
+ After database choice, YOU set TOMORROWOS_STORE and DATABASE_SSL �?do not ask me to fill those.
40
40
  Do not use LLM_PROMPT.md or REPLIT_SETUP.md. No OpenAI/LLM API keys.
41
41
  Apply the NON-NEGOTIABLE VERCEL RUNTIME CONTRACT exactly: one TomorrowOS instance; no public/ Output Directory; Preview Next only under preview/; Publish through the Fluid Function; verify /status JSON, / Control Panel, and WebSocket 101; pair devices only with the Production URL.
42
42
  ```
43
43
 
44
44
  Alternate triggers:
45
45
 
46
- - “Set up TomorrowOS on Vercel using VERCEL_SETUP.md
47
- - “Configure my TomorrowOS CMS (Vercel wizard)
46
+ - “Set up TomorrowOS on Vercel using VERCEL_SETUP.md�?
47
+ - “Configure my TomorrowOS CMS (Vercel wizard)�?
48
48
 
49
49
  When any of these fire, **ignore `REPLIT_SETUP.md` and `LLM_PROMPT.md`** for the Q&A phase (you may skim Replit docs only for shared SDK semantics).
50
50
 
@@ -57,9 +57,9 @@ When any of these fire, **ignore `REPLIT_SETUP.md` and `LLM_PROMPT.md`** for the
57
57
  TomorrowOS CMS is **one Node process** that serves:
58
58
 
59
59
  - Browser Control Panel at `GET /` (`public/` via `staticRoot`)
60
- - Device **WebSocket** on the same origin (`https` players use `wss`)
60
+ - Device **WebSocket** on the same origin (`https` �?players use `wss`)
61
61
 
62
- ### Production / Publish Node only (no Next.js)
62
+ ### Production / Publish �?Node only (no Next.js)
63
63
 
64
64
  | Required | Forbidden |
65
65
  |----------|-----------|
@@ -68,12 +68,12 @@ TomorrowOS CMS is **one Node process** that serves:
68
68
  | **Fluid compute** enabled (needed for WebSockets on Vercel) | Classic short-lived serverless with no WebSocket / no Fluid |
69
69
  | Durable store = **Postgres** (Supabase or Neon, pooled URL) | **SQLite / `data/*.db` as production store** on Vercel (ephemeral filesystem) |
70
70
  | Media = **Cloudinary** or **Vercel Blob** (durable HTTPS URLs) | Relying on local `public/uploads` alone for production fleets |
71
- | Env vars in **Vercel Project Settings Environment Variables** | Committing passwords / API secrets / `.env` with secrets |
71
+ | Env vars in **Vercel Project �?Settings �?Environment Variables** | Committing passwords / API secrets / `.env` with secrets |
72
72
  | `cms.hostingTarget`: `"vercel"` in `brand.json` | Creating `.replit` / Replit artifact files |
73
73
 
74
74
  **Publish / Production Deploy** must run **`TomorrowOS.listen(...)`** directly. Do **not** put Next.js in front of Production traffic.
75
75
 
76
- ### Publish `404: NOT_FOUND` mandatory prevention (read before every deploy)
76
+ ### Publish `404: NOT_FOUND` �?mandatory prevention (read before every deploy)
77
77
 
78
78
  The production URL showing:
79
79
 
@@ -86,7 +86,7 @@ almost always means **Vercel still thinks this is a Next.js / static deploy**, n
86
86
 
87
87
  **Before every Production deploy, you MUST verify all of the following:**
88
88
 
89
- 1. **Project Settings General Framework Preset = Other** (or unset / null).
89
+ 1. **Project �?Settings �?General �?Framework Preset = Other** (or unset / null).
90
90
  If it still says **Next.js**, patch it via Vercel API / CLI / dashboard **before** redeploying.
91
91
  `"framework": null` in `vercel.json` alone is **not enough** if the dashboard still says Next.js.
92
92
  2. **Install Command** = `npm install` / `pnpm install` / `yarn` (normal install).
@@ -101,7 +101,7 @@ almost always means **Vercel still thinks this is a Next.js / static deploy**, n
101
101
 
102
102
  **Setup is NOT complete** while Production `GET /` returns `404: NOT_FOUND` or WebSocket upgrade returns 200 HTML.
103
103
 
104
- ### Publish static trap — `public/` served without Node (read this)
104
+ ### Publish static trap �?`public/` served without Node (read this)
105
105
 
106
106
  A common **worse** failure mode than `404: NOT_FOUND`:
107
107
 
@@ -110,7 +110,7 @@ A common **worse** failure mode than `404: NOT_FOUND`:
110
110
  - The panel shows **CMS unreachable** and console:
111
111
  `Unexpected token 'T', "The page c"... is not valid JSON`
112
112
 
113
- **Root cause:** Vercel deployed `public/` as a **static site** (Output Directory = `public` or Framework=Other default *“public if it exists”*). **No Function** is running TomorrowOS.
113
+ **Root cause:** Vercel deployed `public/` as a **static site** (Output Directory = `public` or Framework=Other default *“public if it exists�?). **No Function** is running TomorrowOS.
114
114
 
115
115
  **Fix (Scheme A):** use `cms-panel/` (not `public/`), Fluid Function `api/index.ts` + rewrites, Output Directory empty. Static UI is served **at runtime** via `staticRoot` in `listen()`.
116
116
 
@@ -124,7 +124,7 @@ curl -s https://YOUR-PROJECT.vercel.app/status | head -c 80
124
124
 
125
125
  **Setup is NOT complete** while `/status` returns HTML, the panel shows the JSON parse error, or devices cannot open `wss://`.
126
126
 
127
- ### Preview (v0 / Vercel Agent sandbox) Next.js shell allowed
127
+ ### Preview (v0 / Vercel Agent sandbox) �?Next.js shell allowed
128
128
 
129
129
  v0 Preview is often a **Next.js runtime** hardwired to `next dev`. A raw `tsx server.ts` may listen successfully while Preview still shows a blank page, because the sandbox intercepts the public port with Next before requests reach TomorrowOS.
130
130
 
@@ -134,10 +134,10 @@ v0 Preview is often a **Next.js runtime** hardwired to `next dev`. A raw `tsx se
134
134
  |---------------------|--------|
135
135
  | Next.js remains the Preview host | Satisfies v0 / sandbox expectations |
136
136
  | TomorrowOS on internal port | e.g. `TOMORROWOS_INTERNAL_PORT=3001` (not the public Preview port) |
137
- | Transparent HTTP reverse proxy | Next route/middleware forwards `/` (and Control Panel paths) → `http://127.0.0.1:3001` |
137
+ | Transparent HTTP reverse proxy | Next route/middleware forwards `/` (and Control Panel paths) �?`http://127.0.0.1:3001` |
138
138
  | Start both processes in Preview | Next + `tsx server.ts` (or a `dev:preview` script that starts both) |
139
139
 
140
- **Preview WebSocket note:** HTTP proxy is enough for Control Panel HTML/API smoke checks. Device `wss` pairing may not work through a Next proxy in Preview that is acceptable. **Pair devices against the Production Publish URL**, not v0 Preview.
140
+ **Preview WebSocket note:** HTTP proxy is enough for Control Panel HTML/API smoke checks. Device `wss` pairing may not work through a Next proxy in Preview �?that is acceptable. **Pair devices against the Production Publish URL**, not v0 Preview.
141
141
 
142
142
  **Do not** replace TomorrowOS with a Next-only CMS. Next is a **Preview adapter**, not the product.
143
143
 
@@ -145,43 +145,43 @@ v0 Preview is often a **Next.js runtime** hardwired to `next dev`. A raw `tsx se
145
145
 
146
146
  ## Questionnaire scope (STRICT)
147
147
 
148
- Ask **only** these questions, in **this exact order**. **Copy the “Ask exactlyblocks verbatim** do not paraphrase options.
148
+ Ask **only** these questions, in **this exact order**. **Copy the “Ask exactly�?blocks verbatim** �?do not paraphrase options.
149
149
 
150
150
  | Step | Section | When |
151
151
  |------|---------|------|
152
- | 1 | **Question 1** Database (Supabase Neon SQLite) | Always |
153
- | 2 | **Question 2** Media (**Cloudinary recommended** Vercel Blob local) | Always (after Q1) |
154
- | 3 | **Question 3** Brand / TomorrowOS look (`brand.json` only) | Always last |
152
+ | 1 | **Question 1** �?Database (Supabase �?Neon �?SQLite) | Always |
153
+ | 2 | **Question 2** �?Media (**Cloudinary recommended** �?Vercel Blob �?local) | Always (after Q1) |
154
+ | 3 | **Question 3** �?Brand / TomorrowOS look (`brand.json` only) | Always last |
155
155
 
156
156
  **That is the complete list.** Do **not** ask how many screens / devices.
157
157
 
158
158
  **Forbidden invented Question 2 options (never offer these in chat or UI):**
159
159
 
160
- - ❌ “Vercel Blob **(recommended)**” as option 1 Blob is option **2**, not recommended over Cloudinary
161
- - ❌ “Supabase Storage / S3 bucket / `SUPABASE_SERVICE_ROLE_KEY` for media
162
- - ❌ “No media storage / “disable media uploads / “remote URLs onlyas a numbered choice
163
- - Any media list that does not start with **Cloudinary (recommended)**
160
+ - �?“Vercel Blob **(recommended)**�?as option 1 �?Blob is option **2**, not recommended over Cloudinary
161
+ - �?“Supabase Storage�?/ S3 bucket / `SUPABASE_SERVICE_ROLE_KEY` for media
162
+ - �?“No media storage�?/ “disable media uploads�?/ “remote URLs only�?as a numbered choice
163
+ - �?Any media list that does not start with **Cloudinary (recommended)**
164
164
 
165
165
  **Forbidden Env UX after Question 1:**
166
166
 
167
- - Showing the user empty fields for `TOMORROWOS_STORE` or `DATABASE_SSL` to fill
168
- - Asking in chat “what should TOMORROWOS_STORE be?”
169
- - After DB choice + URL paste: **you** write those two vars silently (see Q1 Steps B/C/D)
167
+ - �?Showing the user empty fields for `TOMORROWOS_STORE` or `DATABASE_SSL` to fill
168
+ - �?Asking in chat “what should TOMORROWOS_STORE be?�?
169
+ - �?After DB choice + URL paste: **you** write those two vars silently (see Q1 Steps B/C/D)
170
170
 
171
171
  **Forbidden extra questions (never ask, even if the user skips a step):**
172
172
 
173
- - OpenAI / Anthropic / Gemini / any LLM API key (`sk-…`, etc.)
174
- - ❌ “AI pairing”, “AI feature logic”, or inventing auth that needs an LLM key
175
- - Asking the user to type `TOMORROWOS_STORE` or `DATABASE_SSL`
176
- - Any secret not listed in Questions 1–3 (DB URL, media credentials, brand)
173
+ - �?OpenAI / Anthropic / Gemini / any LLM API key (`sk-…`, etc.)
174
+ - �?“AI pairing�? “AI feature logic�? or inventing auth that needs an LLM key
175
+ - �?Asking the user to type `TOMORROWOS_STORE` or `DATABASE_SSL`
176
+ - �?Any secret not listed in Questions 1�? (DB URL, media credentials, brand)
177
177
 
178
178
  TomorrowOS pairing and Control Panel logic live in `@tomorrowos/sdk`. They do **not** call OpenAI.
179
179
 
180
- If the user **skips** a Question 2 option, do **not** invent a substitute question. Re-state Question 2 choices **verbatim**, or proceed with local uploads + warning never pivot to LLM keys or Supabase Storage.
180
+ If the user **skips** a Question 2 option, do **not** invent a substitute question. Re-state Question 2 choices **verbatim**, or proceed with local uploads + warning �?never pivot to LLM keys or Supabase Storage.
181
181
 
182
- If the user says “just set it up, don’t ask questions,” respond:
182
+ If the user says “just set it up, don’t ask questions,�?respond:
183
183
 
184
- > I need a few quick answers from VERCEL_SETUP.md (database Supabase or Neon preferred media storage with Cloudinary recommended, and branding). It takes about a minute and ensures the CMS works on Vercel.
184
+ > I need a few quick answers from VERCEL_SETUP.md (database �?Supabase or Neon preferred �?media storage with Cloudinary recommended, and branding). It takes about a minute and ensures the CMS works on Vercel.
185
185
 
186
186
  Then ask **Question 1**.
187
187
 
@@ -189,10 +189,10 @@ Then ask **Question 1**.
189
189
 
190
190
  ## Hard rules
191
191
 
192
- 1. **Open `VERCEL_QUESTIONS.md` first.** Paste Q1 configure paste Q2 configure paste Q3. Never paraphrase question options. Never use `LLM_PROMPT.md` or `REPLIT_SETUP.md` as the questionnaire. **Never** ask for OpenAI or other LLM API keys.
192
+ 1. **Open `VERCEL_QUESTIONS.md` first.** Paste Q1 �?configure �?paste Q2 �?configure �?paste Q3. Never paraphrase question options. Never use `LLM_PROMPT.md` or `REPLIT_SETUP.md` as the questionnaire. **Never** ask for OpenAI or other LLM API keys.
193
193
  2. **Question 2 options are fixed:** (1) Cloudinary **(recommended)**, (2) Vercel Blob, (3) local disk. Do not reorder; do not invent Supabase Storage / disable-uploads.
194
194
  3. **Cloudinary:** open **one** Vercel Env popup with **all** Cloudinary fields together. Do **not** quiz one credential at a time.
195
- 4. **`TOMORROWOS_STORE` + `DATABASE_SSL` = agent-owned.** After Q1 choice (and URL if needed), set them yourself in Vercel Env Vars. **Never** include them as blank fields for the customer. Mapping: Supabase → `TOMORROWOS_STORE=supabase` + `DATABASE_SSL=true`; Neon → `TOMORROWOS_STORE=postgres` + `DATABASE_SSL=true`; SQLite → `TOMORROWOS_STORE=sqlite` (no `DATABASE_SSL` required).
195
+ 4. **`TOMORROWOS_STORE` + `DATABASE_SSL` = agent-owned.** After Q1 choice (and URL if needed), set them yourself in Vercel Env Vars. **Never** include them as blank fields for the customer. Mapping: Supabase �?`TOMORROWOS_STORE=supabase` + `DATABASE_SSL=true`; Neon �?`TOMORROWOS_STORE=postgres` + `DATABASE_SSL=true`; SQLite �?`TOMORROWOS_STORE=sqlite` (no `DATABASE_SSL` required).
196
196
  5. **Do not invent** Cloudinary credentials, database URLs, Vercel Blob tokens, brand colours, **or LLM API keys**.
197
197
  6. **Postgres env naming:** prefer **`SUPABASE_URL`** for Supabase; prefer **`DATABASE_URL`** for Neon.
198
198
  7. **Always prefer pooled connection strings** (Supabase Session pooler **6543**; Neon pooled host).
@@ -203,63 +203,63 @@ Then ask **Question 1**.
203
203
  12. **Enable Fluid compute** for Production WebSockets.
204
204
  13. **Production:** `api/index.ts` exports `http.Server` from `TomorrowOS.listen`. Not `next start`. No second TomorrowOS in `api/ws.ts`.
205
205
  14. **Preview:** Next reverse-proxy shell when v0 is Next-hardwired.
206
- 15. After Q&A: **configure install deploy minimal verify**.
206
+ 15. After Q&A: **configure �?install �?deploy �?minimal verify**.
207
207
  16. **No inventing CMS login** from a branding URL.
208
- 17. **Skip invent.** Stay on Q1–Q3 only.
208
+ 17. **Skip �?invent.** Stay on Q1–Q3 only.
209
209
  18. **Publish gate:** `/status` JSON + WebSocket 101; no static `public/` trap.
210
210
 
211
- **Question order:** Q1 (auto-set store env) Q2 Q3 execution checklist.
211
+ **Question order:** Q1 �?(auto-set store env) �?Q2 �?Q3 �?execution checklist.
212
212
 
213
213
  ---
214
214
 
215
- ## Scheme A Vercel Functions + WebSockets (mandatory for Production)
215
+ ## Scheme A �?Vercel Functions + WebSockets (mandatory for Production)
216
216
 
217
217
  > **Goal:** Preview may use Next + proxy; **Publish** runs TomorrowOS as a **Vercel Function** (`api/index.ts`) on **Fluid**, with WebSocket support per [Vercel WebSockets docs](https://vercel.com/docs/functions/websockets).
218
218
  > **Scope:** Layout + `vercel.json` + dashboard. Prefer `npx @tomorrowos/sdk init --hosting v0` (`cms-starter-v0`).
219
- > **Do not** break Replit/Railway: those keep using default `init` → `cms-starter` (root `server.ts` + `server.listen`, no `api/`).
219
+ > **Do not** break Replit/Railway: those keep using default `init` �?`cms-starter` (root `server.ts` + `server.listen`, no `api/`).
220
220
 
221
221
  ### Why Scheme A exists
222
222
 
223
223
  | Layer | Preview (v0) | Publish (Production) |
224
224
  |-------|----------------|----------------------|
225
- | Host | Next.js shell + HTTP proxy | **Vercel Function** `api/index.ts` → `export default server` |
226
- | Static UI | Proxied from TomorrowOS | Served by **`staticRoot`** (`cms-panel/`) inside `listen()` — **not** Vercel Output Directory |
227
- | WebSocket | Optional / may fail in Preview | **Required** Fluid Function upgrade (`wss://` `/` rewritten to `/api`, or direct `/api`) |
225
+ | Host | Next.js shell + HTTP proxy | **Vercel Function** `api/index.ts` �?`export default server` |
226
+ | Static UI | Proxied from TomorrowOS | Served by **`staticRoot`** (`cms-panel/`) inside `listen()` �?**not** Vercel Output Directory |
227
+ | WebSocket | Optional / may fail in Preview | **Required** �?Fluid Function upgrade (`wss://` �?`/` rewritten to `/api`, or direct `/api`) |
228
228
  | Next files | Allowed under `preview/` | **Must not** live at project root |
229
229
 
230
- **Root cause of “Control Panel works, TV cannot connect”:** HTTP can be served without WebSocket upgrades. Classic root-only `server.ts` / plain serverless handlers often return **200 HTML** on `Upgrade` instead of **101**. Official pattern is a Function that exports `http.Server` with `ws` attached ([docs](https://vercel.com/docs/functions/websockets)).
230
+ **Root cause of “Control Panel works, TV cannot connect�?** HTTP can be served without WebSocket upgrades. Classic root-only `server.ts` / plain serverless handlers often return **200 HTML** on `Upgrade` instead of **101**. Official pattern is a Function that exports `http.Server` with `ws` attached ([docs](https://vercel.com/docs/functions/websockets)).
231
231
 
232
- ### A1 Project layout (Agent must enforce)
232
+ ### A1 �?Project layout (Agent must enforce)
233
233
 
234
234
  **Production (Vercel / v0 starter):**
235
235
 
236
236
  ```
237
- cms-app.ts shared TomorrowOS.listen + export const server
238
- api/index.ts export { server as default } from "../cms-app.js"
239
- server.ts local/Preview: same default export (npm start / internal port)
240
- vercel.json fluid + rewrites + functions.maxDuration
237
+ cms-app.ts �?shared TomorrowOS.listen + export const server
238
+ api/index.ts �?export { server as default } from "../cms-app.js"
239
+ server.ts �?local/Preview: same default export (npm start / internal port)
240
+ vercel.json �?fluid + rewrites + functions.maxDuration
241
241
  package.json
242
242
  brand.json
243
- cms-panel/ CMS static UI (NOT named public/ avoids static-output trap)
244
- preview/ Next proxy for v0 Preview only
243
+ cms-panel/ �?CMS static UI (NOT named public/ �?avoids static-output trap)
244
+ preview/ �?Next proxy for v0 Preview only
245
245
  ```
246
246
 
247
- **Preview only keep ALL Next under `preview/`:**
247
+ **Preview only �?keep ALL Next under `preview/`:**
248
248
 
249
249
  ```
250
250
  preview/
251
251
  next.config.mjs
252
252
  app/
253
-
253
+ �?
254
254
  ```
255
255
 
256
256
  **Do NOT:**
257
257
 
258
- - Leave `next.config.mjs` + `app/` at project root
259
- - Set Output Directory to `public`, `cms-panel`, or `.`
260
- - Create a **second** TomorrowOS instance in `api/ws.ts` (separate isolate = broken pairing memory)
261
- - Use `(req, res) =>` serverless handlers that drop `upgrade`
262
- - Put `startCommand` / `processes` in `vercel.json` (v0 schema rejects them)
258
+ - �?Leave `next.config.mjs` + `app/` at project root
259
+ - �?Set Output Directory to `public`, `cms-panel`, or `.`
260
+ - �?Create a **second** TomorrowOS instance in `api/ws.ts` (separate isolate = broken pairing memory)
261
+ - �?Use `(req, res) =>` serverless handlers that drop `upgrade`
262
+ - �?Put `startCommand` / `processes` in `vercel.json` (v0 schema rejects them)
263
263
 
264
264
  **`package.json` scripts:**
265
265
 
@@ -276,10 +276,10 @@ preview/
276
276
  }
277
277
  ```
278
278
 
279
- - **Local / Railway-style:** `npm start` → `tsx server.ts` SDK calls `server.listen` (no `VERCEL` env).
279
+ - **Local / Railway-style:** `npm start` �?`tsx server.ts` �?SDK calls `server.listen` (no `VERCEL` env).
280
280
  - **Vercel Production:** Function loads `api/index.ts`; SDK skips `listen` when `process.env.VERCEL` is set and Vercel owns the socket.
281
281
 
282
- ### A2 Function entry (matches Vercel WebSockets docs)
282
+ ### A2 �?Function entry (matches Vercel WebSockets docs)
283
283
 
284
284
  ```ts
285
285
  // api/index.ts
@@ -299,10 +299,10 @@ export { server, tomorrowos };
299
299
  SDK behaviour (0.9.32+):
300
300
 
301
301
  - Accepts WebSocket upgrades on `/`, `/api`, `/api/ws` (players may use either)
302
- - When `VERCEL` is set: **`autoListen` defaults to false** do not bind a port yourself
302
+ - When `VERCEL` is set: **`autoListen` defaults to false** �?do not bind a port yourself
303
303
  - Pattern aligns with docs: create `http.Server`, attach `ws`, **`export default server`**
304
304
 
305
- ### A3 — `vercel.json` (Production)
305
+ ### A3 �?`vercel.json` (Production)
306
306
 
307
307
  ```json
308
308
  {
@@ -323,7 +323,7 @@ SDK behaviour (0.9.32+):
323
323
  }
324
324
  ```
325
325
 
326
- **`maxDuration` plan limits:** Hobby / many default plans allow **1–300** seconds. **800** requires a higher Vercel plan. Prefer **`300`** unless the account already allows more otherwise Publish fails with *“maxDuration must be between 1 and 300upgrade your plan”*.
326
+ **`maxDuration` plan limits:** Hobby / many default plans allow **1�?00** seconds. **800** requires a higher Vercel plan. Prefer **`300`** unless the account already allows more �?otherwise Publish fails with *“maxDuration must be between 1 and 300�?upgrade your plan�?.
327
327
 
328
328
  **Why rewrites:** Public URLs stay `https://app.vercel.app/` and `/status` while the Function mount is `/api`. Device `wss://app.vercel.app/` is rewritten to the same Function so one isolate handles HTTP + WS.
329
329
 
@@ -333,24 +333,24 @@ SDK behaviour (0.9.32+):
333
333
 
334
334
  - `"fluid": true` is required for WebSockets
335
335
  - **Do not** set `outputDirectory` to `cms-panel` / `public`
336
- - `"buildCommand"` = no-op — **never** `next build` for Production
336
+ - `"buildCommand"` = no-op �?**never** `next build` for Production
337
337
  - Dashboard Output Directory must stay **empty**
338
338
 
339
- ### A4 Vercel Project Settings (dashboard mandatory)
339
+ ### A4 �?Vercel Project Settings (dashboard �?mandatory)
340
340
 
341
341
  | Setting | Required value |
342
342
  |---------|----------------|
343
- | Framework Preset | **Other** / null — **not Next.js** |
343
+ | Framework Preset | **Other** / null �?**not Next.js** |
344
344
  | Install Command | `npm install` |
345
345
  | Build Command | `npm run build` (no-op) |
346
346
  | **Output Directory** | **empty** |
347
347
  | Fluid compute | **On** |
348
- | WebSockets | Account must allow Functions WebSockets (see docs “Permissions Required”) |
348
+ | WebSockets | Account must allow Functions WebSockets (see docs “Permissions Required�? |
349
349
 
350
- ### A5 Publish acceptance (all must pass)
350
+ ### A5 �?Publish acceptance (all must pass)
351
351
 
352
- 1. `curl -s https://PROD/status` JSON (`{"status":...}`)
353
- 2. `curl -s https://PROD/` Control Panel HTML
352
+ 1. `curl -s https://PROD/status` �?JSON (`{"status":...}`)
353
+ 2. `curl -s https://PROD/` �?Control Panel HTML
354
354
  3. **WebSocket upgrade returns 101**, not 200 HTML:
355
355
 
356
356
  ```bash
@@ -365,28 +365,28 @@ curl.exe -i --http1.1 ^
365
365
  4. Control Panel does **not** show CMS unreachable / `Unexpected token 'T'`
366
366
  5. Fluid on; Framework = Other
367
367
 
368
- If (1)+(2) pass but (3) fails Function is serving HTTP only. Re-check Fluid, `api/index.ts` default export, rewrites, and WebSockets permission. **Do not** invent a second `api/ws.ts` TomorrowOS instance.
368
+ If (1)+(2) pass but (3) fails �?Function is serving HTTP only. Re-check Fluid, `api/index.ts` default export, rewrites, and WebSockets permission. **Do not** invent a second `api/ws.ts` TomorrowOS instance.
369
369
 
370
- ### A6 What Scheme A does **not** require
370
+ ### A6 �?What Scheme A does **not** require
371
371
 
372
- - No Next.js as Production CMS
373
- - No replacing SDK transport with hand-rolled pairing
374
- - No SQLite as production store on Vercel
375
- - No changes to Replit `cms-starter` layout for Vercel-only fixes
372
+ - �?No Next.js as Production CMS
373
+ - �?No replacing SDK transport with hand-rolled pairing
374
+ - �?No SQLite as production store on Vercel
375
+ - �?No changes to Replit `cms-starter` layout for Vercel-only fixes
376
376
 
377
- ### A7 If v0 Publish still fails (Path B / C)
377
+ ### A7 �?If v0 Publish still fails (Path B / C)
378
378
 
379
379
  **Path B:** Deploy with `vercel --prod` or Git integration (full Vercel), same Scheme A layout.
380
380
 
381
381
  **Path C:** Host CMS on **Railway / Fly.io / Replit** (long-lived Node). Use Vercel only for Preview if needed.
382
382
 
383
- ### A8 Migrate an existing broken Vercel project
383
+ ### A8 �?Migrate an existing broken Vercel project
384
384
 
385
385
  1. Scaffold/compare with `cms-starter-v0` (`init --hosting v0`) or copy:
386
386
  - `cms-app.ts`, `api/index.ts`, `server.ts`, `vercel.json`
387
- - rename `public/` → `cms-panel/` if still present
387
+ - rename `public/` �?`cms-panel/` if still present
388
388
  2. Ensure **one** Function exports the **same** `server` from `cms-app.ts`
389
- 3. Fluid on Redeploy
389
+ 3. Fluid on �?Redeploy
390
390
  4. Verify A5 (especially WebSocket 101)
391
391
  5. On TVs: enter `https://YOUR.vercel.app/` (players also try `/api` automatically on vercel.app hosts)
392
392
 
@@ -396,7 +396,7 @@ If (1)+(2) pass but (3) fails → Function is serving HTTP only. Re-check Fluid,
396
396
 
397
397
  ### Production runtime (Publish)
398
398
 
399
- - Serve TomorrowOS via **Vercel Function** `api/index.ts` → `export default server` (Fluid + WebSockets).
399
+ - Serve TomorrowOS via **Vercel Function** `api/index.ts` �?`export default server` (Fluid + WebSockets).
400
400
  - Keep root **`server.ts`** for **local** `npm start` / Preview internal port only (same `cms-app.ts`).
401
401
  - Keep **`@tomorrowos/sdk`** (0.9.32+) and put **`tsx` in `dependencies`** for local start.
402
402
  - Node **20+**.
@@ -456,7 +456,7 @@ Add `@vercel/blob` only when the user chooses **Vercel Blob** in Question 2.
456
456
 
457
457
  For Preview shell only, add as needed: `next`, `react`, `react-dom`, and a process runner (`concurrently` or equivalent). These must **not** become the Production CMS.
458
458
 
459
- ### `server.ts` pattern (Vercel-adapted Production)
459
+ ### `server.ts` pattern (Vercel-adapted �?Production)
460
460
 
461
461
  Keep the server at **project root** (e.g. `server.ts` / `server.mts`). Do **not** move Production into `api/` solely to satisfy Vercel routing myths.
462
462
 
@@ -496,14 +496,14 @@ const server = tomorrowos.listen({
496
496
  export default server;
497
497
  ```
498
498
 
499
- - **Production:** `PORT` from Vercel TomorrowOS listens on that port (no Next).
499
+ - **Production:** `PORT` from Vercel �?TomorrowOS listens on that port (no Next).
500
500
  - **Preview:** set `TOMORROWOS_INTERNAL_PORT=3001` (or similar) so Next can own `:3000` and proxy inward.
501
501
 
502
502
  Load env with `dotenv` for local/`vercel dev`. On production Deploy, Vercel injects Environment Variables natively.
503
503
 
504
- ### Preview adapter (v0 / Next reverse proxy) required when Preview is Next-hardwired
504
+ ### Preview adapter (v0 / Next reverse proxy) �?required when Preview is Next-hardwired
505
505
 
506
- Detect Preview / v0 (Next already present, or Preview blank while TomorrowOS logs “listening”). Then:
506
+ Detect Preview / v0 (Next already present, or Preview blank while TomorrowOS logs “listening�?. Then:
507
507
 
508
508
  1. Keep a **minimal** Next app **isolated when possible** (e.g. under `preview/`) so Production Framework detection does not latch onto root `next.config.*`.
509
509
  2. TomorrowOS listens on **`127.0.0.1:TOMORROWOS_INTERNAL_PORT`** (default **3001**).
@@ -526,9 +526,9 @@ Preferred UX: proxy **`/`** itself (rewrites) so the Preview URL shows the Contr
526
526
  - Deleting TomorrowOS `server.ts` because Preview needed Next.
527
527
  - Claiming setup complete if Production entrypoint is still `next start`.
528
528
  - Claiming setup complete if Production `GET /` is `404: NOT_FOUND`.
529
- - Creating root `api/index.*` + `routes` catch-all that exports a raw `http.Server` “to fix 404”.
529
+ - Creating root `api/index.*` + `routes` catch-all that exports a raw `http.Server` “to fix 404�?
530
530
 
531
- ### `vercel.json` (Production explicit Node, not Next)
531
+ ### `vercel.json` (Production �?explicit Node, not Next)
532
532
 
533
533
  Write `vercel.json` so Production cannot be mistaken for Next. Minimum:
534
534
 
@@ -545,23 +545,23 @@ Use `pnpm` / `yarn` variants of install/build if that is the project’s package
545
545
 
546
546
  **Do not** add `startCommand`, `processes`, or `outputDirectory` to `vercel.json` when v0 schema rejects them. Start = `package.json` `"start": "tsx server.ts"`. Output Directory = **empty in dashboard**.
547
547
 
548
- **Also force Project Settings (dashboard or Vercel API) this is mandatory (see Scheme A4):**
548
+ **Also force Project Settings (dashboard or Vercel API) �?this is mandatory (see Scheme A4):**
549
549
 
550
550
  | Setting | Required value |
551
551
  |---------|----------------|
552
- | Framework Preset | **Other** / null — **not Next.js** |
552
+ | Framework Preset | **Other** / null �?**not Next.js** |
553
553
  | Install Command | `npm install` (or pnpm/yarn) |
554
554
  | Build Command | `npm run build` (no-op exit 0) |
555
- | Output Directory | **empty / null** — **NOT `public`** |
556
- | Start Command | `npm run start` → `tsx server.ts` (or `.mts`) set in **dashboard**, not `vercel.json` |
555
+ | Output Directory | **empty / null** �?**NOT `public`** |
556
+ | Start Command | `npm run start` �?`tsx server.ts` (or `.mts`) �?set in **dashboard**, not `vercel.json` |
557
557
  | Fluid compute | **On** |
558
558
 
559
559
  If `next` / `next.config.*` exist for Preview and the dashboard still auto-selects Next.js:
560
560
 
561
561
  1. Patch Framework Preset to **Other** via API/CLI/dashboard.
562
562
  2. Redeploy.
563
- 3. Hit Production `GET /status` must be **JSON**, not HTML.
564
- 4. Hit Production `GET /` must be Control Panel HTML, not `404: NOT_FOUND`.
563
+ 3. Hit Production `GET /status` �?must be **JSON**, not HTML.
564
+ 4. Hit Production `GET /` �?must be Control Panel HTML, not `404: NOT_FOUND`.
565
565
 
566
566
  If Vercel’s UI and this file disagree on bundling, **prefer whatever keeps a single long-lived Node HTTP server with WebSocket upgrade on `/` for Production**. Document the adaptation in the final summary.
567
567
 
@@ -574,15 +574,15 @@ If Vercel’s UI and this file disagree on bundling, **prefer whatever keeps a s
574
574
 
575
575
  ### What not to create
576
576
 
577
- - ❌ `.replit`, `.replit-artifact`
578
- - Next.js as the **Production** CMS (App Router replacing TomorrowOS)
579
- - Root `api/` catch-all exporting raw `http.Server` to “fixPublish 404
580
- - Separate “API projectwithout the Control Panel
581
- - Committing database passwords, Cloudinary secrets, or Blob tokens
577
+ - �?`.replit`, `.replit-artifact`
578
+ - �?Next.js as the **Production** CMS (App Router replacing TomorrowOS)
579
+ - �?Root `api/` catch-all exporting raw `http.Server` to “fix�?Publish 404
580
+ - �?Separate “API project�?without the Control Panel
581
+ - �?Committing database passwords, Cloudinary secrets, or Blob tokens
582
582
 
583
583
  ---
584
584
 
585
- ## Question 1 Database (always)
585
+ ## Question 1 �?Database (always)
586
586
 
587
587
  > **Do not** ask screen counts. Present all three options; recommend **Supabase** for Vercel fleets.
588
588
  > **After the user picks a database:** collect the URL if needed, then **you** set `TOMORROWOS_STORE` + `DATABASE_SSL` (see mapping below). The customer must **never** be asked to type those two names or values.
@@ -595,71 +595,71 @@ If Vercel’s UI and this file disagree on bundling, **prefer whatever keeps a s
595
595
  | 2 Neon | `TOMORROWOS_STORE=postgres`, `DATABASE_SSL=true` | `DATABASE_URL` (pooled string) |
596
596
  | 3 SQLite | `TOMORROWOS_STORE=sqlite` | Confirmation only |
597
597
 
598
- If you open an Env configuration UI for Q1, fields visible to the user may include **only** `SUPABASE_URL` or `DATABASE_URL`. **Do not** show blank `TOMORROWOS_STORE` / `DATABASE_SSL` inputs write those yourself in the same step.
598
+ If you open an Env configuration UI for Q1, fields visible to the user may include **only** `SUPABASE_URL` or `DATABASE_URL`. **Do not** show blank `TOMORROWOS_STORE` / `DATABASE_SSL` inputs �?write those yourself in the same step.
599
599
 
600
- ### Step A Ask storage choice
600
+ ### Step A �?Ask storage choice
601
601
 
602
602
  **Ask exactly:**
603
603
 
604
604
  > Which database should TomorrowOS use on Vercel?
605
605
  >
606
- > **1. Supabase Postgres (recommended)** durable pairing/playlists; use the **Session pooler** URL (`*.pooler.supabase.com:6543`).
606
+ > **1. Supabase Postgres (recommended)** �?durable pairing/playlists; use the **Session pooler** URL (`*.pooler.supabase.com:6543`).
607
607
  >
608
- > **2. Neon Postgres** serverless Postgres native to Vercel; use Neon’s **pooled** connection string from the Neon dashboard (not the direct un-pooled host for serverless).
608
+ > **2. Neon Postgres** �?serverless Postgres native to Vercel; use Neon’s **pooled** connection string from the Neon dashboard (not the direct un-pooled host for serverless).
609
609
  >
610
- > **3. SQLite (`data/tomorrowos.db`)** — **demo / local only on Vercel.** The filesystem is ephemeral; pairings and playlists will not survive redeploys. Only choose this if you understand the limitation.
610
+ > **3. SQLite (`data/tomorrowos.db`)** �?**demo / local only on Vercel.** The filesystem is ephemeral; pairings and playlists will not survive redeploys. Only choose this if you understand the limitation.
611
611
  >
612
- > Reply with **1**, **2**, or **3** (or “Supabase / “Neon / “SQLite”).
612
+ > Reply with **1**, **2**, or **3** (or “Supabase�?/ “Neon�?/ “SQLite�?.
613
613
 
614
- ### Step B If **1 / Supabase**
614
+ ### Step B �?If **1 / Supabase**
615
615
 
616
- **Ask exactly (connection string only not TOMORROWOS_STORE / DATABASE_SSL):**
616
+ **Ask exactly (connection string only �?not TOMORROWOS_STORE / DATABASE_SSL):**
617
617
 
618
618
  > Paste your Supabase Postgres connection string. I will store it as **`SUPABASE_URL`**. I will set **`TOMORROWOS_STORE`** and **`DATABASE_SSL`** for you automatically.
619
619
  >
620
- > In Supabase: **Project Settings Database Connection string Connection pooling** (Session mode).
620
+ > In Supabase: **Project Settings �?Database �?Connection string �?Connection pooling** (Session mode).
621
621
  > Preferred shape:
622
622
  > `postgresql://postgres.[PROJECT]:[PASSWORD]@aws-0-[REGION].pooler.supabase.com:6543/postgres`
623
623
  >
624
624
  > **Do not** use the direct host `db.*.supabase.co:5432` for Vercel.
625
625
 
626
- **You must then (automatic do NOT ask the user to type store/ssl vars):**
626
+ **You must then (automatic �?do NOT ask the user to type store/ssl vars):**
627
627
 
628
628
  1. Set Vercel Env Vars in one step:
629
- - `SUPABASE_URL=<user pooler string>` from user
630
- - `TOMORROWOS_STORE=supabase` ← **you set; never ask**
631
- - `DATABASE_SSL=true` ← **you set; never ask**
629
+ - `SUPABASE_URL=<user pooler string>` �?from user
630
+ - `TOMORROWOS_STORE=supabase` �?**you set; never ask**
631
+ - `DATABASE_SSL=true` �?**you set; never ask**
632
632
  2. Wire `cms-app.ts` / `server.ts` store as in **Runtime & Vercel deploy rules**.
633
633
  3. Optional committed `.env.example` with **placeholders only**.
634
634
  4. **Do not** commit the real connection string.
635
635
  5. **Do not** open a form whose blank fields include `TOMORROWOS_STORE` or `DATABASE_SSL`.
636
636
 
637
- ### Step C If **2 / Neon**
637
+ ### Step C �?If **2 / Neon**
638
638
 
639
639
  **Ask exactly (connection string only):**
640
640
 
641
641
  > Paste your **Neon pooled** Postgres connection string. I will store it as **`DATABASE_URL`**. I will set **`TOMORROWOS_STORE`** and **`DATABASE_SSL`** for you automatically.
642
642
  >
643
- > In Neon: **Dashboard Connection details Pooled connection**.
643
+ > In Neon: **Dashboard �?Connection details �?Pooled connection**.
644
644
  > Typical shape:
645
645
  > `postgresql://[user]:[password]@[endpoint]-pooler.[region].aws.neon.tech/[dbname]?sslmode=require`
646
646
 
647
647
  **You must then (automatic):**
648
648
 
649
649
  1. Set Vercel Env Vars:
650
- - `DATABASE_URL=<user pooled string>` from user
651
- - `TOMORROWOS_STORE=postgres` ← **you set; never ask**
652
- - `DATABASE_SSL=true` ← **you set; never ask**
650
+ - `DATABASE_URL=<user pooled string>` �?from user
651
+ - `TOMORROWOS_STORE=postgres` �?**you set; never ask**
652
+ - `DATABASE_SSL=true` �?**you set; never ask**
653
653
  2. Wire `createTomorrowOSStore` for Postgres.
654
654
  3. **Do not** commit the real connection string.
655
655
  4. **Do not** ask the user to fill `TOMORROWOS_STORE` or `DATABASE_SSL` in chat or Env UI.
656
656
  5. If you see `ENETUNREACH` on `:5432`, switch to Neon’s **pooled** URL.
657
657
 
658
- ### Step D If **3 / SQLite**
658
+ ### Step D �?If **3 / SQLite**
659
659
 
660
660
  **Warn exactly before continuing:**
661
661
 
662
- > SQLite on Vercel is **not** suitable for production fleets data in `data/tomorrowos.db` is lost when the instance is recycled or redeployed. Continue only for a quick demo.
662
+ > SQLite on Vercel is **not** suitable for production fleets �?data in `data/tomorrowos.db` is lost when the instance is recycled or redeployed. Continue only for a quick demo.
663
663
 
664
664
  Only proceed after the user explicitly confirms.
665
665
 
@@ -673,29 +673,29 @@ Only proceed after the user explicitly confirms.
673
673
 
674
674
  ---
675
675
 
676
- ## Question 2 Media storage
676
+ ## Question 2 �?Media storage
677
677
 
678
678
  > **This question is only about media files (images/videos).**
679
- > It is **not** about OpenAI, AI pairing, or LLM keys. If you are about to ask for an `sk-` key, **stop** you are off-protocol.
680
- > **Copy the three options below verbatim.** Wrong examples that must **never** appear: “Vercel Blob (recommended)”, “Supabase Storage”, “No media storage / disable uploads”.
679
+ > It is **not** about OpenAI, AI pairing, or LLM keys. If you are about to ask for an `sk-` key, **stop** �?you are off-protocol.
680
+ > **Copy the three options below verbatim.** Wrong examples that must **never** appear: “Vercel Blob (recommended)�? “Supabase Storage�? “No media storage / disable uploads�?
681
681
 
682
- ### Step A Ask storage choice
682
+ ### Step A �?Ask storage choice
683
683
 
684
- **Ask exactly (wording must match Cloudinary is recommended):**
684
+ **Ask exactly (wording must match �?Cloudinary is recommended):**
685
685
 
686
686
  > How should playlist media (images/videos) be stored?
687
687
  >
688
- > **1. Cloudinary (recommended)** durable public HTTPS URLs (`https://res.cloudinary.com/...`). Works out of the box with `@tomorrowos/sdk` auto-detection. **Prefer this on Vercel.**
688
+ > **1. Cloudinary (recommended)** �?durable public HTTPS URLs (`https://res.cloudinary.com/...`). Works out of the box with `@tomorrowos/sdk` auto-detection. **Prefer this on Vercel.**
689
689
  >
690
- > **2. Vercel Blob** Vercel-native object storage; durable `https://*.public.blob.vercel-storage.com/...` URLs. Use when you want media on the same Vercel project without a Cloudinary account.
690
+ > **2. Vercel Blob** �?Vercel-native object storage; durable `https://*.public.blob.vercel-storage.com/...` URLs. Use when you want media on the same Vercel project without a Cloudinary account.
691
691
  >
692
- > **3. Local disk only** — `cms-panel/uploads` or `public/uploads` (OK for quick local tests; **not** for production Vercel fleets files are ephemeral).
692
+ > **3. Local disk only** �?`cms-panel/uploads` or `public/uploads` (OK for quick local tests; **not** for production Vercel fleets �?files are ephemeral).
693
693
  >
694
- > Reply with **1**, **2**, or **3** (or “Cloudinary / “Vercel Blob / “local”).
694
+ > Reply with **1**, **2**, or **3** (or “Cloudinary�?/ “Vercel Blob�?/ “local�?.
695
695
 
696
- If you catch yourself about to offer Blob-as-recommended, Supabase Storage, or “no media”, **stop and paste the block above instead**.
696
+ If you catch yourself about to offer Blob-as-recommended, Supabase Storage, or “no media�? **stop and paste the block above instead**.
697
697
 
698
- ### Step B If **1 / Cloudinary** (same Question 2 — **one Env popup**)
698
+ ### Step B �?If **1 / Cloudinary** (same Question 2 �?**one Env popup**)
699
699
 
700
700
  > **Critical UX rule:** Immediately open the **Vercel Environment Variables configuration popup / form** with **all Cloudinary fields in one UI**. Do **not** ask for cloud name, then key, then secret in separate chat messages.
701
701
 
@@ -726,13 +726,13 @@ If you catch yourself about to offer Blob-as-recommended, Supabase Storage, or
726
726
 
727
727
  The SDK auto-detects these env vars.
728
728
 
729
- ### Step C If **2 / Vercel Blob** (same Question 2)
729
+ ### Step C �?If **2 / Vercel Blob** (same Question 2)
730
730
 
731
731
  **Ask exactly:**
732
732
 
733
733
  > I will enable **Vercel Blob** for media uploads.
734
734
  >
735
- > 1. In the Vercel project: **Storage Create Blob** (or link an existing Blob store to this project).
735
+ > 1. In the Vercel project: **Storage �?Create �?Blob** (or link an existing Blob store to this project).
736
736
  > 2. Confirm **`BLOB_READ_WRITE_TOKEN`** is available (Vercel usually injects it when Blob is linked).
737
737
  >
738
738
  > Paste the token only if it is not already set in your project Env Vars. Do you already have Blob linked on this Vercel project? (yes / no)
@@ -749,57 +749,57 @@ The SDK auto-detects these env vars.
749
749
  import { put } from "@vercel/blob";
750
750
 
751
751
  // On upload: const blob = await put(filename, body, { access: "public", token: process.env.BLOB_READ_WRITE_TOKEN });
752
- // Store blob.url in uploaded_assets same shape as Cloudinary secure URLs.
752
+ // Store blob.url in uploaded_assets �?same shape as Cloudinary secure URLs.
753
753
  ```
754
754
 
755
- 4. Warn: until the project’s Blob bridge is wired, Control Panel **Media** status may show local/ephemeral verify uploads return `https://*.blob.vercel-storage.com/...` before calling setup complete.
755
+ 4. Warn: until the project’s Blob bridge is wired, Control Panel **Media** status may show local/ephemeral �?verify uploads return `https://*.blob.vercel-storage.com/...` before calling setup complete.
756
756
  5. Do **not** proceed to Question 3 until `BLOB_READ_WRITE_TOKEN` exists (or Blob is linked and token is confirmed in the Vercel dashboard).
757
757
 
758
- ### Step D If **3 / local uploads**
758
+ ### Step D �?If **3 / local uploads**
759
759
 
760
760
  1. `mkdir -p public/uploads`
761
- 2. Warn clearly: files may vanish on redeploy; players need stable HTTPS URLs Cloudinary or Vercel Blob is strongly preferred for Vercel.
761
+ 2. Warn clearly: files may vanish on redeploy; players need stable HTTPS URLs �?Cloudinary or Vercel Blob is strongly preferred for Vercel.
762
762
 
763
763
  ---
764
764
 
765
- ## Question 3 Brand (`brand.json` only)
765
+ ## Question 3 �?Brand (`brand.json` only)
766
766
 
767
767
  > Updates **only** `brand.json`. Does **not** change Vercel project settings, Env Vars, or `server.ts` store wiring beyond what Q1–Q2 already required.
768
768
  >
769
- > **IRON RULE website URL build a website / login / auth CMS.**
770
- > If the user pastes a URL (or says “make it look like this site”), that input is **reference material for `brand.json` only** (name, colours, fonts, logo, tagline).
771
- > **Do not** scaffold a login page, signup, OAuth, gated dashboard, or copy the reference site’s IA/pages — **unless the user explicitly asks for CMS login / auth**.
769
+ > **IRON RULE �?website URL �?build a website / login / auth CMS.**
770
+ > If the user pastes a URL (or says “make it look like this site�?, that input is **reference material for `brand.json` only** (name, colours, fonts, logo, tagline).
771
+ > **Do not** scaffold a login page, signup, OAuth, gated dashboard, or copy the reference site’s IA/pages �?**unless the user explicitly asks for CMS login / auth**.
772
772
  > Default TomorrowOS Control Panel has **no login**. Keep it that way.
773
773
 
774
- ### Step A Ask branding input
774
+ ### Step A �?Ask branding input
775
775
 
776
776
  **Ask exactly:**
777
777
 
778
778
  > Let’s brand your TomorrowOS experience. You can answer in either way:
779
779
  >
780
- > **Option A Website URL only (brand reference)**
780
+ > **Option A �?Website URL only (brand reference)**
781
781
  > Paste **one public website URL**. I will use it **only** to infer colours, fonts, name, tagline, and logo for **`brand.json`**.
782
- > I will **not** rebuild that website, add a login page, or change Control Panel features unless you explicitly ask for login/auth later.
782
+ > I will **not** rebuild that website, add a login page, or change Control Panel features �?unless you explicitly ask for login/auth later.
783
783
  >
784
- > **Option B Manual fields**
784
+ > **Option B �?Manual fields**
785
785
  > Provide:
786
786
  > 1. **Product / venue name**
787
787
  > 2. **Tagline** (optional)
788
788
  > 3. **Primary colour** (hex, e.g. `#FF8A3D`)
789
- > 4. **Background colour** (hex, optional default `#FAFAF9`)
790
- > 5. **Text colour** (hex, optional default `#0A0908`)
789
+ > 4. **Background colour** (hex, optional �?default `#FAFAF9`)
790
+ > 5. **Text colour** (hex, optional �?default `#0A0908`)
791
791
  > 6. **Secondary / accent colour** (hex, optional)
792
- > 7. **Logo** upload SVG/PNG into the project, or a URL I can fetch into `./assets/`
792
+ > 7. **Logo** �?upload SVG/PNG into the project, or a URL I can fetch into `./assets/`
793
793
 
794
- ### Step B If the user gives **only a website URL** (Option A)
794
+ ### Step B �?If the user gives **only a website URL** (Option A)
795
795
 
796
796
  **You must:**
797
797
 
798
798
  1. **Fetch and inspect** the page (HTTP GET the URL; follow one redirect if needed). Do not invent colours.
799
799
  2. **Write / update `brand.json` only** from what you infer. Touch nothing else for this step (no new pages, no auth, no Next marketing site, no login UI).
800
800
  3. **Derive branding** using this priority order:
801
- - **Name:** `<title>`, `og:site_name`, or prominent header / logo `alt` text (trim to 60 chars).
802
- - **Tagline:** `meta[name="description"]`, `og:description`, or first hero subtitle (120 chars).
801
+ - **Name:** `<title>`, `og:site_name`, or prominent header / logo `alt` text (trim to �?60 chars).
802
+ - **Tagline:** `meta[name="description"]`, `og:description`, or first hero subtitle (�?120 chars).
803
803
  - **Primary colour:** `meta[name="theme-color"]`, CSS `--primary` / `--brand` variables, or dominant accent from linked stylesheets / inline styles (convert to `#RRGGBB`).
804
804
  - **Background colour:** `body` / `:root` background (default `#FAFAF9` if light site).
805
805
  - **Text colour:** main body text colour (default `#0A0908` if light site).
@@ -808,20 +808,20 @@ import { put } from "@vercel/blob";
808
808
  - **Logo:** prefer `og:image`, then `link[rel="icon"]` / apple-touch-icon, then header `<img>` logo. Download into `./assets/logo.png` or `./assets/logo.svg` and set `logoPath` accordingly. If only favicon exists, use it and note size in the summary.
809
809
  4. **Contrast check:** if background is dark, set `activationScreen.theme` to `"dark"` and add `logoPathOnDark` when a light logo variant exists.
810
810
  5. **Infer `cms.useCase`** from page content when obvious (restaurant / retail / corporate / etc.); otherwise `"other"`.
811
- 6. **Show a one-line summary** to the user (name + primary hex + logo path) before or while writing the file do not ask a second branding question unless fetch failed.
811
+ 6. **Show a one-line summary** to the user (name + primary hex + logo path) before or while writing the file �?do not ask a second branding question unless fetch failed.
812
812
  7. If the URL is unreachable, blocked, or has no usable styles, say so and fall back to **Option B** manual questions for missing fields only.
813
813
 
814
814
  **Explicitly forbidden when the user only gave a URL (no login request):**
815
815
 
816
- - Login / signup / “Sign in to continuescreens
817
- - Auth middleware, sessions, cookies, OAuth, password forms
818
- - Cloning the reference site as a marketing landing or multi-page CMS
819
- - Gating the Control Panel behind authentication
820
- - Treating “make it like this siteas “rebuild this site
816
+ - �?Login / signup / “Sign in to continue�?screens
817
+ - �?Auth middleware, sessions, cookies, OAuth, password forms
818
+ - �?Cloning the reference site as a marketing landing or multi-page CMS
819
+ - �?Gating the Control Panel behind authentication
820
+ - �?Treating “make it like this site�?as “rebuild this site�?
821
821
 
822
- If the reference site itself is a login page, **still only extract visual brand tokens** (colours/logo/name) into `brand.json` do **not** recreate that login flow.
822
+ If the reference site itself is a login page, **still only extract visual brand tokens** (colours/logo/name) into `brand.json` �?do **not** recreate that login flow.
823
823
 
824
- ### Step C Write `brand.json`
824
+ ### Step C �?Write `brand.json`
825
825
 
826
826
  Validate mentally against `brand.schema.json`. Minimum:
827
827
 
@@ -851,11 +851,11 @@ Validate mentally against `brand.schema.json`. Minimum:
851
851
  }
852
852
  ```
853
853
 
854
- Always set **`cms.hostingTarget`: `"vercel"`** without asking. Default `expectedScreens` to `5` unless the user already volunteered a number. Set `cmsEndpoint` only if they already know the public `https://…vercel.app` URL; otherwise tell them to pair TVs with the **Published** HTTPS origin (player maps `https://` → `wss://`).
854
+ Always set **`cms.hostingTarget`: `"vercel"`** without asking. Default `expectedScreens` to `5` unless the user already volunteered a number. Set `cmsEndpoint` only if they already know the public `https://…vercel.app` URL; otherwise tell them to pair TVs with the **Published** HTTPS origin (player maps `https://` �?`wss://`).
855
855
 
856
856
  ---
857
857
 
858
- ## After all answers execution checklist
858
+ ## After all answers �?execution checklist
859
859
 
860
860
  ### A. Seed the project
861
861
 
@@ -865,11 +865,11 @@ npx @tomorrowos/sdk@latest init .
865
865
  # npx @tomorrowos/sdk@latest init . --force
866
866
  ```
867
867
 
868
- Then (**Scheme A mandatory for Publish**):
868
+ Then (**Scheme A �?mandatory for Publish**):
869
869
 
870
870
  1. Keep **`server.ts`** at **project root** with top-level `listen()` + `export default server`.
871
- 2. If Preview needs Next: **move** `next.config.*`, `app/`, Next `tsconfig` / `postcss` / `next-env.d.ts` into **`preview/`** remove them from root so Production is not mistaken for Next/static.
872
- 3. Write `vercel.json` per **Scheme A3** (`fluid`, `framework: null`, no-op `buildCommand` — **no** `startCommand`).
871
+ 2. If Preview needs Next: **move** `next.config.*`, `app/`, Next `tsconfig` / `postcss` / `next-env.d.ts` into **`preview/`** �?remove them from root so Production is not mistaken for Next/static.
872
+ 3. Write `vercel.json` per **Scheme A3** (`fluid`, `framework: null`, no-op `buildCommand` �?**no** `startCommand`).
873
873
  4. **Force Project Settings** per **Scheme A4** (Framework = Other, Output Directory empty, Start = `npm run start`, Fluid on). Patch dashboard via API if it still says Next.js.
874
874
  5. Do **not** add root `api/` catch-all for raw `http.Server`.
875
875
  6. Do **not** add `.replit*`.
@@ -881,8 +881,8 @@ Then (**Scheme A — mandatory for Publish**):
881
881
  |------|----------|--------------|
882
882
  | `SUPABASE_URL` | If Q1 = Supabase | User pastes URL |
883
883
  | `DATABASE_URL` | If Q1 = Neon | User pastes URL |
884
- | `TOMORROWOS_STORE` | Yes | **Agent auto-sets only** never a blank field for the user (`supabase` / `postgres` / `sqlite`) |
885
- | `DATABASE_SSL` | Yes for Postgres | **Agent auto-sets only** never a blank field for the user (`true`) |
884
+ | `TOMORROWOS_STORE` | Yes | **Agent auto-sets only** �?never a blank field for the user (`supabase` / `postgres` / `sqlite`) |
885
+ | `DATABASE_SSL` | Yes for Postgres | **Agent auto-sets only** �?never a blank field for the user (`true`) |
886
886
  | `CLOUDINARY_*` | If Q2 = Cloudinary | User via **one Env popup** (all fields) |
887
887
  | `BLOB_READ_WRITE_TOKEN` | If Q2 = Vercel Blob | User / Blob link |
888
888
  | `TOMORROWOS_INTERNAL_PORT` | Preview only | Agent (e.g. `3001`) |
@@ -904,40 +904,40 @@ Confirm browser:
904
904
  - **Preview:** Control Panel visible through the Next proxy at `/`
905
905
  - **Production start:** Control Panel served directly by TomorrowOS at `/`
906
906
 
907
- ### D. Deploy (Publish Node only)
907
+ ### D. Deploy (Publish �?Node only)
908
908
 
909
909
  ```bash
910
910
  vercel --prod
911
- # or git push Vercel Git integration
911
+ # or git push �?Vercel Git integration
912
912
  ```
913
913
 
914
- **Publish acceptance checklist (all must pass Scheme A5):**
914
+ **Publish acceptance checklist (all must pass �?Scheme A5):**
915
915
 
916
- 1. Framework Preset = **Other** / null (dashboard confirms not Next.js)
916
+ 1. Framework Preset = **Other** / null (dashboard confirms �?not Next.js)
917
917
  2. Output Directory = **empty** (not `public`)
918
918
  3. Start Command = Node `TomorrowOS.listen` (`npm run start` / `tsx server.ts`)
919
919
  4. Fluid compute on
920
920
  5. Env Vars present for Production
921
- 6. Production `GET /status` returns **JSON** — **not** HTML (`Unexpected token 'T'` means this failed)
922
- 7. Production `GET /` returns Control Panel HTML — **not** `404: NOT_FOUND`
921
+ 6. Production `GET /status` returns **JSON** �?**not** HTML (`Unexpected token 'T'` means this failed)
922
+ 7. Production `GET /` returns Control Panel HTML �?**not** `404: NOT_FOUND`
923
923
 
924
- If step 6 fails but step 7 passes: **static `public/` trap** Vercel is not running Node. Re-apply Scheme A3–A4; try **Path B** (Git / `vercel --prod`); do **not** invent `api/` + `rewrites`.
924
+ If step 6 fails but step 7 passes: **static `public/` trap** �?Vercel is not running Node. Re-apply Scheme A3–A4; try **Path B** (Git / `vercel --prod`); do **not** invent `api/` + `rewrites`.
925
925
 
926
926
  Tell the user:
927
927
 
928
928
  - **Control Panel (Publish):** `https://YOUR-PROJECT.vercel.app`
929
929
  - **TV CMS endpoint:** same HTTPS origin (players use `wss://`)
930
- - **Preview:** Next shell is for v0 only do not pair devices to Preview
930
+ - **Preview:** Next shell is for v0 only �?do not pair devices to Preview
931
931
 
932
932
  ### E. Minimal verification only
933
933
 
934
934
  **Do:**
935
935
 
936
936
  1. Preview: Control Panel HTML at `/` (via Next proxy in `preview/` if applicable)
937
- 2. Publish: `GET /status` JSON; `GET /` Control Panel from **Node** TomorrowOS
937
+ 2. Publish: `GET /status` �?JSON; `GET /` �?Control Panel from **Node** TomorrowOS
938
938
  3. Publish: panel does **not** show `Unexpected token 'T'` / CMS unreachable
939
939
  4. Server status: Media OK when Cloudinary or Blob configured
940
- 5. Database: if ERROR shows **`ENETUNREACH` IPv6 … `:5432`**, switch to **pooled** URL (Supabase `:6543` or Neon pooler) and redeploy call this out explicitly to the user
940
+ 5. Database: if ERROR shows **`ENETUNREACH` �?IPv6 �?`:5432`**, switch to **pooled** URL (Supabase `:6543` or Neon pooler) and redeploy �?call this out explicitly to the user
941
941
  6. Fluid compute on for Production
942
942
 
943
943
  **Do not block** on long WebSocket/device pairing tests unless the user asks. Prefer pairing against **Publish**, not Preview.
@@ -949,25 +949,25 @@ Tell the user:
949
949
  | Symptom | Likely cause | Fix |
950
950
  |---------|--------------|-----|
951
951
  | Publish `404: NOT_FOUND` | Framework Preset still **Next.js** / no Function | Scheme A: Framework = **Other**, Fluid on, `api/index.ts` + rewrites; redeploy |
952
- | Panel loads but **CMS unreachable** / `Unexpected token 'T', "The page c"...` | **`public/` deployed as static site** no Function | Rename to `cms-panel/`; Output empty; Scheme A Function entry; **Path B** if needed |
953
- | Only `/api/status` returns JSON, `/status` is 404 | Missing **rewrites** to `/api` | Add Scheme A3 rewrite `/(.*) → /api` (exclude existing `/api/`); redeploy |
952
+ | Panel loads but **CMS unreachable** / `Unexpected token 'T', "The page c"...` | **`public/` deployed as static site** �?no Function | Rename to `cms-panel/`; Output empty; Scheme A Function entry; **Path B** if needed |
953
+ | Only `/api/status` returns JSON, `/status` is 404 | Missing **rewrites** to `/api` | Add Scheme A3 rewrite `/(.*) �?/api` (exclude existing `/api/`); redeploy |
954
954
  | Control Panel OK, TV **could not connect** | WebSocket upgrade not reaching Function (200 HTML) | Fluid on; `export default server`; verify 101 on `/` and `/api`; check WebSockets permission |
955
955
  | `vercel.json` error: invalid `startCommand` | v0 schema rejects it | Remove from `vercel.json`; Function entry does not need startCommand |
956
- | Separate `api/ws.ts` with a second `new TomorrowOS` | Two isolates pairing memory split | **One** Function only (`api/index.ts` shared `cms-app.ts`) |
956
+ | Separate `api/ws.ts` with a second `new TomorrowOS` | Two isolates �?pairing memory split | **One** Function only (`api/index.ts` �?shared `cms-app.ts`) |
957
957
  | `vercel inspect` shows Output = `public if it exists` | Framework=Other static default | Force Output Directory **empty**; use `cms-panel/`; Path B redeploy |
958
958
  | Build runs `next build` / missing routes-manifest | Next auto-detected from root `next.config` / `app/` | Move Next to `preview/`; Framework null/Other; `build` = no-op |
959
- | Preview blank but logs show TomorrowOS listening | v0 Next intercepts public port | Add Next reverse proxy internal TomorrowOS port |
959
+ | Preview blank but logs show TomorrowOS listening | v0 Next intercepts public port | Add Next reverse proxy �?internal TomorrowOS port |
960
960
  | Preview works, Publish broken / Next-only | Production still on Next | Switch Production to Fluid Function `api/index.ts` |
961
961
  | `ENETUNREACH` / IPv6 / `:5432` | Direct Postgres URL | Use **pooled** URL (Supabase `:6543` or Neon pooler) |
962
962
  | Control Panel OK, devices never stay paired after restart | SQLite / ephemeral disk | Use Supabase or Neon |
963
963
  | Uploads break / broken thumbs in prod | Local uploads only | Cloudinary or Vercel Blob |
964
- | Blob uploads 401 / missing token | Blob not linked | Vercel Storage Blob + `BLOB_READ_WRITE_TOKEN` |
964
+ | Blob uploads 401 / missing token | Blob not linked | Vercel Storage �?Blob + `BLOB_READ_WRITE_TOKEN` |
965
965
  | WebSocket fails on Publish | Fluid off / static-only / Next as Production / no Function WS | Enable Fluid; Scheme A; test 101 upgrade |
966
- | Devices fail only on Preview | Next proxy cannot upgrade `wss` | Expected pair on Publish URL |
966
+ | Devices fail only on Preview | Next proxy cannot upgrade `wss` | Expected �?pair on Publish URL |
967
967
  | Agent asked user to fill `TOMORROWOS_STORE` / `DATABASE_SSL` | Off-protocol | Agent must auto-set after DB choice; remove those fields from user Env forms |
968
- | Agent offered Blob-as-recommended / Supabase Storage / “no mediafor Q2 | Invented options | Re-ask Q2 verbatim: **1 Cloudinary (recommended)**, 2 Blob, 3 local |
968
+ | Agent offered Blob-as-recommended / Supabase Storage / “no media�?for Q2 | Invented options | Re-ask Q2 verbatim: **1 Cloudinary (recommended)**, 2 Blob, 3 local |
969
969
  | Agent asked Cloudinary key/secret in three chat turns | Off-protocol | Use **one Env popup** with all Cloudinary fields |
970
- | Agent asked for OpenAI / `sk-` key during setup | Hallucinated “AI pairingrequirement | **Refuse.** TomorrowOS does not need LLM keys. Return to Q2 media choices only |
970
+ | Agent asked for OpenAI / `sk-` key during setup | Hallucinated “AI pairing�?requirement | **Refuse.** TomorrowOS does not need LLM keys. Return to Q2 media choices only |
971
971
  | Agent built a login page after user pasted a URL | Misread brand reference as product scope | Remove login; keep starter Control Panel; apply URL only to `brand.json` |
972
972
  | Secrets in git | Mistake | Rotate keys; move to Vercel Env Vars |
973
973
  | Replit files present | Copied wrong protocol | Delete `.replit*`; use this file |
@@ -985,22 +985,22 @@ Tell the user:
985
985
  | Media | Cloudinary or Replit Object Storage | Cloudinary **or Vercel Blob** |
986
986
  | Preview | Replit Preview / Autoscale Node | **Next shell + proxy** when v0 hardwires Next |
987
987
  | Publish | Long-lived Node | **Node `TomorrowOS.listen` only** (not Next); gate on no `404 NOT_FOUND` |
988
- | Brand from URL | Not in Replit protocol | **Option A** infer **`brand.json` only**; never invent login |
988
+ | Brand from URL | Not in Replit protocol | **Option A** �?infer **`brand.json` only**; never invent login |
989
989
 
990
990
  ---
991
991
 
992
- ## Final summary template (Agent human)
992
+ ## Final summary template (Agent �?human)
993
993
 
994
994
  After setup, report:
995
995
 
996
996
  1. **Live URL** (Production Publish)
997
- 2. **Publish health:** Framework = Other; Output ≠ `public`; `GET /status` JSON; `GET /` OK
997
+ 2. **Publish health:** Framework = Other; Output �?`public`; `GET /status` �?JSON; `GET /` OK
998
998
  3. **Preview note:** Next proxy shell used / not needed
999
- 4. **Env Vars set** (names only never values); note which were auto-set (`TOMORROWOS_STORE`, `DATABASE_SSL`)
999
+ 4. **Env Vars set** (names only �?never values); note which were auto-set (`TOMORROWOS_STORE`, `DATABASE_SSL`)
1000
1000
  5. **Store:** Supabase pooler / Neon pooled / SQLite (with warning)
1001
1001
  6. **Media:** Cloudinary, Vercel Blob, or local (with warning)
1002
1002
  7. **Brand:** name + `hostingTarget: vercel` (+ note if derived from website URL)
1003
- 8. **Runtime split:** Preview = ; Publish = Node TomorrowOS (confirm **not** Next)
1003
+ 8. **Runtime split:** Preview = �?; Publish = Node TomorrowOS (confirm **not** Next)
1004
1004
  9. **Open action:** if Database still ERROR, paste the pooler instruction again
1005
1005
 
1006
1006
  Protocol complete when:
@@ -1009,4 +1009,4 @@ Protocol complete when:
1009
1009
  - **Publish** returns Control Panel at `/` **and** `/status` JSON from pure Node `TomorrowOS.listen` with Fluid (not static `public/`, not `404: NOT_FOUND`, not `Unexpected token 'T'`), **and**
1010
1010
  - Q1–Q3 configuration is saved
1011
1011
 
1012
- even if a sandbox DB probe still flakes **after** the pooled URL is correctly set for Production.
1012
+ �?even if a sandbox DB probe still flakes **after** the pooled URL is correctly set for Production.