@surph_ai/cli 0.0.36 → 0.0.38

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/board/deploy.js CHANGED
@@ -13,6 +13,9 @@
13
13
  // 4. Unzip it on S3 into boards/<slug>/site/ (the board's pages and assets).
14
14
  // 5. Upload board.json to boards/<slug>/board.json — last, so the live
15
15
  // board's definition only changes once its site is up.
16
+ // 6. Clear the board's files from the CDN cache (CloudFront serves
17
+ // https://<slug>.surph-boards.com from boards/<slug>/site/), so the new
18
+ // version shows right away.
16
19
  //
17
20
  // Node's built-ins only (zlib for compression, fetch for the upload). Needs
18
21
  // Node.js 20 or later.
@@ -26,7 +29,11 @@ const ZIP_NAME = 'site.zip';
26
29
  const RPC_URL = 'https://rpc.surph.ai';
27
30
  // Unzips an uploaded archive in place on S3 (the `sync` action).
28
31
  const SYNC_URL = 'https://e3lunom2hka3u6iiotrlzrcez40uwdnk.lambda-url.us-east-1.on.aws/';
29
- const SLUG_RE = /^[a-z0-9][a-z0-9-]*$/;
32
+ // Also the board's host name (<slug>.surph-boards.com), so at most 63 characters.
33
+ const SLUG_RE = /^[a-z0-9][a-z0-9-]{0,62}$/;
34
+ // The CloudFront distribution in front of the boards (*.surph-boards.com).
35
+ const CLOUDFRONT_ID = 'E1AB54JQZBIU11';
36
+ const PLACEHOLDER_SLUG = 'my-board';
30
37
 
31
38
  // The board's site: site/ in its folder (boards/<slug>/site/ when live).
32
39
  const SITE = path.join(ROOT, 'site');
@@ -139,7 +146,10 @@ const boardSlug = () => {
139
146
  fail(`board.json isn't valid JSON: ${error.message}`);
140
147
  }
141
148
  const slug = typeof board?.slug === 'string' ? board.slug.trim() : '';
142
- if (!SLUG_RE.test(slug)) fail('board.json needs a "slug": lowercase letters, digits and dashes (e.g. "my-board").');
149
+ if (!SLUG_RE.test(slug)) fail('board.json needs a "slug": lowercase letters, digits and dashes, at most 63 characters (e.g. "breakledger").');
150
+ // The template's placeholder: publishing under it would overwrite whatever
151
+ // else was published as "my-board".
152
+ if (slug === PLACEHOLDER_SLUG) fail(`board.json's "slug" is still the placeholder "${PLACEHOLDER_SLUG}". Set it to this board's own slug (from its name, e.g. "breakledger"), then deploy.`);
143
153
  return slug;
144
154
  };
145
155
 
@@ -179,6 +189,28 @@ const upload = async (uploadUrl, body, filetype, filename) => {
179
189
  }
180
190
  };
181
191
 
192
+ // Clear boards/<slug>/* from CloudFront's cache (the `invalidateCache`
193
+ // action, on the same Lambda as `sync`). The board is already published by
194
+ // now, so a failure is reported, not fatal: the old files expire on their own.
195
+ const invalidateCache = async (slug) => {
196
+ const path = `/boards/${slug}/*`;
197
+ try {
198
+ const res = await fetch(SYNC_URL, {
199
+ method: 'POST',
200
+ headers: { 'Content-Type': 'application/json', Accept: 'application/json', 'Turbo-Client': 'portal' },
201
+ body: JSON.stringify({ action: 'invalidateCache', args: { cloudfront: CLOUDFRONT_ID, path } }),
202
+ });
203
+ const text = await res.text().catch(() => '');
204
+ let json = null;
205
+ try { json = JSON.parse(text); } catch { /* not JSON */ }
206
+ const error = json?.error || json?.response?.error || json?.data?.error;
207
+ if (!res.ok || error) throw new Error(typeof error === 'string' ? error : (error?.message || text.slice(0, 300) || `HTTP ${res.status}`));
208
+ console.log(`Cleared the CDN cache: ${path} (https://${slug}.surph-boards.com shows the new version in a minute or two)`);
209
+ } catch (e) {
210
+ console.warn(`The board is published, but clearing the CDN cache (${path}) failed: ${e.message}. https://${slug}.surph-boards.com may show the old version for a while.`);
211
+ }
212
+ };
213
+
182
214
  // Unzip boards/<slug>/site/site.zip on S3 into boards/<slug>/site/.
