@surph_ai/cli 0.0.35 → 0.0.37

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
@@ -1,11 +1,11 @@
1
1
  #!/usr/bin/env node
2
- // Deploy a board to Surph (`npm run deploy` → `surph-board-deploy`), run from
3
- // the board's folder. Built up step by step; so far:
2
+ // Deploy a board to Surph (`npm run deploy` → `surph board-deploy`), run from
3
+ // the board's folder. The folder mirrors the live board on S3
4
+ // (boards/<slug>/): board.json at the top, the pages and assets in site/.
4
5
  //
5
- // 1. Zip the board's site: every file in the folder except the project
6
- // files (package.json, package-lock.json, README.md, AGENTS.md, CLAUDE.md, board.json,
7
- // node_modules), dot-files (.env, .git, .DS_Store) and the zip itself.
8
- // Written to site.zip in the board's folder.
6
+ // 1. Zip the board's site: every file in site/ except dot-files (.env,
7
+ // .DS_Store) and node_modules. Written to site.zip in the board's folder
8
+ // (not inside site/).
9
9
  // 2. Ask Surph for an upload address for it: boards/<slug>/site/site.zip on
10
10
  // S3, the slug from board.json.
11
11
  // 3. Upload site.zip there, then delete the local site.zip (it's kept when
@@ -26,18 +26,20 @@ const ZIP_NAME = 'site.zip';
26
26
  const RPC_URL = 'https://rpc.surph.ai';
27
27
  // Unzips an uploaded archive in place on S3 (the `sync` action).
28
28
  const SYNC_URL = 'https://e3lunom2hka3u6iiotrlzrcez40uwdnk.lambda-url.us-east-1.on.aws/';
29
- const SLUG_RE = /^[a-z0-9][a-z0-9-]*$/;
29
+ // Also the board's host name (<slug>.surph-boards.com), so at most 63 characters.
30
+ const SLUG_RE = /^[a-z0-9][a-z0-9-]{0,62}$/;
31
+ const PLACEHOLDER_SLUG = 'my-board';
30
32
 
31
- // Left out at the board's top level: the project's own files.
32
- const SKIP_AT_ROOT = new Set(['package.json', 'package-lock.json', 'README.md', 'AGENTS.md', 'CLAUDE.md', 'board.json', ZIP_NAME]);
33
+ // The board's site: site/ in its folder (boards/<slug>/site/ when live).
34
+ const SITE = path.join(ROOT, 'site');
33
35
 
