@tomorrowos/sdk 0.9.21 → 0.9.23

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -4,7 +4,7 @@
4
4
 
5
5
  **On Replit:** if the user asks to set up / configure a CMS interactively, follow **`REPLIT_SETUP.md`** instead (Q&A for store → media → `brand.json`).
6
6
 
7
- **On Vercel:** if the user asks to set up / configure a CMS interactively, follow **`VERCEL_SETUP.md`** (Session pooler Supabase URL, Fluid compute + WebSocket, Cloudinary, `cms.hostingTarget: "vercel"`).
7
+ **On Vercel:** if the user asks to set up / configure a CMS interactively, follow **`VERCEL_SETUP.md`** (Supabase or Neon pooled Postgres, Cloudinary or Vercel Blob media, v0 Preview Next proxy shell + **Publish = Node only**, Fluid compute + WebSocket, `cms.hostingTarget: "vercel"`).
8
8
 
9
9
  TomorrowOS is the open standard for digital signage. This SDK lets you build a CMS that controls Samsung Tizen, BrightSign, LG webOS, Android, ChromeOS, and Windows screens through a single unified API.
10
10
 
@@ -71,8 +71,10 @@ Follow @tomorrowos/sdk VERCEL_SETUP.md and set up my TomorrowOS CMS.
71
71
  IF any step contradicts Vercel, adapt to settings that Vercel can accept.
72
72
  ```
73
73
 
74
- That wizard requires the Supabase **Session pooler** URL, prefers Cloudinary,
75
- enables Fluid compute for WebSockets, and sets `cms.hostingTarget` to `"vercel"`.
74
+ That wizard asks: database (**Supabase** **Neon** → SQLite demo), media (**Cloudinary** → **Vercel Blob** → local),
75
+ branding (manual fields or **infer from a website URL**). Preview may use a thin **Next.js reverse proxy**;
76
+ **Publish stays pure Node** `TomorrowOS.listen` (not Next). Cloudinary credentials are collected one-at-a-time
77
+ (cloud name → API key → API secret). Sets `cms.hostingTarget` to `"vercel"`.
76
78
 
77
79
  ### Replit upgrade (existing CMS)
78
80
 
package/VERCEL_SETUP.md CHANGED
@@ -5,7 +5,7 @@
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.0` — pairs with `@tomorrowos/sdk` packages that ship `templates/cms-starter` and this file.
8
+ > Protocol id: `vercel-setup/1.2` — pairs with `@tomorrowos/sdk` packages that ship `templates/cms-starter` and this file.
9
9
 
10
10
  ## User trigger (copy-paste)
11
11
 
@@ -15,8 +15,9 @@ When a human pastes a prompt like this, **read this file and start Question 1 im
15
15
  Follow @tomorrowos/sdk VERCEL_SETUP.md and set up my TomorrowOS CMS.
16
16
  Ask only the questions listed in VERCEL_SETUP.md, in order. Do not use LLM_PROMPT.md or REPLIT_SETUP.md.
17
17
  If any step contradicts Vercel, adapt to settings Vercel can accept (document the adaptation).
18
- After all setup questions are answered (including Cloudinary Env Vars if you chose Cloudinary), configure and deploy — minimal verification only (Control Panel at /). Prefer Supabase Session pooler URL. Do not treat sandbox ENETUNREACH / IPv6 :5432 errors as a hard setup failure when the pooler URL is configured for production.
18
+ After all setup questions are answered (including Cloudinary or Vercel Blob Env Vars if you chose those), configure and deploy — minimal verification only (Control Panel at /). Prefer Supabase Session pooler URL or Neon pooled URL. Do not treat sandbox ENETUNREACH / IPv6 :5432 errors as a hard setup failure when a pooler URL is configured for production.
19
19
  Question 3 only updates brand.json. Do not skip steps.