183
215
  const unzipOnS3 = async (slug) => {
184
216
  const res = await fetch(SYNC_URL, {
@@ -203,11 +235,19 @@ const main = async () => {
203
235
  fail(`No board.json in ${ROOT}. Run this from the board's folder.`);
204
236
  }
205
237
  const slug = boardSlug();
206
- if (!fs.existsSync(SITE) || !fs.statSync(SITE).isDirectory()) {
207
- fail(`No site/ folder in ${ROOT}. The board's pages and assets go in site/ (site/index.html, site/assets/…).`);
238
+ // A board whose pages live on its own site (pages by `url`, the site in
239
+ // board.json's `origins`) has no site/ to publish: only board.json goes up.
240
+ const hasSite = fs.existsSync(SITE) && fs.statSync(SITE).isDirectory();
241
+ const files = hasSite ? siteFiles() : [];
242
+ if (!files.length) {
243
+ console.log(`No site/ files to publish${hasSite ? ' (site/ is empty)' : ''}: publishing board.json only.`);
244
+ console.log(`\nDeploying the board "${slug}"…`);
245
+ const boardUrl = await uploadUrlFor(slug, 'board.json', 'application/json');
246
+ await upload(boardUrl, fs.readFileSync(path.join(ROOT, 'board.json')), 'application/json', 'board.json');
247
+ console.log(`Uploaded: ${boardUrl.split('?')[0]}`);
248
+ await invalidateCache(slug);
249
+ return;
208
250
  }
209
- const files = siteFiles();
210
- if (!files.length) fail('Nothing to deploy: site/ has no files.');
211
251
 
212
252
  const zip = zipFiles(files);
213
253
  fs.writeFileSync(path.join(ROOT, ZIP_NAME), zip);
@@ -226,6 +266,7 @@ const main = async () => {
226
266
  const boardUrl = await uploadUrlFor(slug, 'board.json', 'application/json');
227
267
  await upload(boardUrl, fs.readFileSync(path.join(ROOT, 'board.json')), 'application/json', 'board.json');
228
268
  console.log(`Uploaded: ${boardUrl.split('?')[0]}`);
269
+ await invalidateCache(slug);
229
270
  };
230
271
 
231
272
  const MIN_NODE = 20;
package/board/server.js CHANGED
@@ -4,15 +4,17 @@
4
4
  // http://localhost:4180 for Surph's dev mode (Settings → Developer → Run
5
5
  // Surph as a local board). Node's built-ins only; needs Node.js 20 or later.
6
6
  //
7
- // The folder mirrors a live board on S3 (boards/<slug>/): board.json at the
8
- // top, the pages and assets in site/. Only those two are served — the
9
- // project's own files (README.md, AGENTS.md, package.json…) aren't.
7
+ // The folder holds board.json at the top and the board's pages and assets in
8
+ // site/. site/ is the server's base: http://localhost:4180/ is
9
+ // site/index.html. Only site/ and board.json are served — the project's own
10
+ // files (README.md, AGENTS.md, package.json…) aren't.
10
11
  //
11
- // GET /board.json The board, with CORS so Surph (another address)
12
- // can read it. Never cached, like everything here.
13
- // GET /site/<file> The board's pages and assets (site/ in this folder).
14
- // GET / Redirects to /site/ (its index.html).
15
- // GET /__surph/assets The image files in site/assets/ (the logo picker).
12
+ // GET /board.json The board (board.json at the folder's top), with
13
+ // CORS so Surph (another address) can read it.
14
+ // Never cached, like everything here.
15
+ // GET /<file> The board's pages and assets, from site/ (/ is
16
+ // site/index.html).
17
+ // GET /__surph/assets The image files in site/assets/ (the icon picker).
16
18
  // PUT /board.json Surph's Local board panel saving the board. Only
17
19
  // from Surph's own addresses, only this one file,
18
20
  // only a JSON object with a config.pages list.
@@ -76,16 +78,15 @@ const cors = (req) => {
76
78
  return headers;
77
79
  };
78
80
 
79
- // The board's site: site/ in this folder (boards/<slug>/site/ when live).
81
+ // The board's site: site/ in this folder, served as the base (it becomes
82
+ // boards/<slug>/site/ when live).
80
83
  const SITE = path.join(ROOT, 'site');
81
84
 
82
- // A /site/… path → its file inside site/, or null (no way out of it, no
83
- // dot-files, no node_modules).
85
+ // A path → its file inside site/, or null (no way out of it, no dot-files,
86
+ // no node_modules).
84
87
  const inside = (urlPath) => {
85
88
  let rel;
86
89
  try { rel = decodeURIComponent(urlPath.split('?')[0]); } catch { return null; }
87
- if (rel !== '/site' && !rel.startsWith('/site/')) return null;
88
- rel = rel.slice('/site'.length) || '/';
89
90
  if (rel.endsWith('/')) rel += 'index.html';
90
91
  const full = path.normalize(path.join(SITE, rel));
91
92
  if (!full.startsWith(SITE + path.sep)) return null;
@@ -151,10 +152,6 @@ const handle = async (req, res) => {
151
152
  return;
152
153
  }
153
154
  const pathname = urlPath.split('?')[0];
154
- if (pathname === '/') {
155
- send(res, 302, '', { ...headers, Location: '/site/' });
156
- return;
157
- }
158
155
  if (pathname === '/board.json') {
159
156
  const file = path.join(ROOT, 'board.json');
160
157
  if (!fs.existsSync(file)) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@surph_ai/cli",
3
- "version": "0.0.36",
3
+ "version": "0.0.38",
4
4
  "description": "",
5
5
  "main": "index.js",
6
6
  "types": "./types.d.ts",
@@ -16,17 +16,33 @@ Previewing and publishing the board need **Node.js 20 or later**. The pages them
16
16
  3. A fresh install may not be visible in the current terminal. If `node -v` still fails afterwards, open a new terminal session (or load the shell profile) and check again.
17
17
  4. If an install isn't possible (declined, or blocked on a work computer), point them to the installer at https://nodejs.org and wait for them.
18
18
 
19
+ ## The board's name and slug
20
+
21
+ `board.json` has two identifiers, and they do different jobs:
22
+
23
+ - **`name`** is the label people see (Surph's header, `board.get`). Change it whenever the person asks.
24
+ - **`slug`** is the board's identity in Surph: it names where the board is published (`boards/<slug>/`) and its live address (`https://<slug>.surph-boards.com`), and it's shared across everyone on Surph, so it must be unique. Lowercase letters, digits and dashes only, at most 63 characters.
25
+
26
+ Keep them in step on a board that hasn't been published yet. The person won't think about the slug, so you do:
27
+
28
+ 1. **Whenever you set or change the board's name on an unpublished board, set `slug` from it in the same edit:** lowercase, spaces and punctuation turned into single dashes (BreakLedger → `breakledger`, "DC Tech Events" → `dc-tech-events`). Tell the person the slug you chose, in a word: it's the board's address (`https://<slug>.surph-boards.com`). Every page names the slug too, on its SDK tag (`<script data-board="<slug>" src="…/surph-board.js">`): change it there in the same edit, or the page stops speaking for the board.
29
+ 2. **A new board's slug starts from its folder name** (`surph board break-ledger` → `break-ledger`). Older boards may still have the placeholder `my-board`: replace it as soon as the board has a name. `npm run deploy` refuses to publish with `my-board`.
30
+ 3. **Check the slug is free before you settle on it:** `curl -s -o /dev/null -w "%{http_code}" https://surph.s3.us-east-1.amazonaws.com/boards/<slug>/board.json`. `200` means a board with that slug already exists. If this folder never published it, it's someone else's: pick another (add a short suffix, e.g. `breakledger-dk`) and say so. Anything else (403 or 404) means it's free.
31
+ 4. **Is this board published?** Treat it as published if the person says so, or if you ran `npm run deploy` for it. If unsure, ask.
32
+ 5. **Once the board is published, don't change `slug`** unless the person asks for a new address. A new slug publishes a second, separate board; the old one stays live under the old slug. Renaming a published board only changes `name`.
33
+
19
34
  ## The working loop
20
35
 
21
- 1. **Preview:** run `npm run dev` from this folder and leave it running. It serves the board at `http://localhost:4180`: `board.json`, and the pages in `site/` under `/site/` (nothing to install; it uses the Surph CLI through `npx`).
22
- 2. **See it in Surph:** ask the person to open Surph, go to **Settings → Developer**, turn on **Run Surph as a local board**, and keep the address `http://localhost:4180`. Surph reloads as this board.
36
+ 1. **Preview:** run `npm run dev` from this folder and leave it running. It serves the board at `http://localhost:4180`: the pages in `site/` as its base (`/` is `site/index.html`), plus `/board.json` (nothing to install; it uses the Surph CLI through `npx`).
37
+ 2. **See it in Surph:** ask the person to open Surph, go to **Settings → Developer**, turn on **Run Surph as a local board**, and keep the address `http://localhost:4180`. Surph reloads as this board. Or, simpler: with `npm run dev` running, the person says "open http://localhost:4180" in Surph's chat, and the board joins the chat (its pages must load the SDK with `board: '<slug>'` and call `ready()`; see the README).
23
38
  3. **Configure the board** (name, contexts, tools, instructions): the person can click the **DEV** label in Surph's canvas header to open the Local board panel, which saves `board.json` for you. You can also edit `board.json` directly; the README describes every field.
24
39
  4. **Change pages:** the pages and their assets live in `site/` (`site/index.html`, `site/assets/…`). Edit them there and keep `board.json`'s `config.pages` in step (every page listed, `shows` written as plain facts, `ops` matching the page's `host.rpc` calls). The person reloads Surph to see the change.
25
- 5. **Publish:** `npm run deploy`. **Only when the person asks you to in this conversation.** It replaces the live board for everyone who uses it. Check the board in dev mode first.
40
+ 5. **Publish:** `npm run deploy`. **Only when the person asks you to in this conversation.** It replaces the live board for everyone who uses it. Check the board in dev mode first, and check `slug` first (see above): not `my-board`, and either free or this board's own. Once deployed, the site is live at `https://<slug>.surph-boards.com/`.
26
41
 
27
42
  ## What to keep in mind
28
43
 
29
44
  - Pages talk to Surph only through the Surph SDK (`host.rpc(…)`) and only with the operations the README lists. Don't invent operations or fields; if a page needs data Surph doesn't offer, say so.
30
45
  - Write data from Surph with `textContent`, never `innerHTML`.
46
+ - `site/` is served at the root of the board's address, locally (`http://localhost:4180/`) and live (`https://<slug>.surph-boards.com/`), so paths like `/assets/…` and app routes work the same in both. It's static: there's no server for `/api/…` calls. The README's "Where the site is served" has the details.
31
47
  - Everything in `site/` (except dot-files) gets published by `npm run deploy`, and nothing outside it except `board.json`. Keep drafts, notes and secrets out of `site/`.
32
48
  - Before handing off, go through the README's "Checklist before handing off a change".
@@ -1,6 +1,6 @@
1
1
  # Surph board
2
2
 
3
- This project is a **Surph board**: a set of web pages that Surph shows in its **canvas**, the panel beside the Surph chat. Each page is a static HTML file. Surph loads it in an iframe and answers the page's requests for data. The page talks to Surph only through the Surph SDK (`surph-wave.js`), which works over `postMessage`.
3
+ This project is a **Surph board**: a set of web pages that Surph shows in its **canvas**, the panel beside the Surph chat. Each page is a static HTML file. Surph loads it in an iframe and answers the page's requests for data. The page talks to Surph only through the Surph board SDK (`surph-board.js`), which works over `postMessage`.
4
4
 
5
5
  **If you're a coding agent, read this whole file before writing a page.** Surph answers only the operations listed in [Operations the host answers](#operations-the-host-answers), with exactly the payloads shown. A page that calls anything else gets an error, and a field not listed here doesn't exist.
6
6
 
@@ -31,9 +31,24 @@ AGENTS.md, CLAUDE.md Notes for coding agents
31
31
  The folder mirrors the live board in Surph's storage: `board.json` at the top, the pages and assets in `site/`.
32
32
 
33
33
  - **Pages sit at the top of `site/`.** A page's registry entry names its file without a folder, so a page can't live in a subfolder of `site/`.
34
- - **Everything else a page loads, apart from the SDK, goes under `site/assets/`**, linked with relative paths from the page (`assets/css/site.css`, `./items.html`).
34
+ - **Everything else a page loads, apart from the SDK, goes under `site/assets/`** (`assets/css/site.css`, `assets/js/…`).
35
35
  - **Only `site/` is published.** Keep notes, drafts and anything else outside it.
36
36
 
37
+ ### Where the site is served
38
+
39
+ `site/` is always served at the root of the board's own address, so `/` is `site/index.html`:
40
+
41
+ | | Address |
42
+ | --- | --- |
43
+ | Developing (`npm run dev`) | `http://localhost:4180/` |
44
+ | Live, once deployed | `https://<slug>.surph-boards.com/` (for example `https://breakledger.surph-boards.com/`) |
45
+
46
+ Surph's canvas loads the pages from that address too, and the live address also works on its own in a browser tab. What follows from it:
47
+
48
+ - **Root paths and relative paths both work** (`/assets/css/site.css` or `assets/css/site.css`), the same locally and live.
49
+ - **App routes work.** A page load at an address with no file extension (`/account`, a reload, a shared link) gets `index.html`, so a single-page app with its own router (React Router, Vite's default build) works as it does locally.
50
+ - **The site is static: there's no server behind it.** A script's call to the site's own address that isn't a file (`fetch('/api/…')`) fails. Data comes from Surph through the SDK; a page that needs another server calls it by its full `https://` address, and that server has to allow it (CORS).
51
+
37
52
  ### Shared styles: `assets/css/site.css`
38
53
 
39
54
  Every page links it in `<head>`, before its own `<style>`:
@@ -58,11 +73,11 @@ Surph only shows pages that are registered with the board. The board itself is d
58
73
  "slug": "my-board",
59
74
  "name": "My Board",
60
75
  "description": "What the board is about.",
61
- "logo": "logo.png",
62
76
  "contexts": ["<context slug>"],
63
77
  "defaultContext": "<context slug>",
64
78
  "tools": [],
65
79
  "config": {
80
+ "icon": "assets/img/icon.png",
66
81
  "instructions": "How the board's chat should behave: its tone, what it helps with.",
67
82
  "pages": [
68
83
  { "name": "home", "file": "index.html", "shows": "…", "start": "always", "ops": ["board.get"] }
@@ -73,15 +88,16 @@ Surph only shows pages that are registered with the board. The board itself is d
73
88
 
74
89
  | Field | Meaning |
75
90
  | --- | --- |
76
- | `slug` | The board's short id. |
91
+ | `slug` | The board's identity in Surph: where it's published (`boards/<slug>/`) and its live address (`https://<slug>.surph-boards.com`). Unique across Surph; lowercase letters, digits and dashes. A new board's slug starts from its folder name; when the board gets its real name, set the slug from that before publishing (BreakLedger → `breakledger`), and don't change it after (a new slug publishes a separate board). The deploy refuses the old placeholder `my-board`. See `AGENTS.md`, "The board's name and slug". |
77
92
  | `name` | The board's name, shown in Surph's header and returned by `board.get`. |
78
- | `description` | One line about the board. |
79
- | `logo` | An image file in `site/assets/` (just its name, e.g. `logo.png`), shown in Surph's header. |
93
+ | `description` | The board's opening line in the chat: what a visitor sees first on the board's home, shown as written. Write it to the visitor ("Track every Whatnot break: ask me about spend, sales and ROI."). Without one, the chat greets from the board's contexts, or says "Welcome to <name>!" when it has none. |
80
94
  | `contexts` | Slugs of the Surph contexts the board draws on. Their items are what `board.get` and the chat's lookups return. They're live Surph contexts; find a context's slug in its address (`/c/<slug>`). |
81
95
  | `defaultContext` | The context the chat starts with; else the first in `contexts`. |
82
96
  | `tools` | Slugs of Surph tools the board's chat can use. Optional. |
83
97
  | `config.instructions` | Instructions from the board's owner for the board's chat: tone, behavior, what it helps with. On a board they take the place of each context's own instructions. |
84
98
  | `config.pages` | The board's pages, one registry entry each (below). |
99
+ | `origins` | Optional. Other sites allowed to speak for this board, as origins (`["https://breakledger.com"]`), for pages hosted outside Surph that load the SDK with `board: '<slug>'`. The board's own `https://<slug>.surph-boards.com` is always allowed. List only sites you control: a listed site can speak as the board. |
100
+ | `config.icon` | Optional. The board's icon: a file in `site/`, by its path there (`apple-touch-icon.png`, `assets/img/icon.png`). Surph shows it beside the board's name: on each of the chat's replies ("@BreakLedger") and in the header. A small square image works best. |
85
101
 
86
102
  A registry entry:
87
103
 
@@ -106,31 +122,31 @@ A registry entry:
106
122
 
107
123
  ## The SDK
108
124
 
109
- Every page loads the SDK from this exact URL. Don't copy, bundle, edit or replace it.
125
+ Every page loads the SDK from this exact URL, with the board's slug on the tag. Don't copy, bundle, edit or replace it.
110
126
 
111
127
  ```html
112
- <script src="https://surph.s3.us-east-1.amazonaws.com/admin/desktop/js/wave-sdk/v1/surph-wave.js"></script>
128
+ <script data-board="my-board" src="https://surph.s3.us-east-1.amazonaws.com/admin/desktop/js/board-sdk/v1/surph-board.js"></script>
113
129
  ```
114
130
 
115
- It defines one global, `SurphWave`:
131
+ `data-board` is this board's slug (`board.json`'s `slug`). With it, the SDK connects to Surph and announces the board as it loads; there's nothing to call. Add `data-page="events"` to name the page (optional). It defines one global, `SurphBoard`, for the page's own scripts:
116
132
 
117
133
  ```js
118
- const host = SurphWave.connect({
119
- pageSlug: 'events', // The page's own name (any short string)
120
- onRefresh: function () { paint(); } // Surph's data changed: ask again and repaint
121
- });
122
-
123
- host.ready(); // Call once, after connect
134
+ const host = SurphBoard.host; // The page's connection to Surph
135
+ SurphBoard.onRefresh(paint); // Surph's data changed: ask again and repaint
124
136
  host.rpc(op, args, { timeout }); // Ask Surph for something; returns a Promise
125
137
  host.hosted; // true inside the Surph canvas, false when opened directly
126
138
  ```
127
139
 
140
+ Setting it up by hand instead (a tag without `data-board`) also works: `const host = SurphBoard.connect({ board: 'my-board', page: 'events', onRefresh: paint }); host.ready();`. Either way there's one connection per page: `SurphBoard.connect()` returns `SurphBoard.host` when it already exists, and `ready()` announces once.
141
+
128
142
  - `rpc(op, args, extra)` sends `args` as an object (use `{}` when there are none). It resolves with the operation's result object.
129
143
  - It rejects with:
130
144
  - `Error('host timeout')`: no reply in time. The default wait is 15 seconds; pass `{ timeout: 800 }` on reads.
131
145
  - `Error('not in a Surph host')`: the page isn't inside the canvas (`hosted` is false).
132
146
  - `Error('<message>')`: Surph answered with an error, for example `Unknown op: …` for an operation it doesn't support.
133
- - Only use `connect`, `ready`, `rpc`, `hosted` and the `onRefresh` option. The script has other options; they belong to other hosts and do nothing here.
147
+ - `data-board` (or `board` in `connect`) says which board the page speaks for. The SDK passes it to Surph when the page loads, which checks the page's site against the board's: the board's own `https://<slug>.surph-boards.com`, plus any listed in `board.json`'s `origins`. A page on any other site is shown as an ordinary web page.
148
+ - When the chat reads the page, the SDK answers with the page's visible text on its own; nothing to add.
149
+ - Pages written before the board SDK load `surph-wave.js` and call `SurphWave.connect({ pageSlug, board, onRefresh })`. They still work; use the board SDK's tag for new pages and when you touch an old one.
134
150
 
135
151
  ## Call pattern
136
152
 
@@ -140,10 +156,9 @@ Every page that reads data uses this loop. Keep its rules:
140
156
  - paint only after a successful reply.
141
157
 
142
158
  ```js
143
- const host = SurphWave.connect({
144
- pageSlug: 'events',
145
- onRefresh: function () { paint(); }
146
- });
159
+ // The page loads <script data-board="my-board" src="…/surph-board.js"></script> first.
160
+ const host = SurphBoard.host;
161
+ SurphBoard.onRefresh(paint);
147
162
 
148
163
  let painting = false;
149
164
  async function paint() {
@@ -161,7 +176,6 @@ async function paint() {
161
176
  // got is the operation's result. Paint it here (see "Rules for pages").
162
177
  }
163
178
 
164
- host.ready();
165
179
  paint();
166
180
  ```
167
181
 
@@ -405,11 +419,12 @@ A field marked "omitted" is missing from the object, not `null`. Every URL is `h
405
419
 
406
420
  ## Developing locally
407
421
 
408
- 1. Run `npm run dev` (nothing to install). It starts the board dev server from the Surph CLI (`npx @surph_ai/cli board-server`), which serves the board at `http://localhost:4180` (`board.json`, and `site/` under `/site/`) with caching off, lets Surph read it from another address, and lets Surph's Local board panel save `board.json`. It only listens on your own machine.
422
+ 1. Run `npm run dev` (nothing to install). It starts the board dev server from the Surph CLI (`npx @surph_ai/cli board-server`), which serves the board at `http://localhost:4180` (the pages in `site/` as its base, plus `/board.json`) with caching off, lets Surph read it from another address, and lets Surph's Local board panel save `board.json`. It only listens on your own machine.
409
423
  2. Keep it running while you work; the terminal shows a line each time Surph saves `board.json`.
410
- 3. Open `http://localhost:4180/site/` to check a page loads without console errors. It shows its empty layout there (`hosted` is false); real data only appears inside the Surph canvas.
411
- 4. **Run Surph as your board with dev mode.** Keep `npm run dev` running. In Surph, open **Settings → Developer**, turn on **Run Surph as a local board**, and keep the address `http://localhost:4180`. Surph reloads as the board `board.json` describes: its name and logo in the header, its contexts and instructions in the chat, and its pages in the canvas (it opens on your start page, the Pages menu lists your pages, and the chat's model sees their `shows` lines). The status under the address says what loaded, or why it couldn't read `board.json`; until it loads, Surph runs as usual. A **DEV** label in the canvas header shows your board is loaded. While it's on, anything the chat saves goes to your own space in Surph, not to a board's shared space.
412
- 5. **Configure the board from Surph.** Click the **DEV** label to open the **Local board** panel: set the board's name, slug, description and logo (an image in `site/assets/`), pick its contexts (yours, or public ones by search) and which one the chat starts with, pick its tools, and write its instructions. The panel also lists the pages and flags a missing `shows` line or an operation the host doesn't answer. **Save and reload** writes `board.json` through the dev server and reloads Surph as the updated board. Pages and any other fields are kept as they are in the file; edit pages in `board.json` itself.
424
+ 3. Open `http://localhost:4180/` (your `site/index.html`) to check a page loads without console errors. It shows its empty layout there (`hosted` is false); real data only appears inside the Surph canvas.
425
+ 4. **Run Surph as your board with dev mode.** Keep `npm run dev` running. In Surph, open **Settings → Developer**, turn on **Run Surph as a local board**, and keep the address `http://localhost:4180`. Surph reloads as the board `board.json` describes: its name and icon in the header, its contexts and instructions in the chat, and its pages in the canvas (it opens on your start page, the Pages menu lists your pages, and the chat's model sees their `shows` lines). The status under the address says what loaded, or why it couldn't read `board.json`; until it loads, Surph runs as usual. A **DEV** label in the canvas header shows your board is loaded. While it's on, anything the chat saves goes to your own space in Surph, not to a board's shared space.
426
+ - **Or chat with the board without dev mode:** with `npm run dev` running, say "open http://localhost:4180" in Surph's chat. When the page loads the SDK with `board: '<slug>'` and calls `ready()`, the board joins the chat from the `board.json` at that address (it's your own machine, so no live board is needed): the canvas header shows it, its description opens its part of the conversation, and its replies are signed with its name. Live, the same happens for pages on `https://<slug>.surph-boards.com` or a site in `origins`.
427
+ 5. **Configure the board from Surph.** Click the **DEV** label to open the **Local board** panel: set the board's name, slug, description and icon (an image in `site/`), pick its contexts (yours, or public ones by search) and which one the chat starts with, pick its tools, and write its instructions. The panel also lists the pages and flags a missing `shows` line or an operation the host doesn't answer. **Save and reload** writes `board.json` through the dev server and reloads Surph as the updated board. Pages and any other fields are kept as they are in the file; edit pages in `board.json` itself.
413
428
  6. Dev mode affects only your browser. Turn it off in the same place to get Surph's own pages back.
414
429
  7. In Surph, keep your browser's DevTools open with **Network → Disable cache** checked, or the canvas may keep showing an older copy of a page after you change it. After editing `board.json` by hand, reload Surph so it reads the file again.
415
430
 
@@ -417,13 +432,16 @@ A field marked "omitted" is missing from the object, not `null`. Every URL is `h
417
432
 
418
433
  `npm run deploy` (`npx @surph_ai/cli board-deploy`, from the Surph CLI) publishes the board to Surph. Run it from the board's folder. It needs Node.js 20 or later and nothing else installed. In order, it:
419
434
 
420
- 1. **Reads the board's `slug`** from `board.json` (lowercase letters, digits and dashes). The slug names the board's place in Surph; without a valid one it stops before doing anything.
435
+ A board whose pages live on its own site (pages listed by `url`, the site in `origins`) can have no `site/` folder, or an empty one: the deploy then publishes `board.json` only.
436
+
437
+ 1. **Reads the board's `slug`** from `board.json` (lowercase letters, digits and dashes). The slug names the board's place in Surph, so it must be this board's own, not the placeholder `my-board` and not another board's (see the `slug` field above); without a valid one it stops before doing anything.
421
438
  2. **Zips the site** into `site.zip`: every file in `site/`, except dot-files (`.env`, `.DS_Store`) and `node_modules`. Nothing outside `site/` is included (`board.json` goes up on its own in step 5). It lists the files it zipped.
422
439
  3. **Uploads `site.zip`** to the board's `site/` folder in Surph's storage, then deletes the local `site.zip`. If the upload fails, the zip is kept to look at.
423
440
  4. **Unzips it there**, so the board's `site/` folder in Surph's storage holds the same files as your `site/`, where each page's `file` is found.
424
441
  5. **Uploads `board.json`**, last, so the live board's definition only changes once its pages are up. If an earlier step fails, `board.json` isn't uploaded and the live board keeps its previous version.
442
+ 6. **Clears the board's cached files** at `https://<slug>.surph-boards.com`, so the new version shows within a minute or two. If this step fails, the board is still published; it says so, and the old files may show for a while.
425
443
 
426
- Each step prints what it did; a failure prints why and stops. After a deploy, Surph picks up the new `board.json` within about a minute.
444
+ Each step prints what it did; a failure prints why and stops. After a deploy, the site is live at `https://<slug>.surph-boards.com/`, and Surph picks up the new `board.json` within about a minute. The deploy clears the cached copies there, so a changed file shows within a minute or two; reload to check.
427
445
 
428
446
  **For coding agents:**
429
447
  - **Deploying publishes the board to everyone who uses it.** Don't run `npm run deploy` unless the person asks you to in this conversation. Check the board with dev mode first (see [Developing locally](#developing-locally)).
@@ -436,9 +454,9 @@ Each step prints what it did; a failure prints why and stops. After a deploy, Su
436
454
  - [ ] Reads use the call pattern: a timeout retries, other errors stop, nothing is cleared before a reply.
437
455
  - [ ] Data from Surph is written with `textContent`; links and images use only `http(s)` values.
438
456
  - [ ] Operations that act for the person run only from their click (a start page's `page.open` aside).
439
- - [ ] The SDK `<script>` tag is the v1 URL above, unchanged.
457
+ - [ ] The SDK `<script>` tag is the v1 URL above, unchanged, and every page's SDK tag has `data-board="<slug>"` matching `board.json`'s `slug`.
440
458
  - [ ] Every page (in `site/`) links `assets/css/site.css?v=<n>`; if `site.css` changed, `<n>` is bumped in every page.
441
459
  - [ ] `board.json`'s `config.pages` lists every page: `name`, `file` (or `url`), `shows` written as facts, `ops` matching the file's `rpc` calls, and one `start` page.
442
- - [ ] Opened at `http://localhost:4180/site/<page>`, every page loads without console errors.
460
+ - [ ] Opened at `http://localhost:4180/<page>`, every page loads without console errors.
443
461
  - [ ] Checked in Surph with dev mode on: the canvas opens on the start page and each page paints with real data.
444
462
  - [ ] Not deployed unless the person asked for it; if deployed, `npm run deploy` finished without errors and its file list holds only the board's site.
@@ -2,11 +2,11 @@
2
2
  "slug": "my-board",
3
3
  "name": "My Board",
4
4
  "description": "",
5
- "logo": "",
6
5
  "contexts": [],
7
6
  "defaultContext": "",
8
7
  "tools": [],
9
8
  "config": {
9
+ "icon": "",
10
10
  "instructions": "",
11
11
  "pages": [
12
12
  {
@@ -81,7 +81,7 @@
81
81
  <h2>How a page works in Surph</h2>
82
82
  <ul class="facts">
83
83
  <li><strong>It's a static HTML file.</strong> No build step or framework. Surph loads it in an iframe; you can host it anywhere on <code>https</code>.</li>
84
- <li><strong>It talks to Surph through the SDK.</strong> The page loads <code>surph-wave.js</code>, calls <code>SurphWave.connect()</code>, then asks the host for data with <code>host.rpc(op, args)</code>: the board's recent items, the chat's context, the items the chat looked up, and more. The page never calls Surph's servers itself.</li>
84
+ <li><strong>It talks to Surph through the SDK.</strong> The page loads <code>surph-board.js</code>, calls <code>SurphBoard.connect()</code>, then asks the host for data with <code>host.rpc(op, args)</code>: the board's recent items, the chat's context, the items the chat looked up, and more. The page never calls Surph's servers itself.</li>
85
85
  <li><strong>The chat decides when to show it.</strong> Each page is registered with a <code>shows</code> line, plain facts about what it displays. The chat's model reads those and opens the page that fits what the person asked. A page can also take values in its address (<code>?ids=…</code>) that the chat fills in.</li>
86
86
  <li><strong>It updates as the chat goes.</strong> When the host's data changes, it calls the page's <code>onRefresh</code>, and the page asks again and repaints.</li>
87
87
  <li><strong>Opened directly, it still loads.</strong> Outside Surph, <code>host.hosted</code> is false and every call rejects, so the page shows its empty layout. That's what you see here when you run it locally.</li>
@@ -90,17 +90,16 @@
90
90
  <h2>Next steps</h2>
91
91
  <ol class="steps">
92
92
  <li><strong>Read <code>README.md</code>.</strong> It lists every operation the host answers, the item shape, and the rules a page follows. Point your coding agent at it.</li>
93
- <li><strong>Run it locally:</strong> <code>npm run dev</code> serves the board at <code>http://localhost:4180</code> (this page is <code>/site/</code>) with caching off.</li>
93
+ <li><strong>Run it locally:</strong> <code>npm run dev</code> serves the board at <code>http://localhost:4180</code> (this page is its home) with caching off.</li>
94
94
  <li><strong>Add pages</strong> in <code>site/</code>, next to this one (<code>items.html</code>, <code>events.html</code>…), and list each in <code>board.json</code> with its <code>name</code>, <code>file</code>, <code>shows</code> and the <code>ops</code> it calls. Shared styles go in <code>site/assets/css/site.css</code>.</li>
95
95
  <li><strong>See them in Surph:</strong> open <strong>Settings → Developer</strong> and turn on dev mode for <code>http://localhost:4180</code>. The canvas then shows the board's pages, with real data.</li>
96
96
  </ol>
97
97
 
98
- <script src="https://surph.s3.us-east-1.amazonaws.com/admin/desktop/js/wave-sdk/v1/surph-wave.js"></script>
98
+ <!-- data-board: this board's slug (board.json's "slug"). The SDK connects and announces the board as it loads. -->
99
+ <script data-board="my-board" src="https://surph.s3.us-east-1.amazonaws.com/admin/desktop/js/board-sdk/v1/surph-board.js"></script>
99
100
  <script>
100
- const host = SurphWave.connect({
101
- pageSlug: 'index',
102
- onRefresh: function () { paint(); }
103
- });
101
+ const host = SurphBoard.host;
102
+ SurphBoard.onRefresh(paint);
104
103
 
105
104
  const statusBox = document.getElementById('status');
106
105
  const statusTitle = document.getElementById('status-title');
@@ -145,7 +144,6 @@
145
144
  : 'board.get answered: this chat isn’t on a board, so it has no board items.');
146
145
  }
147
146
 
148
- host.ready();
149
147
  paint();
150
148
  </script>
151
149
  </body>
package/scaffold/index.js CHANGED
@@ -272,12 +272,22 @@ const scaffoldBoard = async (name) => {
272
272
  shell.cp('-R', path.join(__dirname, `board/README.md`), 'README.md')
273
273
  shell.cp('-R', path.join(__dirname, `board/AGENTS.md`), 'AGENTS.md')
274
274
  shell.cp('-R', path.join(__dirname, `board/CLAUDE.md`), 'CLAUDE.md')
275
- shell.cp('-R', path.join(__dirname, `board/board.json`), 'board.json')
275
+ // board.json gets the board's own slug and name from <name> (the folder
276
+ // it was created in), so a new board never carries the template's
277
+ // placeholder slug. The slug is where the board publishes in Surph.
278
+ const boardJson = JSON.parse(utils.readFile(path.join(__dirname, 'board/board.json')))
279
+ const folderName = path.basename(name)
280
+ const boardSlug = slugify(folderName, 0)
281
+ if (/^[a-z0-9][a-z0-9-]*$/.test(boardSlug)) boardJson.slug = boardSlug
282
+ boardJson.name = folderName
283
+ utils.write('board.json', JSON.stringify(boardJson, null, 2) + '\n')
276
284
 
277
285
  // The board's pages and assets go in site/ (boards/<slug>/site/ when live).
278
286
  shell.mkdir('-p', 'site/assets/css')
279
287
  shell.mkdir('-p', 'site/assets/js')
280
- shell.cp('-R', path.join(__dirname, `board/site/index.html`), 'site/index.html')
288
+ // The starter page speaks for this board: its SDK tag names the slug.
289
+ const indexHtml = utils.readFile(path.join(__dirname, 'board/site/index.html'))
290
+ utils.write('site/index.html', indexHtml.replace('data-board="my-board"', `data-board="${boardJson.slug}"`))
281
291
  shell.cp('-R', path.join(__dirname, `board/site/assets/css/site.css`), 'site/assets/css/site.css')
282
292
 
283
293
  await progress.done()