34
- // The files to deploy, as paths relative to the board's folder ('/'-separated).
35
- const siteFiles = (dir = ROOT, rel = '') => {
36
+ // The files to deploy, as paths relative to site/ ('/'-separated) — where
37
+ // they land under boards/<slug>/site/.
38
+ const siteFiles = (dir = SITE, rel = '') => {
36
39
  const out = [];
37
40
  for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
38
41
  const name = entry.name;
39
42
  if (name.startsWith('.') || name === 'node_modules') continue;
40
- if (!rel && SKIP_AT_ROOT.has(name)) continue;
41
43
  const relPath = rel ? `${rel}/${name}` : name;
42
44
  const full = path.join(dir, name);
43
45
  if (entry.isDirectory()) out.push(...siteFiles(full, relPath));
@@ -74,7 +76,7 @@ const zipFiles = (files) => {
74
76
  const central = [];
75
77
  let offset = 0;
76
78
  for (const rel of files) {
77
- const full = path.join(ROOT, ...rel.split('/'));
79
+ const full = path.join(SITE, ...rel.split('/'));
78
80
  const data = fs.readFileSync(full);
79
81
  const deflated = zlib.deflateRawSync(data, { level: 9 });
80
82
  const stored = deflated.length >= data.length;
@@ -139,7 +141,10 @@ const boardSlug = () => {
139
141
  fail(`board.json isn't valid JSON: ${error.message}`);
140
142
  }
141
143
  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").');
144
+ if (!SLUG_RE.test(slug)) fail('board.json needs a "slug": lowercase letters, digits and dashes, at most 63 characters (e.g. "breakledger").');
145
+ // The template's placeholder: publishing under it would overwrite whatever
146
+ // else was published as "my-board".
147
+ 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
148
  return slug;
144
149
  };
145
150
 
@@ -203,12 +208,15 @@ const main = async () => {
203
208
  fail(`No board.json in ${ROOT}. Run this from the board's folder.`);
204
209
  }
205
210
  const slug = boardSlug();
211
+ if (!fs.existsSync(SITE) || !fs.statSync(SITE).isDirectory()) {
212
+ fail(`No site/ folder in ${ROOT}. The board's pages and assets go in site/ (site/index.html, site/assets/…).`);
213
+ }
206
214
  const files = siteFiles();
207
- if (!files.length) fail('Nothing to deploy: the board has no site files.');
215
+ if (!files.length) fail('Nothing to deploy: site/ has no files.');
208
216
 
209
217
  const zip = zipFiles(files);
210
218
  fs.writeFileSync(path.join(ROOT, ZIP_NAME), zip);
211
- console.log(`Zipped ${files.length} file${files.length === 1 ? '' : 's'} into ${ZIP_NAME} (${(zip.length / 1024).toFixed(1)} KB):`);
219
+ console.log(`Zipped ${files.length} file${files.length === 1 ? '' : 's'} from site/ into ${ZIP_NAME} (${(zip.length / 1024).toFixed(1)} KB):`);
212
220
  for (const file of files) console.log(` ${file}`);
213
221
 
214
222
  console.log(`\nDeploying the board "${slug}"…`);
package/board/server.js CHANGED
@@ -1,12 +1,20 @@
1
1
  #!/usr/bin/env node
2
- // The board's local dev server (`npm run dev` → `surph-board-dev`): serves
3
- // the board folder it's run from (the current directory) at
2
+ // The board's local dev server (`npm run dev` → `surph board-server`):
3
+ // serves the board folder it's run from (the current directory) at
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
- // GET /<file> This folder's files, never cached, with CORS so
8
- // Surph (another address) can read board.json.
9
- // GET /__surph/assets The image files in assets/ (the logo picker).
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.
11
+ //
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).
10
18
  // PUT /board.json Surph's Local board panel saving the board. Only
11
19
  // from Surph's own addresses, only this one file,
12
20
  // only a JSON object with a config.pages list.
@@ -70,14 +78,19 @@ const cors = (req) => {
70
78
  return headers;
71
79
  };
72
80
 
73
- // A path inside this folder, or null (no way out of it, no dot-files).
81
+ // The board's site: site/ in this folder, served as the base (it becomes
82
+ // boards/<slug>/site/ when live).
83
+ const SITE = path.join(ROOT, 'site');
84
+
85
+ // A path → its file inside site/, or null (no way out of it, no dot-files,
86
+ // no node_modules).
74
87
  const inside = (urlPath) => {
75
88
  let rel;
76
89
  try { rel = decodeURIComponent(urlPath.split('?')[0]); } catch { return null; }
77
90
  if (rel.endsWith('/')) rel += 'index.html';
78
- const full = path.normalize(path.join(ROOT, rel));
79
- if (full !== ROOT && !full.startsWith(ROOT + path.sep)) return null;
80
- if (path.relative(ROOT, full).split(path.sep).some((part) => part.startsWith('.') || part === 'node_modules')) return null;
91
+ const full = path.normalize(path.join(SITE, rel));
92
+ if (!full.startsWith(SITE + path.sep)) return null;
93
+ if (path.relative(SITE, full).split(path.sep).some((part) => part.startsWith('.') || part === 'node_modules')) return null;
81
94
  return full;
82
95
  };
83
96
 
@@ -138,8 +151,18 @@ const handle = async (req, res) => {
138
151
  send(res, 405, 'Method not allowed', headers);
139
152
  return;
140
153
  }
141
- if (urlPath.split('?')[0] === '/__surph/assets') {
142
- const dir = path.join(ROOT, 'assets');
154
+ const pathname = urlPath.split('?')[0];
155
+ if (pathname === '/board.json') {
156
+ const file = path.join(ROOT, 'board.json');
157
+ if (!fs.existsSync(file)) {
158
+ send(res, 404, 'No board.json in this folder.', headers);
159
+ return;
160
+ }
161
+ send(res, 200, req.method === 'HEAD' ? '' : fs.readFileSync(file), { ...headers, 'Content-Type': TYPES['.json'] });
162
+ return;
163
+ }
164
+ if (pathname === '/__surph/assets') {
165
+ const dir = path.join(SITE, 'assets');
143
166
  const files = fs.existsSync(dir) ? fs.readdirSync(dir).filter((name) => IMAGE.test(name)).sort() : [];
144
167
  send(res, 200, JSON.stringify({ images: files }), { ...headers, 'Content-Type': 'application/json' });
145
168
  return;
@@ -174,7 +197,7 @@ const start = () => {
174
197
  process.exit(1);
175
198
  })
176
199
  .listen(PORT, '127.0.0.1', () => {
177
- console.log(`Board dev server: http://localhost:${PORT} (serving ${ROOT})`);
200
+ console.log(`Board dev server: http://localhost:${PORT} (serving ${ROOT}: board.json and site/)`);
178
201
  console.log('In Surph: Settings → Developer → Run Surph as a local board.');
179
202
  });
180
203
  http.createServer(handle).listen(PORT, '::1').on('error', () => { /* no IPv6 loopback */ });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@surph_ai/cli",
3
- "version": "0.0.35",
3
+ "version": "0.0.37",
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`).
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` (nothing to install; it uses the Surph CLI through `npx`).
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`).
22
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.
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
- 4. **Change pages:** edit the HTML in this folder 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.
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.
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`.
31
- - Everything in this folder except `package.json`, `package-lock.json`, `README.md`, `AGENTS.md`, `CLAUDE.md`, `board.json`, `node_modules` and dot-files gets published by `npm run deploy`. Keep drafts, exports and secrets out of it (or in dot-files).
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.
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".
@@ -16,18 +16,38 @@ This project is a **Surph board**: a set of web pages that Surph shows in its **
16
16
  ## Project layout
17
17
 
18
18
  ```
19
- index.html The board's home page (edit it; it starts as an intro to this setup)
20
- <page>.html Each further page sits next to it (items.html, events.html, …)
21
- assets/
22
- css/site.css Styles shared by every page in the board
23
- js/ Scripts shared by the board's pages (optional)
19
+ site/ The board's site: everything here is published, nothing else is
20
+ index.html The board's home page (edit it; it starts as an intro to this setup)
21
+ <page>.html Each further page sits next to it (items.html, events.html, …)
22
+ assets/
23
+ css/site.css Styles shared by every page in the board
24
+ js/ Scripts shared by the board's pages (optional)
24
25
  board.json The board itself: name, contexts, instructions, and its pages (see "Registering pages")
25
- package.json `npm run dev` serves this folder locally; `npm run deploy` publishes the board (see "Deploying")
26
+ package.json `npm run dev` serves the board locally; `npm run deploy` publishes it (see "Deploying")
26
27
  README.md This file
28
+ AGENTS.md, CLAUDE.md Notes for coding agents
27
29
  ```
28
30
 
29
- - **Pages sit at the top of the folder.** A page's registry entry names its file without a folder, so a page can't live in a subfolder.
30
- - **Everything else a page loads, apart from the SDK, goes under `assets/`**, linked with relative paths (`assets/css/site.css`, `./items.html`).
31
+ The folder mirrors the live board in Surph's storage: `board.json` at the top, the pages and assets in `site/`.
32
+
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/`** (`assets/css/site.css`, `assets/js/…`).
35
+ - **Only `site/` is published.** Keep notes, drafts and anything else outside it.
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).
31
51
 
32
52
  ### Shared styles: `assets/css/site.css`
33
53
 
@@ -53,11 +73,11 @@ Surph only shows pages that are registered with the board. The board itself is d
53
73
  "slug": "my-board",
54
74
  "name": "My Board",
55
75
  "description": "What the board is about.",
56
- "logo": "logo.png",
57
76
  "contexts": ["<context slug>"],
58
77
  "defaultContext": "<context slug>",
59
78
  "tools": [],
60
79
  "config": {
80
+ "icon": "assets/img/icon.png",
61
81
  "instructions": "How the board's chat should behave: its tone, what it helps with.",
62
82
  "pages": [
63
83
  { "name": "home", "file": "index.html", "shows": "…", "start": "always", "ops": ["board.get"] }
@@ -68,15 +88,15 @@ Surph only shows pages that are registered with the board. The board itself is d
68
88
 
69
89
  | Field | Meaning |
70
90
  | --- | --- |
71
- | `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". |
72
92
  | `name` | The board's name, shown in Surph's header and returned by `board.get`. |
73
- | `description` | One line about the board. |
74
- | `logo` | An image file in `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. |
75
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>`). |
76
95
  | `defaultContext` | The context the chat starts with; else the first in `contexts`. |
77
96
  | `tools` | Slugs of Surph tools the board's chat can use. Optional. |
78
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. |
79
98
  | `config.pages` | The board's pages, one registry entry each (below). |
99
+ | `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. |
80
100
 
81
101
  A registry entry:
82
102
 
@@ -92,7 +112,7 @@ A registry entry:
92
112
  | Field | Meaning |
93
113
  | --- | --- |
94
114
  | `name` | Short, stable label. The chat's model and saved conversations refer to the page by this name, and the canvas header shows it, so don't rename it casually. Unique within the board. |
95
- | `file` | The HTML file's name only, with no folder (`events.html`). It's found next to `board.json`: on your local server while you develop, and wherever the board's files are hosted when it's live. |
115
+ | `file` | The HTML file's name only, with no folder (`events.html`). It's found in `site/`: on your local server while you develop, and in the board's `site/` folder in Surph's storage when it's live. |
96
116
  | `url` | Instead of `file`, a page's full `https://` address when it lives somewhere else. |
97
117
  | `shows` | What the page displays, as plain facts. The chat's model reads this to judge when the page fits. Describe the content, not instructions: write "Every item in the board's newest 25, …", not "Use this when the user asks about…". |
98
118
  | `start` | Optional. Marks a page the canvas opens on before the chat starts: `"always"`, `"context"` (a chat that has a context) or `"no-context"` (a chat without one). A board needs at least one. A start page may route on load to another page with [`page.open`](#pageopen) and `replace: true`. |
@@ -396,15 +416,15 @@ A field marked "omitted" is missing from the object, not `null`. Every URL is `h
396
416
  - **One `ready()` per page load**, called after `connect`.
397
417
  - **Don't invent operations or fields.** If a page needs data the operations above don't provide, say so in your hand-off; a new operation needs a change in Surph first.
398
418
  - **Keep the registry in step.** A page added, renamed or removed, or with changed `rpc` calls, needs its registry entry in your hand-off.
399
- - **Plain, static files.** No build step is needed or expected. Scripts can live inline or under `assets/js/`; keep the SDK `<script>` tag as given.
419
+ - **Plain, static files.** No build step is needed or expected. Scripts can live inline or under `site/assets/js/`; keep the SDK `<script>` tag as given.
400
420
 
401
421
  ## Developing locally
402
422
 
403
- 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 this folder at `http://localhost:4180` 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.
423
+ 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.
404
424
  2. Keep it running while you work; the terminal shows a line each time Surph saves `board.json`.
405
- 3. Open `http://localhost:4180/` 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.
406
- 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.
407
- 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 `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.
425
+ 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.
426
+ 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.
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.
408
428
  6. Dev mode affects only your browser. Turn it off in the same place to get Surph's own pages back.
409
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.
410
430
 
@@ -412,17 +432,17 @@ A field marked "omitted" is missing from the object, not `null`. Every URL is `h
412
432
 
413
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:
414
434
 
415
- 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.
416
- 2. **Zips the site** into `site.zip`: every file in the folder except `package.json`, `package-lock.json`, `README.md`, `AGENTS.md`, `CLAUDE.md`, `board.json`, `node_modules`, dot-files (`.env`, `.git`, `.DS_Store`) and `site.zip` itself. It lists the files it zipped.
435
+ 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.
436
+ 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.
417
437
  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.
418
- 4. **Unzips it there**, so the pages and `assets/` sit in the board's `site/` folder, where each page's `file` is found.
438
+ 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.
419
439
  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.
420
440
 
421
- 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.
441
+ 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. A changed file can take a few minutes to replace the old copy there; reload to check.
422
442
 
423
443
  **For coding agents:**
424
444
  - **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)).
425
- - **Everything in the folder outside the list above is uploaded.** Keep drafts, exports, archives and secrets out of the board's folder, or in dot-files, which are never deployed.
445
+ - **Everything in `site/` is uploaded.** Keep drafts, exports, archives and secrets out of `site/` (anywhere else in the board's folder is fine; dot-files are never deployed).
426
446
  - **A deploy replaces the live `board.json` with the local one.** Make sure `slug` is the board you mean to update: a deploy under another board's slug overwrites that board.
427
447
 
428
448
  ## Checklist before handing off a change
@@ -432,8 +452,8 @@ Each step prints what it did; a failure prints why and stops. After a deploy, Su
432
452
  - [ ] Data from Surph is written with `textContent`; links and images use only `http(s)` values.
433
453
  - [ ] Operations that act for the person run only from their click (a start page's `page.open` aside).
434
454
  - [ ] The SDK `<script>` tag is the v1 URL above, unchanged.
435
- - [ ] Every page links `assets/css/site.css?v=<n>`; if `site.css` changed, `<n>` is bumped in every page.
455
+ - [ ] Every page (in `site/`) links `assets/css/site.css?v=<n>`; if `site.css` changed, `<n>` is bumped in every page.
436
456
  - [ ] `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.
437
- - [ ] Opened at `http://localhost:4180/`, every page loads without console errors.
457
+ - [ ] Opened at `http://localhost:4180/<page>`, every page loads without console errors.
438
458
  - [ ] Checked in Surph with dev mode on: the canvas opens on the start page and each page paints with real data.
439
459
  - [ ] 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
  {
@@ -68,7 +68,7 @@
68
68
  </head>
69
69
  <body>
70
70
  <h1>your surph board</h1>
71
- <p class="lead">This page is part of a Surph board. Surph shows it in the <strong>canvas</strong>, the panel beside its chat, and answers its requests for data. Edit <code>index.html</code> to make it your board's home page.</p>
71
+ <p class="lead">This page is part of a Surph board. Surph shows it in the <strong>canvas</strong>, the panel beside its chat, and answers its requests for data. Edit <code>site/index.html</code> to make it your board's home page.</p>
72
72
 
73
73
  <div class="status" id="status">
74
74
  <span class="dot" aria-hidden="true"></span>
@@ -90,9 +90,9 @@
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 this folder at <code>http://localhost:4180</code> with caching off.</li>
94
- <li><strong>Add pages</strong> 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>assets/css/site.css</code>.</li>
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 this folder's pages, with real data.</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
+ <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
+ <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
98
  <script src="https://surph.s3.us-east-1.amazonaws.com/admin/desktop/js/wave-sdk/v1/surph-wave.js"></script>
package/scaffold/index.js CHANGED
@@ -272,15 +272,21 @@ 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/index.html`), 'index.html')
276
- shell.cp('-R', path.join(__dirname, `board/board.json`), 'board.json')
277
-
278
- shell.mkdir('-p', 'assets')
279
- shell.mkdir('-p', 'assets/css')
280
- shell.mkdir('-p', 'assets/js')
281
- shell.cp('-R', path.join(__dirname, `board/assets/css/site.css`), 'assets/css/site.css')
282
-
283
- // shell.cp('-R', path.join(__dirname, `board/assets/js/wave-sdk/v1/surph-wave.js`), 'assets/js/surph-wave.js')
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')
284
+
285
+ // The board's pages and assets go in site/ (boards/<slug>/site/ when live).
286
+ shell.mkdir('-p', 'site/assets/css')
287
+ shell.mkdir('-p', 'site/assets/js')
288
+ shell.cp('-R', path.join(__dirname, `board/site/index.html`), 'site/index.html')
289
+ shell.cp('-R', path.join(__dirname, `board/site/assets/css/site.css`), 'site/assets/css/site.css')
284
290
 
285
291
  await progress.done()
286
292