20
+ Use Next.js only as a v0/Preview shell with a reverse proxy; Production Publish must remain pure Node TomorrowOS.listen (not Next).
20
21
  ```
21
22
 
22
23
  Alternate triggers:
@@ -28,7 +29,7 @@ When any of these fire, **ignore `REPLIT_SETUP.md` and `LLM_PROMPT.md`** for the
28
29
 
29
30
  ---
30
31
 
31
- ## IRON RULE: long-lived Node + WebSocket (not classic static / short Functions)
32
+ ## IRON RULE: Preview vs Publish (dual runtime)
32
33
 
33
34
  > **This is the #1 deployability rule on Vercel. Non-negotiable.**
34
35
 
@@ -37,16 +38,35 @@ TomorrowOS CMS is **one Node process** that serves:
37
38
  - Browser Control Panel at `GET /` (`public/` via `staticRoot`)
38
39
  - Device **WebSocket** on the same origin (`https` → players use `wss`)
39
40
 
41
+ ### Production / Publish — Node only (no Next.js)
42
+
40
43
  | Required | Forbidden |
41
44
  |----------|-----------|
42
- | Single **Node HTTP server** entry (`tsx server.ts` / exported `http.Server` from `listen()`) | Next.js / Remix / static-only site as the CMS |
45
+ | Single **Node HTTP server** entry (`tsx server.ts` / exported `http.Server` from `listen()`) | **Next.js as the Production CMS** (`next start`, Next App Router as the live app) |
43
46
  | **Fluid compute** enabled (needed for WebSockets on Vercel) | Classic short-lived serverless with no WebSocket / no Fluid |
44
- | Durable store = **Supabase Postgres** (pooler URL) | **SQLite / `data/*.db` as production store** on Vercel (ephemeral filesystem) |
45
- | Media = **Cloudinary** (recommended) or other durable HTTPS URLs | Relying on local `public/uploads` alone for production fleets |
46
- | Env vars in **Vercel Project → Settings → Environment Variables** | Committing passwords / Cloudinary secrets / `.env` with secrets |
47
+ | Durable store = **Postgres** (Supabase or Neon, pooled URL) | **SQLite / `data/*.db` as production store** on Vercel (ephemeral filesystem) |
48
+ | Media = **Cloudinary** or **Vercel Blob** (durable HTTPS URLs) | Relying on local `public/uploads` alone for production fleets |
49
+ | Env vars in **Vercel Project → Settings → Environment Variables** | Committing passwords / API secrets / `.env` with secrets |
47
50
  | `cms.hostingTarget`: `"vercel"` in `brand.json` | Creating `.replit` / Replit artifact files |
48
51
 
49
- **Do not** scaffold a leftover Next.js app “and put TomorrowOS beside it.” Remove Next scaffolds so TomorrowOS `server.ts` is the sole app.
52
+ **Publish / Production Deploy** must run **`TomorrowOS.listen(...)`** directly. Do **not** put Next.js in front of Production traffic.
53
+
54
+ ### Preview (v0 / Vercel Agent sandbox) — Next.js shell allowed
55
+
56
+ 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.
57
+
58
+ **For Preview only**, you **must** keep a thin Next.js host that **reverse-proxies** HTTP to TomorrowOS on an **internal** port.
59
+
60
+ | Preview requirement | Detail |
61
+ |---------------------|--------|
62
+ | Next.js remains the Preview host | Satisfies v0 / sandbox expectations |
63
+ | TomorrowOS on internal port | e.g. `TOMORROWOS_INTERNAL_PORT=3001` (not the public Preview port) |
64
+ | Transparent HTTP reverse proxy | Next route/middleware forwards `/` (and Control Panel paths) → `http://127.0.0.1:3001` |
65
+ | Start both processes in Preview | Next + `tsx server.ts` (or a `dev:preview` script that starts both) |
66
+
67
+ **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.
68
+
69
+ **Do not** replace TomorrowOS with a Next-only CMS. Next is a **Preview adapter**, not the product.
50
70
 
51
71
  ---
52
72
 
@@ -56,15 +76,15 @@ Ask **only** these questions, in **this exact order**:
56
76
 
57
77
  | Step | Section | When |
58
78
  |------|---------|------|
59
- | 1 | **Question 1** — Supabase **Session pooler** connection string | Always |
60
- | 2 | **Question 2** — Media storage (+ Cloudinary Env Vars if chosen) | Always (after Q1) |
79
+ | 1 | **Question 1** — Database (Supabase Neon SQLite) | Always |
80
+ | 2 | **Question 2** — Media storage (Cloudinary Vercel Blob → local) | Always (after Q1) |
61
81
  | 3 | **Question 3** — Brand / TomorrowOS look (`brand.json` only) | Always last |
62
82
 
63
- **That is the complete list.** Do **not** ask how many screens / devices. There is **no** “Question 2b” — Cloudinary credentials stay **inside Question 2**.
83
+ **That is the complete list.** Do **not** ask how many screens / devices.
64
84
 
65
85
  If the user says “just set it up, don’t ask questions,” respond:
66
86
 
67
- > I need a few quick answers from VERCEL_SETUP.md (Supabase Session pooler URL, media storageplus Cloudinary credentials if you choose Cloudinary — and branding). It takes about a minute and ensures the CMS works on Vercel.
87
+ > I need a few quick answers from VERCEL_SETUP.md (database Supabase or Neon preferredmedia storage, and branding). It takes about a minute and ensures the CMS works on Vercel.
68
88
 
69
89
  Then ask **Question 1**.
70
90
 
@@ -73,17 +93,18 @@ Then ask **Question 1**.
73
93
  ## Hard rules
74
94
 
75
95
  1. **Ask only VERCEL_SETUP.md questions 1–3.** Never use `LLM_PROMPT.md` or `REPLIT_SETUP.md` as the questionnaire.
76
- 2. **One question at a time** (exception: after Cloudinary choice, immediately collect the three Cloudinary env vars in the same Question 2).
77
- 3. **Do not invent** Cloudinary credentials, Supabase URLs, or brand colours.
78
- 4. **Prefer `SUPABASE_URL`** for Supabase (same as Replit). `DATABASE_URL` may be used as fallback on Vercel, but document one canonical name — prefer **`SUPABASE_URL`**.
79
- 5. **Always prefer the Supabase Session pooler URL** (port **6543**, host like `*.pooler.supabase.com`). Direct `db.*.supabase.co:5432` URLs commonly fail on Vercel/sandbox with **`ENETUNREACH`** (often IPv6).
96
+ 2. **One question / one credential at a time.** For Cloudinary: ask **cloud name**, then **API key alone**, then **API secret alone** never one message that mixes key + secret.
97
+ 3. **Do not invent** Cloudinary credentials, database URLs, Vercel Blob tokens, or brand colours.
98
+ 4. **Postgres env naming:** prefer **`SUPABASE_URL`** when using Supabase; prefer **`DATABASE_URL`** (or `NEON_DATABASE_URL`) when using Neon. `createTomorrowOSStore` accepts `SUPABASE_URL` then `DATABASE_URL`.
99
+ 5. **Always prefer pooled connection strings** (Supabase Session pooler port **6543**; Neon pooled host). Direct `:5432` URLs commonly fail on Vercel/sandbox with **`ENETUNREACH`** (often IPv6).
80
100
  6. **Prefer `npx @tomorrowos/sdk init`** (or `templates/cms-starter`) as the seed. Do not rebuild pairing / WebSocket / playlist APIs.
81
101
  7. **Never commit secrets.** Use Vercel Environment Variables (+ optional non-secret flags in committed `.env.example`).
82
102
  8. **Skip Replit-only files:** do **not** create `.replit`, `.replit-artifact`, or Replit `deploymentTarget`.
83
- 9. **SQLite is not a production store on Vercel.** Configure Supabase for pairing/playlists. Keep `sqlitePath` only as a local-dev fallback in `createTomorrowOSStore`.
84
- 10. **Enable Fluid compute** for WebSockets. Confirm Project settings after scaffold.
85
- 11. **Export the HTTP server** Vercel can capture: `const server = tomorrowos.listen(...); export default server;` (or equivalent). Do not export only the `TomorrowOS` class instance unless the platform documents another capture path.
86
- 12. After Q&A: **configure install run/deploy minimal verify** (Control Panel at `/`). Do not run a long test suite.
103
+ 9. **SQLite is not a production store on Vercel.** Offer it only as option **3** after explicit warning. Keep `sqlitePath` as local-dev fallback in `createTomorrowOSStore`.
104
+ 10. **Enable Fluid compute** for Production WebSockets. Confirm Project settings after scaffold.
105
+ 11. **Production:** export the HTTP server Vercel can capture: `const server = tomorrowos.listen(...); export default server;` (or equivalent). **Do not** make `next start` the Production entrypoint.
106
+ 12. **Preview:** if the environment is v0 / Next-hardwired, install the Next reverse-proxy shell (see **Preview adapter**). Do not leave Preview broken.
107
+ 13. After Q&A: **configure → install → run/deploy → minimal verify** (Control Panel at `/`). Do not run a long test suite.
87
108
 
88
109
  **Question order:** Q1 → Q2 → Q3 → execution checklist.
89
110
 
@@ -91,12 +112,13 @@ Then ask **Question 1**.
91
112
 
92
113
  ## Runtime & Vercel deploy rules (mandatory)
93
114
 
94
- ### Runtime
115
+ ### Production runtime (Publish)
95
116
 
96
117
  - Serve with **`tsx server.ts`** (or compiled Node that still calls `TomorrowOS.listen` with `staticRoot`).
97
118
  - Keep **`@tomorrowos/sdk`** and put **`tsx` in `dependencies`** (not only `devDependencies`) if production runs via `tsx`.
98
119
  - Node **20+**.
99
120
  - **Do not** replace the SDK with a hand-rolled server that drops WebSocket upgrades.
121
+ - **Forbidden as the Production Deploy entrypoint:** `next start`, static export only, or any command that does not start `TomorrowOS.listen(...)`.
100
122
 
101
123
  ### Expected `package.json` scripts
102
124
 
@@ -104,14 +126,21 @@ Then ask **Question 1**.
104
126
  {
105
127
  "scripts": {
106
128
  "dev": "tsx watch server.ts",
107
- "start": "tsx server.ts"
129
+ "dev:preview": "concurrently -k \"npm:dev:tomorrowos\" \"npm:dev:next\"",
130
+ "dev:tomorrowos": "cross-env PORT=3001 TOMORROWOS_INTERNAL_PORT=3001 tsx watch server.ts",
131
+ "dev:next": "next dev -p 3000",
132
+ "start": "tsx server.ts",
133
+ "build": "echo \"Production is Node — no Next production build required\""
108
134
  }
109
135
  }
110
136
  ```
111
137
 
112
- **Forbidden as the Deploy entrypoint:** `next start`, static export only, or any command that does not start `TomorrowOS.listen(...)`.
138
+ Adapt script names / process runners as needed. **Invariant:**
139
+
140
+ - **`npm start` / Production** = TomorrowOS Node only.
141
+ - **Preview / v0** = Next on public port + TomorrowOS on internal port + proxy.
113
142
 
114
- ### Minimal dependencies
143
+ ### Minimal dependencies (Production)
115
144
 
116
145
  ```json
117
146
  {
@@ -137,7 +166,11 @@ Then ask **Question 1**.
137
166
 
138
167
  Use the latest published `@tomorrowos/sdk` when scaffolding (do not invent versions).
139
168
 
140
- ### `server.ts` pattern (Vercel-adapted)
169
+ Add `@vercel/blob` only when the user chooses **Vercel Blob** in Question 2.
170
+
171
+ 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.
172
+
173
+ ### `server.ts` pattern (Vercel-adapted — Production)
141
174
 
142
175
  ```ts
143
176
  import "dotenv/config";
@@ -150,15 +183,23 @@ const __dirname = dirname(fileURLToPath(import.meta.url));
150
183
  const brand = JSON.parse(readFileSync(join(__dirname, "brand.json"), "utf8"));
151
184
 
152
185
  const store = createTomorrowOSStore({
153
- // Prefer SUPABASE_URL. On Vercel use the Session pooler connection string.
154
- databaseUrl: process.env.SUPABASE_URL || process.env.DATABASE_URL,
186
+ // Supabase: SUPABASE_URL (pooler). Neon: DATABASE_URL (pooled).
187
+ databaseUrl:
188
+ process.env.SUPABASE_URL ||
189
+ process.env.NEON_DATABASE_URL ||
190
+ process.env.DATABASE_URL,
155
191
  sqlitePath: join(__dirname, "data", "tomorrowos.db") // local/dev fallback only
156
192
  });
157
193
 
158
194
  const tomorrowos = new TomorrowOS({ brand, store });
159
195
 
196
+ const port =
197
+ Number(process.env.TOMORROWOS_INTERNAL_PORT) ||
198
+ Number(process.env.PORT) ||
199
+ 3000;
200
+
160
201
  const server = tomorrowos.listen({
161
- port: Number(process.env.PORT) || 3000,
202
+ port,
162
203
  host: "0.0.0.0",
163
204
  staticRoot: join(__dirname, "public")
164
205
  });
@@ -167,9 +208,37 @@ const server = tomorrowos.listen({
167
208
  export default server;
168
209
  ```
169
210
 
211
+ - **Production:** `PORT` from Vercel → TomorrowOS listens on that port (no Next).
212
+ - **Preview:** set `TOMORROWOS_INTERNAL_PORT=3001` (or similar) so Next can own `:3000` and proxy inward.
213
+
170
214
  Load env with `dotenv` for local/`vercel dev`. On production Deploy, Vercel injects Environment Variables natively.
171
215
 
172
- ### `vercel.json` (recommended minimum)
216
+ ### Preview adapter (v0 / Next reverse proxy) — required when Preview is Next-hardwired
217
+
218
+ Detect Preview / v0 (Next already present, or Preview blank while TomorrowOS logs “listening”). Then:
219
+
220
+ 1. Keep a **minimal** Next app (App Router or Pages — whichever the sandbox expects).
221
+ 2. TomorrowOS listens on **`127.0.0.1:TOMORROWOS_INTERNAL_PORT`** (default **3001**).
222
+ 3. Add a **catch-all** Next route (or middleware) that proxies HTTP to TomorrowOS:
223
+
224
+ ```ts
225
+ // Example: app/api/tomorrowos-proxy/[...path]/route.ts or a rewrite + route handler
226
+ // Forward method, headers, and body to http://127.0.0.1:3001/<path>
227
+ // Return the upstream status, headers, and body to the browser.
228
+ ```
229
+
230
+ Preferred UX: proxy **`/`** itself (rewrites) so the Preview URL shows the Control Panel at `/`, not only under `/api/...`.
231
+
232
+ 4. Start **both** processes for Preview (`dev:preview` pattern above).
233
+ 5. Document in the final summary: **Preview = Next shell + proxy; Publish = Node only.**
234
+
235
+ **Forbidden:**
236
+
237
+ - Making Next the Production CMS or the only server after Publish.
238
+ - Deleting TomorrowOS `server.ts` because Preview needed Next.
239
+ - Claiming setup complete if Production entrypoint is still `next start`.
240
+
241
+ ### `vercel.json` (recommended minimum — Production)
173
242
 
174
243
  Adapt to the project’s current Vercel Node-server / Fluid conventions. At minimum ensure:
175
244
 
@@ -181,35 +250,53 @@ Adapt to the project’s current Vercel Node-server / Fluid conventions. At mini
181
250
 
182
251
  Also:
183
252
 
184
- 1. Project → **Fluid compute** enabled (required for WebSockets).
185
- 2. Framework Preset: **Other** (or Node) — **not** Next.js unless wrapping is intentional and still exposes TomorrowOS `listen()`.
186
- 3. Install / Start commands must end in running the CMS server (e.g. `npm run start` or Vercel’s detected `server.ts` entry).
253
+ 1. Project → **Fluid compute** enabled (required for WebSockets on Publish).
254
+ 2. Framework Preset for **Production:** **Other** (or Node) — **not** Next.js as the live CMS.
255
+ 3. Install / Start commands for **Production** must end in running the CMS Node server (e.g. `npm run start` `tsx server.ts`).
187
256
  4. Do **not** enable “Output Directory” static-only hosting for this CMS.
257
+ 5. If the project also contains a Next Preview shell, configure Vercel so **Production Deploy ignores Next as the app** (Node `server.ts` / `export default server` remains the capture path). Document any dashboard setting you change.
188
258
 
189
- If Vercel’s UI and this file disagree on bundling, **prefer whatever keeps a single long-lived Node HTTP server with WebSocket upgrade on `/`**. Document the adaptation in the final summary.
259
+ 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.
190
260
 
191
261
  ### WebSocket / multi-instance note
192
262
 
193
263
  - Device sockets are pinned to **one** function/instance. TomorrowOS device maps are **in-memory**.
194
264
  - For large fleets on multi-instance Vercel, prefer **one stable instance / region**, or accept that reconnects may land on another instance until the product adds shared presence (out of scope for this setup).
195
- - Always use **Supabase** so pairings/playlists survive cold starts and deploys.
265
+ - Always use **Postgres** (Supabase or Neon) so pairings/playlists survive cold starts and deploys.
266
+ - **Pair TVs to the Production HTTPS origin**, not the v0 Preview URL.
196
267
 
197
268
  ### What not to create
198
269
 
199
270
  - ❌ `.replit`, `.replit-artifact`
200
- - ❌ Next.js `app/` / `pages/` CMS
271
+ - ❌ Next.js as the **Production** CMS (App Router replacing TomorrowOS)
201
272
  - ❌ Separate “API project” without the Control Panel
202
- - ❌ Committing `SUPABASE_URL` passwords or Cloudinary secrets
273
+ - ❌ Committing database passwords, Cloudinary secrets, or Blob tokens
203
274
 
204
275
  ---
205
276
 
206
- ## Question 1 — Supabase Session pooler (always)
277
+ ## Question 1 — Database (always)
207
278
 
208
- > Always configure Supabase for Vercel. Do **not** ask screen counts.
279
+ > **Do not** ask screen counts. Present all three options; recommend **Supabase** for Vercel fleets.
280
+
281
+ ### Step A — Ask storage choice
282
+
283
+ **Ask exactly:**
284
+
285
+ > Which database should TomorrowOS use on Vercel?
286
+ >
287
+ > **1. Supabase Postgres (recommended)** — durable pairing/playlists; use the **Session pooler** URL (`*.pooler.supabase.com:6543`).
288
+ >
289
+ > **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).
290
+ >
291
+ > **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.
292
+ >
293
+ > Reply with **1**, **2**, or **3** (or “Supabase” / “Neon” / “SQLite”).
294
+
295
+ ### Step B — If **1 / Supabase**
209
296
 
210
297
  **Ask exactly:**
211
298
 
212
- > Paste your Supabase Postgres **Session pooler** connection string. I will store it as the Vercel Environment Variable **`SUPABASE_URL`**.
299
+ > Paste your Supabase Postgres connection string. I will store it as the Vercel Environment Variable **`SUPABASE_URL`**.
213
300
  >
214
301
  > In Supabase: **Project Settings → Database → Connection string → Connection pooling** (Session mode).
215
302
  > Preferred shape:
@@ -230,9 +317,45 @@ If Vercel’s UI and this file disagree on bundling, **prefer whatever keeps a s
230
317
  4. **Do not** commit the real connection string.
231
318
  5. **Configuration over probing:** if local/`vercel dev` still shows `ENETUNREACH` / IPv6 errors, verify the string is the **pooler** (`pooler.supabase.com` + `:6543`). Treat remaining sandbox DNS issues as non-fatal when Production deploy Env Vars are correct — but **do** fix an obvious direct `:5432` URL before finishing.
232
319
 
233
- If the user refuses Supabase, warn that Vercel cannot use durable SQLite for fleets, then only continue with SQLite after explicit confirmation (local/demo only).
320
+ ### Step C If **2 / Neon**
321
+
322
+ **Ask exactly:**
323
+
324
+ > Paste your **Neon pooled** Postgres connection string. I will store it as **`DATABASE_URL`** (or `NEON_DATABASE_URL` if you prefer — wire whichever you set in `server.ts`).
325
+ >
326
+ > In Neon: **Dashboard → Connection details → Pooled connection** (enable pooling).
327
+ > Typical shape:
328
+ > `postgresql://[user]:[password]@[endpoint].pooler.[region].aws.neon.tech/[dbname]?sslmode=require`
329
+ >
330
+ > **Do not** use the direct (non-pooled) connection string for Vercel serverless unless Neon docs explicitly say otherwise for your plan.
331
+ >
332
+ > Confirm SSL (`sslmode=require`) is on (usually **yes**).
333
+
334
+ **You must then:**
335
+
336
+ 1. Set Vercel Env Vars:
337
+ - `DATABASE_URL=<pooled Neon string>` (or `NEON_DATABASE_URL` + read it in `server.ts`)
338
+ - `TOMORROWOS_STORE=postgres`
339
+ - `DATABASE_SSL=true` (unless user explicitly disables SSL)
340
+ 2. Wire `server.ts` — `createTomorrowOSStore` uses `PostgresStore` for `postgres` / `supabase` drivers.
341
+ 3. **Do not** commit the real connection string.
342
+ 4. Same pooler guidance as Supabase: if you see `ENETUNREACH` on `:5432`, switch to Neon’s **pooled** URL.
343
+
344
+ ### Step D — If **3 / SQLite**
345
+
346
+ **Warn exactly before continuing:**
347
+
348
+ > 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.
349
+
350
+ Only proceed after the user explicitly confirms.
234
351
 
235
- **Later in Question 3:** set `cms.hostingTarget` to **`"vercel"`**.
352
+ **You must then:**
353
+
354
+ 1. Set `TOMORROWOS_STORE=sqlite` (or omit `DATABASE_URL` / `SUPABASE_URL` so the factory defaults to SQLite).
355
+ 2. Keep `sqlitePath` in `server.ts` for `data/tomorrowos.db`.
356
+ 3. Warn again in the final summary that they should move to Supabase or Neon for real devices.
357
+
358
+ **Later in Question 3:** set `cms.hostingTarget` to **`"vercel"`** (all Q1 branches).
236
359
 
237
360
  ---
238
361
 
@@ -244,30 +367,91 @@ If the user refuses Supabase, warn that Vercel cannot use durable SQLite for fle
244
367
 
245
368
  > How should playlist media (images/videos) be stored?
246
369
  >
247
- > **Recommended on Vercel: Cloudinary** — durable public HTTPS URLs (`https://res.cloudinary.com/...`). Local `public/uploads` is ephemeral on serverless/filesystem deploys and is a poor fleet default.
370
+ > **1. Cloudinary (recommended)** — durable public HTTPS URLs (`https://res.cloudinary.com/...`). Works out of the box with `@tomorrowos/sdk` auto-detection.
248
371
  >
249
- > Alternatives:
250
- > - **Local disk only** — `public/uploads` (OK for quick local tests; **not** for production Vercel fleets).
372
+ > **2. Vercel Blob** — Vercel-native object storage; durable `https://*.public.blob.vercel-storage.com/...` URLs. Good when you want media on the same Vercel project without a Cloudinary account.
251
373
  >
252
- > Do you want me to set up **Cloudinary**? (yes / no)
374
+ > **3. Local disk only** `public/uploads` (OK for quick local tests; **not** for production Vercel fleets — files are ephemeral).
375
+ >
376
+ > Reply with **1**, **2**, or **3** (or “Cloudinary” / “Vercel Blob” / “local”).
377
+
378
+ ### Step B — If **1 / Cloudinary** (same Question 2 — sequential prompts)
379
+
380
+ > **Critical UX rule:** Collect Cloudinary credentials in **separate messages**. Never ask for API key and API secret in the same message or the same form field group.
381
+
382
+ #### B1 — Cloud name (ask alone)
383
+
384
+ **Ask exactly:**
385
+
386
+ > Paste your Cloudinary **Cloud name** only (`CLOUDINARY_CLOUD_NAME`).
387
+ > Find it at [cloudinary.com/console](https://cloudinary.com/console) → Dashboard.
388
+
389
+ Save as Vercel Env Var `CLOUDINARY_CLOUD_NAME`. Do **not** ask for key or secret yet.
390
+
391
+ #### B2 — API key (ask alone — after B1 is saved)
253
392
 
254
- ### Step B — If YES / Cloudinary (same Question 2)
393
+ **Ask exactly:**
394
+
395
+ > Paste your Cloudinary **API Key** only (`CLOUDINARY_API_KEY`).
396
+ > Do not paste the API Secret in this step.
397
+
398
+ Save as Vercel Env Var `CLOUDINARY_API_KEY`. Do **not** ask for the secret in this message.
399
+
400
+ #### B3 — API secret (ask alone — after B2 is saved)
401
+
402
+ **Ask exactly:**
255
403
 
256
- Immediately collect and store as **Vercel Environment Variables**:
404
+ > Paste your Cloudinary **API Secret** only (`CLOUDINARY_API_SECRET`).
405
+ > This is the last Cloudinary credential.
257
406
 
258
- - `CLOUDINARY_CLOUD_NAME`
259
- - `CLOUDINARY_API_KEY`
260
- - `CLOUDINARY_API_SECRET`
261
- - Optional: `CLOUDINARY_FOLDER` (e.g. `tomorrowos`)
407
+ Save as Vercel Env Var `CLOUDINARY_API_SECRET`.
262
408
 
263
- Do **not** invent placeholders. Do **not** proceed to Question 3 until the three required vars exist. Do **not** run a formal Cloudinary upload test unless the user asks.
409
+ Optional fourth message (only if useful):
410
+
411
+ > Optional: Cloudinary folder name (`CLOUDINARY_FOLDER`, e.g. `tomorrowos`). Press Enter to skip.
412
+
413
+ **You must then:**
414
+
415
+ 1. Confirm all three required Env Vars exist: `CLOUDINARY_CLOUD_NAME`, `CLOUDINARY_API_KEY`, `CLOUDINARY_API_SECRET`.
416
+ 2. **Do not** invent placeholders.
417
+ 3. **Do not** proceed to Question 3 until all three exist.
418
+ 4. **Do not** run a formal Cloudinary upload test unless the user asks.
264
419
 
265
420
  The SDK auto-detects these env vars.
266
421
 
267
- ### If NOlocal uploads
422
+ ### Step CIf **2 / Vercel Blob** (same Question 2)
423
+
424
+ **Ask exactly:**
425
+
426
+ > I will enable **Vercel Blob** for media uploads.
427
+ >
428
+ > 1. In the Vercel project: **Storage → Create → Blob** (or link an existing Blob store to this project).
429
+ > 2. Confirm **`BLOB_READ_WRITE_TOKEN`** is available (Vercel usually injects it when Blob is linked).
430
+ >
431
+ > 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)
432
+
433
+ **You must then:**
434
+
435
+ 1. Set Vercel Env Var: `BLOB_READ_WRITE_TOKEN=<token>` (if not auto-injected).
436
+ 2. `npm install @vercel/blob` (add to `dependencies`).
437
+ 3. Wire uploads so media returns **absolute HTTPS Blob URLs** stored in `uploaded_assets` (players need stable public URLs).
438
+ - The SDK **natively auto-detects Cloudinary** today. For Vercel Blob, add a thin upload bridge in the CMS project (e.g. custom route or middleware that calls `put()` from `@vercel/blob` and persists the returned `url` the same way Cloudinary URLs are stored).
439
+ - Minimum pattern:
440
+
441
+ ```ts
442
+ import { put } from "@vercel/blob";
443
+
444
+ // On upload: const blob = await put(filename, body, { access: "public", token: process.env.BLOB_READ_WRITE_TOKEN });
445
+ // Store blob.url in uploaded_assets — same shape as Cloudinary secure URLs.
446
+ ```
447
+
448
+ 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.
449
+ 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).
450
+
451
+ ### Step D — If **3 / local uploads**
268
452
 
269
453
  1. `mkdir -p public/uploads`
270
- 2. Warn clearly: files may vanish on redeploy; players need stable HTTPS URLs — Cloudinary is strongly preferred for Vercel.
454
+ 2. Warn clearly: files may vanish on redeploy; players need stable HTTPS URLs — Cloudinary or Vercel Blob is strongly preferred for Vercel.
271
455
 
272
456
  ---
273
457
 
@@ -275,10 +459,17 @@ The SDK auto-detects these env vars.
275
459
 
276
460
  > Updates **only** `brand.json`. Does **not** change Vercel project settings, Env Vars, or `server.ts` store wiring beyond what Q1–Q2 already required.
277
461
 
462
+ ### Step A — Ask branding input
463
+
278
464
  **Ask exactly:**
279
465
 
280
- > Let’s brand your TomorrowOS experience. Please provide:
466
+ > Let’s brand your TomorrowOS experience. You can answer in either way:
467
+ >
468
+ > **Option A — Website URL only**
469
+ > Paste **one public website URL** (your company / venue site). I will infer colours, fonts, name, tagline, and logo from that page and write `brand.json` to match its look.
281
470
  >
471
+ > **Option B — Manual fields**
472
+ > Provide:
282
473
  > 1. **Product / venue name**
283
474
  > 2. **Tagline** (optional)
284
475
  > 3. **Primary colour** (hex, e.g. `#FF8A3D`)
@@ -287,12 +478,33 @@ The SDK auto-detects these env vars.
287
478
  > 6. **Secondary / accent colour** (hex, optional)
288
479
  > 7. **Logo** — upload SVG/PNG into the project, or a URL I can fetch into `./assets/`
289
480
 
290
- Then write `brand.json` (validate mentally against `brand.schema.json`). Minimum:
481
+ ### Step B If the user gives **only a website URL** (Option A)
482
+
483
+ **You must:**
484
+
485
+ 1. **Fetch and inspect** the page (HTTP GET the URL; follow one redirect if needed). Do not invent colours.
486
+ 2. **Derive branding** using this priority order:
487
+ - **Name:** `<title>`, `og:site_name`, or prominent header / logo `alt` text (trim to ≤ 60 chars).
488
+ - **Tagline:** `meta[name="description"]`, `og:description`, or first hero subtitle (≤ 120 chars).
489
+ - **Primary colour:** `meta[name="theme-color"]`, CSS `--primary` / `--brand` variables, or dominant accent from linked stylesheets / inline styles (convert to `#RRGGBB`).
490
+ - **Background colour:** `body` / `:root` background (default `#FAFAF9` if light site).
491
+ - **Text colour:** main body text colour (default `#0A0908` if light site).
492
+ - **Secondary colour:** muted border / secondary button colour, or a tint of the primary.
493
+ - **Font:** first `font-family` on `body` (strip quotes; default `Inter` if generic system stack).
494
+ - **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.
495
+ 3. **Contrast check:** if background is dark, set `activationScreen.theme` to `"dark"` and add `logoPathOnDark` when a light logo variant exists.
496
+ 4. **Infer `cms.useCase`** from page content when obvious (restaurant / retail / corporate / etc.); otherwise `"other"`.
497
+ 5. **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.
498
+ 6. 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.
499
+
500
+ ### Step C — Write `brand.json`
501
+
502
+ Validate mentally against `brand.schema.json`. Minimum:
291
503
 
292
504
  ```json
293
505
  {
294
- "name": "<user name>",
295
- "tagline": "<user tagline or Digital signage>",
506
+ "name": "<user name or site-derived name>",
507
+ "tagline": "<tagline or Digital signage>",
296
508
  "targetPlatforms": ["tizen"],
297
509
  "primaryColor": "#FF8A3D",
298
510
  "secondaryColor": "#F5F3EF",
@@ -315,7 +527,7 @@ Then write `brand.json` (validate mentally against `brand.schema.json`). Minimum
315
527
  }
316
528
  ```
317
529
 
318
- 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 deployed HTTPS origin (player maps `https://` → `wss://`).
530
+ 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://`).
319
531
 
320
532
  ---
321
533
 
@@ -331,20 +543,27 @@ npx @tomorrowos/sdk@latest init .
331
543
 
332
544
  Then:
333
545
 
334
- 1. Remove leftover Next.js / other frameworks so TomorrowOS is sole app.
335
- 2. Apply Vercel-adapted `server.ts` (**export `listen()` HTTP server**).
336
- 3. Add/adjust `vercel.json` with **`fluid: true`** (or enable Fluid in the dashboard).
337
- 4. Do **not** add `.replit*`.
546
+ 1. Keep TomorrowOS `server.ts` as the **Production** app (**export `listen()` HTTP server**).
547
+ 2. If the environment is **v0 / Next Preview**, add the **Preview adapter** (Next shell + reverse proxy + internal TomorrowOS port). Do **not** make Next the Production CMS.
548
+ 3. Add/adjust `vercel.json` with **`fluid: true`** (or enable Fluid in the dashboard) for Publish.
549
+ 4. Ensure Production start command is **`npm run start` → `tsx server.ts`** (or equivalent Node), **not** `next start`.
550
+ 5. Do **not** add `.replit*`.
551
+ 6. If Q2 = Vercel Blob: install `@vercel/blob` and wire the upload bridge.
338
552
 
339
553
  ### B. Environment Variables (Vercel dashboard + local)
340
554
 
341
555
  | Name | Required | Notes |
342
556
  |------|----------|--------|
343
- | `SUPABASE_URL` | Yes (production) | **Session pooler** `:6543` |
344
- | `TOMORROWOS_STORE` | Yes | `supabase` |
557
+ | `SUPABASE_URL` | If Q1 = Supabase | **Session pooler** `:6543` |
558
+ | `DATABASE_URL` | If Q1 = Neon | **Pooled** Neon string |
559
+ | `TOMORROWOS_STORE` | Yes | `supabase`, `postgres`, or `sqlite` |
345
560
  | `DATABASE_SSL` | Yes (typical) | `true` |
346
- | `CLOUDINARY_*` | If Q2 = Cloudinary | Three keys |
347
- | `PORT` | Optional | Vercel injects as needed |
561
+ | `CLOUDINARY_CLOUD_NAME` | If Q2 = Cloudinary | Collected alone (B1) |
562
+ | `CLOUDINARY_API_KEY` | If Q2 = Cloudinary | Collected alone after cloud name (B2) |
563
+ | `CLOUDINARY_API_SECRET` | If Q2 = Cloudinary | Collected alone after API key (B3) |
564
+ | `BLOB_READ_WRITE_TOKEN` | If Q2 = Vercel Blob | From Vercel Blob store |
565
+ | `TOMORROWOS_INTERNAL_PORT` | Preview only | e.g. `3001` when Next owns `:3000` |
566
+ | `PORT` | Optional | Vercel injects for Production |
348
567
 
349
568
  Mirror non-secrets in `.env` for local `npm run dev` if helpful; never commit secrets.
350
569
 
@@ -353,34 +572,46 @@ Mirror non-secrets in `.env` for local `npm run dev` if helpful; never commit se
353
572
  ```bash
354
573
  npm install
355
574
  npm run start
356
- # or: vercel dev
575
+ # Preview / v0: npm run dev:preview (Next + TomorrowOS internal)
357
576
  ```
358
577
 
359
578
  Confirm logs: `[TomorrowOS] listening on http://0.0.0.0:…`
360
- Confirm browser: Control Panel at `/`.
579
+ Confirm browser:
361
580
 
362
- ### D. Deploy
581
+ - **Preview:** Control Panel visible through the Next proxy at `/`
582
+ - **Production start:** Control Panel served directly by TomorrowOS at `/`
583
+
584
+ ### D. Deploy (Publish — Node only)
363
585
 
364
586
  ```bash
365
587
  vercel --prod
366
588
  # or git push → Vercel Git integration
367
589
  ```
368
590
 
591
+ Before finishing, verify Production settings:
592
+
593
+ 1. Start / entry = **Node `TomorrowOS.listen`** (`npm run start` / exported `server`)
594
+ 2. **Not** `next start` as Production
595
+ 3. Fluid compute on
596
+ 4. Env Vars present for Production environment
597
+
369
598
  Tell the user:
370
599
 
371
- - **Control Panel:** `https://YOUR-PROJECT.vercel.app`
600
+ - **Control Panel (Publish):** `https://YOUR-PROJECT.vercel.app`
372
601
  - **TV CMS endpoint:** same HTTPS origin (players use `wss://`)
602
+ - **Preview:** Next shell is for v0 only — do not pair devices to Preview
373
603
 
374
604
  ### E. Minimal verification only
375
605
 
376
606
  **Do:**
377
607
 
378
- 1. Control Panel HTML at `/`
379
- 2. Server status: Media OK when Cloudinary configured
380
- 3. Database: if ERROR shows **`ENETUNREACH` IPv6 … `:5432`**, switch `SUPABASE_URL` to **pooler `:6543`** and redeploy — call this out explicitly to the user
381
- 4. Fluid compute on
608
+ 1. Preview: Control Panel HTML at `/` (via Next proxy if applicable)
609
+ 2. Publish: Control Panel HTML at `/` from **Node** TomorrowOS
610
+ 3. Server status: Media OK when Cloudinary or Blob configured
611
+ 4. 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
612
+ 5. Fluid compute on for Production
382
613
 
383
- **Do not block** on long WebSocket/device pairing tests unless the user asks.
614
+ **Do not block** on long WebSocket/device pairing tests unless the user asks. Prefer pairing against **Publish**, not Preview.
384
615
 
385
616
  ---
386
617
 
@@ -388,13 +619,18 @@ Tell the user:
388
619
 
389
620
  | Symptom | Likely cause | Fix |
390
621
  |---------|--------------|-----|
391
- | `ENETUNREACH` / IPv6 / `:5432` | Direct Supabase DB URL | Use **Session pooler** `*.pooler.supabase.com:6543` |
392
- | Control Panel OK, devices never stay paired after restart | SQLite / ephemeral disk | Must use Supabase |
393
- | Uploads break / broken thumbs in prod | Local `public/uploads` | Cloudinary |
394
- | WebSocket fails / devices drop immediately | Fluid off / static-only / Next wrapper | Enable Fluid; sole Node `listen()` server |
395
- | Agent created Next.js + TomorrowOS | Wrong scaffold | Remove Next; keep cms-starter |
622
+ | Preview blank but logs show TomorrowOS listening | v0 Next intercepts public port | Add Next reverse proxy → internal TomorrowOS port |
623
+ | Preview works, Publish broken / Next-only | Production still on Next | Switch Production entry to `tsx server.ts` / `export default server` |
624
+ | `ENETUNREACH` / IPv6 / `:5432` | Direct Postgres URL | Use **pooled** URL (Supabase `:6543` or Neon pooler) |
625
+ | Control Panel OK, devices never stay paired after restart | SQLite / ephemeral disk | Use Supabase or Neon |
626
+ | Uploads break / broken thumbs in prod | Local `public/uploads` | Cloudinary or Vercel Blob |
627
+ | Blob uploads 401 / missing token | Blob not linked | Vercel Storage → Blob + `BLOB_READ_WRITE_TOKEN` |
628
+ | WebSocket fails on Publish | Fluid off / static-only / Next as Production | Enable Fluid; sole Node `listen()` for Publish |
629
+ | Devices fail only on Preview | Next proxy cannot upgrade `wss` | Expected — pair on Publish URL |
630
+ | Agent mixed Cloudinary key+secret in one prompt | Violated Step B | Re-ask key alone, then secret alone |
396
631
  | Secrets in git | Mistake | Rotate keys; move to Vercel Env Vars |
397
632
  | Replit files present | Copied wrong protocol | Delete `.replit*`; use this file |
633
+ | Brand colours wrong from URL | SPA / blocked fetch | Manual Option B for colours + logo |
398
634
 
399
635
  ---
400
636
 
@@ -405,10 +641,11 @@ Tell the user:
405
641
  | Secrets | Replit Secrets | Vercel Environment Variables |
406
642
  | Deploy config | `.replit`, artifact `kind=web` | `vercel.json` + Fluid; **no** `.replit` |
407
643
  | `hostingTarget` | `"here"` | `"vercel"` |
408
- | Supabase URL | Prefer pooler; Preview DNS may fail | **Require pooler**; direct `:5432` often `ENETUNREACH` |
409
- | SQLite | Demo OK on persistent Repl disk | **Not** production on Vercel |
410
- | Object Storage option | Replit Object Storage | N/A prefer Cloudinary |
411
- | WebSocket | Long-lived Node (Autoscale) | Fluid compute + Node server export |
644
+ | Database | Supabase (primary) | Supabase **or Neon** (pooled); SQLite demo only |
645
+ | Media | Cloudinary or Replit Object Storage | Cloudinary **or Vercel Blob** |
646
+ | Preview | Replit Preview / Autoscale Node | **Next shell + proxy** when v0 hardwires Next |
647
+ | Publish | Long-lived Node | **Node `TomorrowOS.listen` only** (not Next) |
648
+ | Brand from URL | Not in Replit protocol | **Option A** in Question 3 — infer from website |
412
649
 
413
650
  ---
414
651
 
@@ -416,12 +653,19 @@ Tell the user:
416
653
 
417
654
  After setup, report:
418
655
 
419
- 1. **Live URL** (Production)
420
- 2. **Env Vars set** (names only never values)
421
- 3. **Store:** Supabase pooler / SSL flags
422
- 4. **Media:** Cloudinary or local (with warning)
423
- 5. **Brand:** name + `hostingTarget: vercel`
424
- 6. **Adaptations from Replit protocol:** list (e.g. skipped `.replit`, exported HTTP server, Fluid, pooler URL)
425
- 7. **Open action:** if Database still ERROR, paste the pooler instruction again
656
+ 1. **Live URL** (Production Publish)
657
+ 2. **Preview note:** Next proxy shell used / not needed
658
+ 3. **Env Vars set** (names only — never values)
659
+ 4. **Store:** Supabase pooler / Neon pooled / SQLite (with warning)
660
+ 5. **Media:** Cloudinary, Vercel Blob, or local (with warning)
661
+ 6. **Brand:** name + `hostingTarget: vercel` (+ note if derived from website URL)
662
+ 7. **Runtime split:** Preview = ; Publish = Node TomorrowOS (confirm **not** Next)
663
+ 8. **Open action:** if Database still ERROR, paste the pooler instruction again
664
+
665
+ Protocol complete when:
666
+
667
+ - **Preview** shows the Control Panel at `/` (via Next proxy if required), **and**
668
+ - **Publish** is configured for pure Node `TomorrowOS.listen` with Fluid, **and**
669
+ - Q1–Q3 configuration is saved
426
670
 
427
- Protocol complete when the Control Panel renders at `/` on the deployed (or local) server and Q1–Q3 configuration is saved — even if a sandbox DB probe still flakes **after** the pooler URL is correctly set for Production.
671
+ — even if a sandbox DB probe still flakes **after** the pooled URL is correctly set for Production.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tomorrowos/sdk",
3
- "version": "0.9.21",
3
+ "version": "0.9.23",
4
4
  "description": "TomorrowOS CMS server SDK - WebSocket transport, pairing, device commands, optional static CMS UI. Includes CLI (tomorrowos init / build) and starter templates.",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "my-cms",
3
- "version": "0.9.21",
3
+ "version": "0.9.23",
4
4
  "description": "CMS server on @tomorrowos/sdk. Add your UI (React, static files, etc.) alongside this server.",
5
5
  "private": true,
6
6
  "type": "module",
@@ -13,7 +13,7 @@
13
13
  "build-player": "tomorrowos build --platform tizen"
14
14
  },
15
15
  "dependencies": {
16
- "@tomorrowos/sdk": "^0.9.21",
16
+ "@tomorrowos/sdk": "^0.9.23",
17
17
  "dotenv": "^17.2.3",
18
18
  "tsx": "^4.19.0"
19
19
  },