@cfbender/cesium 0.4.0 → 0.5.1

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.
Files changed (45) hide show
  1. package/CHANGELOG.md +94 -0
  2. package/README.md +2 -5
  3. package/package.json +3 -2
  4. package/src/cli/commands/serve.ts +18 -2
  5. package/src/index.ts +4 -1
  6. package/src/prompt/field-reference.ts +94 -0
  7. package/src/prompt/system-fragment.md +56 -65
  8. package/src/render/blocks/catalog.ts +39 -0
  9. package/src/render/blocks/escape.ts +27 -0
  10. package/src/render/blocks/highlight.ts +188 -0
  11. package/src/render/blocks/index.ts +6 -0
  12. package/src/render/blocks/markdown.ts +217 -0
  13. package/src/render/blocks/render.ts +104 -0
  14. package/src/render/blocks/renderers/callout.ts +38 -0
  15. package/src/render/blocks/renderers/code.ts +46 -0
  16. package/src/render/blocks/renderers/compare-table.ts +56 -0
  17. package/src/render/blocks/renderers/diagram.ts +48 -0
  18. package/src/render/blocks/renderers/divider.ts +31 -0
  19. package/src/render/blocks/renderers/hero.ts +66 -0
  20. package/src/render/blocks/renderers/kv.ts +45 -0
  21. package/src/render/blocks/renderers/list.ts +51 -0
  22. package/src/render/blocks/renderers/pill-row.ts +45 -0
  23. package/src/render/blocks/renderers/prose.ts +29 -0
  24. package/src/render/blocks/renderers/raw-html.ts +32 -0
  25. package/src/render/blocks/renderers/risk-table.ts +76 -0
  26. package/src/render/blocks/renderers/section.ts +97 -0
  27. package/src/render/blocks/renderers/timeline.ts +58 -0
  28. package/src/render/blocks/renderers/tldr.ts +30 -0
  29. package/src/render/blocks/themes/claret-dark.ts +206 -0
  30. package/src/render/blocks/themes/claret-light.ts +227 -0
  31. package/src/render/blocks/types.ts +127 -0
  32. package/src/render/blocks/validate-block.ts +202 -0
  33. package/src/render/critique.ts +410 -10
  34. package/src/render/fallback.ts +18 -0
  35. package/src/render/theme.ts +154 -0
  36. package/src/render/validate.ts +282 -17
  37. package/src/render/wrap.ts +7 -7
  38. package/src/server/lifecycle.ts +190 -3
  39. package/src/storage/assets.ts +66 -0
  40. package/src/storage/index-cache.ts +1 -0
  41. package/src/storage/index-gen.ts +13 -14
  42. package/src/tools/ask.ts +7 -5
  43. package/src/tools/critique.ts +41 -6
  44. package/src/tools/publish.ts +43 -14
  45. package/src/tools/styleguide.ts +118 -9
package/CHANGELOG.md CHANGED
@@ -1,5 +1,99 @@
1
1
  # Changelog
2
2
 
3
+ ## v0.5.1 — 2026-05-12
4
+
5
+ Server-side syntax highlighting for `code` blocks via shiki, custom claret
6
+ themes derived from the canonical `claret.nvim` sources, and a critical fix
7
+ to the lazy-start lifecycle so `cesium stop` (or test friendly-fire) can no
8
+ longer kill the plugin host process.
9
+
10
+ - **fix (critical):** Lazy-started cesium server now runs as a detached
11
+ subprocess. Previously `ensureRunning` (called from publish/ask plugin
12
+ paths) ran `Bun.serve()` in-process and wrote `pid: process.pid` to the PID
13
+ file — meaning that PID was the *plugin host* (e.g. opencode). Any
14
+ invocation of `cesium stop` (CLI, tool, or test) would signal the host
15
+ process and kill it. Now lazy-start spawns `bun run cli serve` as a
16
+ detached child; the PID file points at that child. Foreground `cesium
17
+ serve` still runs in-process (correct for its semantics).
18
+ - **api:** Split `ensureRunning` into `runServerForeground` (in-process, for
19
+ the foreground CLI) and `ensureServerRunning` (detached subprocess, for
20
+ plugins). `ensureRunning` is kept as a backward-compat alias for
21
+ `runServerForeground`.
22
+ - **fix:** `test/cli-entry.test.ts` now sets `CESIUM_STATE_DIR` to a temp
23
+ dir per spawned subprocess, so the `cesium stop` test no longer reads the
24
+ user's real PID file.
25
+ - **feat:** Server-side syntax highlighting via shiki. `code` blocks are
26
+ tokenized at publish time and emit styled token spans. Async cascade
27
+ through `renderBlock`/`renderBlocks`/`renderSection`/`renderCode`.
28
+ - **feat:** Custom `claret-dark` shiki theme — converted directly from
29
+ `claret.nvim`'s `ports/bat/ClaretDark.tmTheme` so colors are faithful to
30
+ the canonical claret palette (keywords claret rose, strings olive,
31
+ comments muted italic, functions gold, etc.).
32
+ - **feat:** Custom `claret-light` shiki theme — derived from claret.nvim's
33
+ light palette using the same scope grammar as the dark theme.
34
+ - **feat:** `resolveHighlightTheme(cesiumThemeName)` — maps cesium presets
35
+ to the right shiki theme. `claret`/`claret-dark` → claret-dark,
36
+ `claret-light` → claret-light, all others → `vitesse-dark`.
37
+ - **feat:** `RenderCtx.highlightTheme` threaded from publish through the
38
+ render context. Default is `claret-dark` (matches the framework default).
39
+ - **feat:** `cesium serve` gains a `--state-dir` flag and respects
40
+ `CESIUM_STATE_DIR` / `CESIUM_PORT` env vars (used by the detached spawn).
41
+ - **dep:** Added `shiki@^4.0.0` as a runtime dependency (~3.8MB on disk;
42
+ languages loaded lazily).
43
+
44
+ ## v0.5.0 — 2026-05-12
45
+
46
+ Block-mode refactor — `cesium_publish` now accepts a structured `blocks` array
47
+ alongside the legacy `html` field. The server templates 15 block types from
48
+ JSON; raw HTML stays available as a per-block escape hatch (`raw_html`,
49
+ `diagram`). Framework CSS moves out of every artifact and into a single served
50
+ `/theme.css`. Styleguide is generated from a catalog at request time.
51
+ Critique is mode-aware. Expected savings on a balanced doc: roughly 2× output
52
+ tokens, more on heavily structured artifacts.
53
+
54
+ - **feat:** `cesium_publish({ blocks: Block[] })` — closed discriminated union
55
+ of 15 block types: `hero`, `tldr`, `section`, `prose`, `list`, `callout`,
56
+ `code`, `timeline`, `compare_table`, `risk_table`, `kv`, `pill_row`,
57
+ `divider`, `diagram`, `raw_html`. Mutually exclusive with `html`.
58
+ - **feat:** Owned markdown subset (~80 lines, no dependency) for `prose`,
59
+ `tldr`, `callout`, list items, and table cells. Supports paragraphs, lists,
60
+ blockquotes, hr, hard breaks, `**bold**`, `*italic*`, `` `code` ``, local
61
+ links, and the safelisted inline tags `<kbd>`, `<span class="pill">`,
62
+ `<span class="tag">`.
63
+ - **feat:** Sections recurse to depth 3; non-section children get auto-wrapped
64
+ in `<div class="card">` for visual consistency.
65
+ - **feat:** `theme.css` served from `<state-dir>/theme.css` with a small
66
+ inline fallback (~8 lines) so standalone-opened `.html` files remain
67
+ readable. Existing artifacts (with full CSS inlined) are never rewritten and
68
+ stay self-contained.
69
+ - **feat:** `cesium_styleguide` returns a markdown reference generated from
70
+ the block catalog at request time — schema, examples, and renderer can no
71
+ longer drift.
72
+ - **feat:** `cesium_critique` is mode-aware. `html` mode adds a soft
73
+ `prefer-blocks` nag; `blocks` mode focuses on quality (raw-html overuse,
74
+ prose walls, missing tldr on long docs, table-shape, redundant raw_html,
75
+ nesting depth). Findings carry path tags like `blocks[2].children[1]`.
76
+ - **feat:** Deep block validation walks the catalog schema per type. Returns
77
+ path-tagged errors with "did you mean" suggestions for common drift
78
+ (`label`→`k`, `value`→`v`, `description`→`text`, `med`→`medium`, etc.).
79
+ - **feat:** System prompt fragment generated from the block catalog at plugin
80
+ load time — drift between schema and prompt is now physically impossible.
81
+ - **feat:** `inputMode: "html" | "blocks"` recorded in artifact metadata and
82
+ surfaced as a small badge on index cards.
83
+ - **feat:** Framework CSS extended with rules for every block-renderer
84
+ pattern: `dl.kv` (2-column grid), `.pill-row`, `.check-list`, `<hr
85
+ data-label>`, `figure.code`, timeline-item internals, `.lede`, plus
86
+ `.diagram svg text { fill: currentColor }` so SVGs inherit theme color.
87
+ - **feat:** `escapeHtml` and `escapeAttr` throw a clear error on non-string
88
+ input instead of crashing inside `.replace()`.
89
+ - **fix:** `ensureThemeCss` respects the configured `themePreset` (regression
90
+ introduced when the framework CSS was first extracted to a served file).
91
+ - **fix:** `wait` test fixtures use relative timestamps so they don't go
92
+ stale.
93
+ - **docs:** `AGENTS.md` updated with the new project layout, two-input-modes
94
+ architecture, catalog-as-source-of-truth, and softened CSS portability
95
+ invariant ("no external network resources; local `/theme.css` is allowed").
96
+
3
97
  ## v0.3.6 — 2026-05-11
4
98
 
5
99
  Adds a periodic-table-themed favicon for the cesium HTTP server.
package/README.md CHANGED
@@ -9,10 +9,7 @@ on disk, instead of dumping markdown into the terminal. The browser becomes the
9
9
  reading surface; the terminal stays the control surface. Each artifact is a single
10
10
  `.html` file: portable, archivable, viewable offline, shareable as a URL over SSH.
11
11
 
12
- <video src="assets/cesium.mp4" autoplay loop muted playsinline width="720">
13
- Demo video — see <a href="assets/cesium.mp4">assets/cesium.mp4</a> if it
14
- doesn't play inline (some markdown viewers strip <code>&lt;video&gt;</code>).
15
- </video>
12
+ https://github.com/user-attachments/assets/03fdf32a-c4d5-4819-84eb-d272178d35cb
16
13
 
17
14
  ## Examples
18
15
 
@@ -488,7 +485,7 @@ Cesium took inspiration from:
488
485
  - [@trq212's tweet](https://x.com/trq212/status/2052809885763747935) on
489
486
  letting agents respond with HTML instead of dumping markdown into the
490
487
  terminal — the seed idea for the whole project.
491
- - **Octto** — for the model of an agent that publishes a live, browser-served
488
+ - [Octto](https://github.com/vtemian/octto) — for the model of an agent that publishes a live, browser-served
492
489
  surface alongside the terminal, rather than replacing it.
493
490
 
494
491
  ## License
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cfbender/cesium",
3
- "version": "0.4.0",
3
+ "version": "0.5.1",
4
4
  "description": "Beautiful self-contained HTML artifacts from your opencode agent.",
5
5
  "license": "MIT",
6
6
  "author": "Cody Bender",
@@ -50,7 +50,8 @@
50
50
  "dependencies": {
51
51
  "@opencode-ai/plugin": "latest",
52
52
  "nanoid": "^5.0.0",
53
- "parse5": "^7.1.0"
53
+ "parse5": "^7.1.0",
54
+ "shiki": "^4.0.2"
54
55
  },
55
56
  "devDependencies": {
56
57
  "@types/bun": "latest",
@@ -2,8 +2,9 @@
2
2
 
3
3
  import { parseArgs } from "node:util";
4
4
  import { loadConfig, type CesiumConfig } from "../../config.ts";
5
- import { ensureRunning, stopRunning } from "../../server/lifecycle.ts";
5
+ import { runServerForeground, stopRunning } from "../../server/lifecycle.ts";
6
6
  import { resolveDisplayHost } from "../../tools/publish.ts";
7
+ import { themeFromPreset, mergeTheme } from "../../render/theme.ts";
7
8
 
8
9
  export interface ServeContext {
9
10
  stdout: { write: (s: string) => void };
@@ -21,6 +22,7 @@ function defaultCtx(): ServeContext {
21
22
  export interface ServeOptions {
22
23
  port?: number;
23
24
  hostname?: string;
25
+ stateDir?: string;
24
26
  /**
25
27
  * Idle timeout in milliseconds. 0 (the default for `cesium serve`) means the
26
28
  * server runs forever until SIGINT/SIGTERM. Override with --idle-timeout to
@@ -53,6 +55,7 @@ export function parseServeArgs(
53
55
  port: string | undefined;
54
56
  hostname: string | undefined;
55
57
  "idle-timeout": string | undefined;
58
+ "state-dir": string | undefined;
56
59
  help: boolean;
57
60
  };
58
61
 
@@ -63,6 +66,7 @@ export function parseServeArgs(
63
66
  port: { type: "string", short: "p" },
64
67
  hostname: { type: "string", short: "H" },
65
68
  "idle-timeout": { type: "string" },
69
+ "state-dir": { type: "string" },
66
70
  help: { type: "boolean", short: "h", default: false },
67
71
  },
68
72
  allowPositionals: false,
@@ -84,6 +88,7 @@ export function parseServeArgs(
84
88
  "Options:",
85
89
  " --port, -p N Override configured port (default: 3030)",
86
90
  " --hostname, -H H Override configured bind address (default: 127.0.0.1)",
91
+ " --state-dir DIR Override the cesium state directory",
87
92
  " --idle-timeout DUR Auto-shutdown after DUR of inactivity. Accepts plain",
88
93
  " milliseconds or a suffixed value (90s, 30m, 2h).",
89
94
  " Use 0 / never / off to disable. Default: 0 (never).",
@@ -120,6 +125,14 @@ export function parseServeArgs(
120
125
  opts.hostname = values.hostname;
121
126
  }
122
127
 
128
+ if (values["state-dir"] !== undefined) {
129
+ if (values["state-dir"].length === 0) {
130
+ ctx.stderr.write(`cesium serve: --state-dir must not be empty\n`);
131
+ return null;
132
+ }
133
+ opts.stateDir = values["state-dir"];
134
+ }
135
+
123
136
  if (values["idle-timeout"] !== undefined) {
124
137
  const ms = parseDuration(values["idle-timeout"]);
125
138
  if (ms === null) {
@@ -153,6 +166,7 @@ export async function serveCommand(argv: string[], ctx?: Partial<ServeContext>):
153
166
  // Apply overrides from CLI flags
154
167
  const effectiveCfg = {
155
168
  ...cfg,
169
+ ...(opts.stateDir !== undefined ? { stateDir: opts.stateDir } : {}),
156
170
  ...(opts.port !== undefined ? { port: opts.port, portMax: opts.port } : {}),
157
171
  ...(opts.hostname !== undefined ? { hostname: opts.hostname } : {}),
158
172
  idleTimeoutMs: effectiveIdleTimeoutMs,
@@ -160,12 +174,14 @@ export async function serveCommand(argv: string[], ctx?: Partial<ServeContext>):
160
174
 
161
175
  let serverInfo: { port: number; url: string };
162
176
  try {
163
- serverInfo = await ensureRunning({
177
+ const theme = mergeTheme(themeFromPreset(effectiveCfg.themePreset), effectiveCfg.theme);
178
+ serverInfo = await runServerForeground({
164
179
  stateDir: effectiveCfg.stateDir,
165
180
  port: effectiveCfg.port,
166
181
  portMax: effectiveCfg.portMax,
167
182
  idleTimeoutMs: effectiveCfg.idleTimeoutMs,
168
183
  hostname: effectiveCfg.hostname,
184
+ theme,
169
185
  });
170
186
  } catch (err) {
171
187
  const e = err as Error;
package/src/index.ts CHANGED
@@ -10,12 +10,15 @@ import { createWaitTool } from "./tools/wait.ts";
10
10
  import { createStyleguideTool } from "./tools/styleguide.ts";
11
11
  import { createCritiqueTool } from "./tools/critique.ts";
12
12
  import { createStopTool } from "./tools/stop.ts";
13
+ import { generateBlockFieldReference } from "./prompt/field-reference.ts";
13
14
 
14
- const PROMPT_FRAGMENT = await readFile(
15
+ const rawFragment = await readFile(
15
16
  join(dirname(fileURLToPath(import.meta.url)), "prompt/system-fragment.md"),
16
17
  "utf8",
17
18
  );
18
19
 
20
+ const PROMPT_FRAGMENT = rawFragment.replace("{{BLOCK_FIELD_REFERENCE}}", generateBlockFieldReference());
21
+
19
22
  export const CesiumPlugin: Plugin = async (ctx): Promise<Hooks> => {
20
23
  return {
21
24
  tool: {
@@ -0,0 +1,94 @@
1
+ // Generates a compact block field reference from the catalog.
2
+ // Injected into the system-fragment at plugin load time via placeholder replacement.
3
+ // src/prompt/field-reference.ts
4
+
5
+ import { blockCatalog } from "../render/blocks/catalog.ts";
6
+ import type { Block } from "../render/blocks/types.ts";
7
+
8
+ // ─── Schema → compact field description ──────────────────────────────────────
9
+
10
+ type SchemaNode = Record<string, unknown>;
11
+
12
+ function formatFieldType(node: SchemaNode): string {
13
+ if ("const" in node) return `"${String(node["const"])}"`;
14
+ const type = node["type"] as string | undefined;
15
+ if (type === undefined) return "unknown";
16
+ if (type === "string") {
17
+ const enumVals = node["enum"] as string[] | undefined;
18
+ if (enumVals !== undefined) return enumVals.map((e) => `"${e}"`).join(" | ");
19
+ return "string";
20
+ }
21
+ if (type === "number") return "number";
22
+ if (type === "boolean") return "boolean";
23
+ if (type === "array") {
24
+ const items = node["items"] as SchemaNode | undefined;
25
+ if (items !== undefined) {
26
+ const innerType = items["type"] as string | undefined;
27
+ if (innerType === "object") {
28
+ const props = items["properties"] as Record<string, SchemaNode> | undefined;
29
+ const req = items["required"] as string[] | undefined;
30
+ if (props !== undefined) {
31
+ const fields = Object.entries(props)
32
+ .filter(([k]) => k !== "type")
33
+ .map(([k, v]) => {
34
+ const isRequired = req !== undefined && req.includes(k);
35
+ return `${k}${isRequired ? "" : "?"}: ${formatFieldType(v)}`;
36
+ })
37
+ .join(", ");
38
+ return `[{ ${fields} }]`;
39
+ }
40
+ return "object[]";
41
+ }
42
+ if (innerType === "string") return "string[]";
43
+ return `${innerType ?? "unknown"}[]`;
44
+ }
45
+ return "array";
46
+ }
47
+ if (type === "object") return "object";
48
+ return type;
49
+ }
50
+
51
+ function formatBlockLine(type: Block["type"]): string {
52
+ const entry = blockCatalog[type];
53
+ const schema = entry.schema as SchemaNode;
54
+ const props = schema["properties"] as Record<string, SchemaNode> | undefined;
55
+ const required = schema["required"] as string[] | undefined;
56
+
57
+ if (props === undefined) return `- \`${type}\``;
58
+
59
+ const fields = Object.entries(props)
60
+ .filter(([k]) => k !== "type")
61
+ .map(([k, v]) => {
62
+ const isRequired = required !== undefined && required.includes(k);
63
+ return `${k}${isRequired ? "" : "?"}: ${formatFieldType(v)}`;
64
+ })
65
+ .join(", ");
66
+
67
+ return `- \`${type}\` — ${fields}`;
68
+ }
69
+
70
+ /**
71
+ * Generates a compact markdown block field reference from the catalog.
72
+ * This is injected into system-fragment.md at the {{BLOCK_FIELD_REFERENCE}} placeholder.
73
+ */
74
+ export function generateBlockFieldReference(): string {
75
+ const lines = [
76
+ "## Block field reference",
77
+ "",
78
+ "For full schemas with rendered examples, call `cesium_styleguide`. Exact field names:",
79
+ "",
80
+ ];
81
+
82
+ for (const type of Object.keys(blockCatalog) as Block["type"][]) {
83
+ lines.push(formatBlockLine(type));
84
+ }
85
+
86
+ lines.push("");
87
+ lines.push(
88
+ "All `markdown` fields support `**bold**`, `*italic*`, `` `code` ``, lists, blockquotes, " +
89
+ "and the safelisted inline tags `<kbd>`, `<span class=\"pill\">`, `<span class=\"tag\">`. " +
90
+ "External URLs in links render as plain text.",
91
+ );
92
+
93
+ return lines.join("\n");
94
+ }
@@ -1,97 +1,88 @@
1
1
  # Cesium — beautiful HTML artifacts
2
2
 
3
+ Cesium publishes beautiful self-contained artifacts to a local server you can open in a browser.
4
+
3
5
  You have access to six tools:
4
6
 
5
7
  - `cesium_publish` — write a substantive response as a self-contained HTML document
6
8
  - `cesium_ask` — publish an interactive Q&A artifact; returns `{ id, httpUrl, ... }`
7
9
  - `cesium_wait` — block until the user completes a `cesium_ask` artifact (polls disk)
8
- - `cesium_styleguide` — fetch the full HTML design system reference (call this before writing anything complex)
9
- - `cesium_critique` — analyze a draft body for design-system adherence; returns a 0-100 score and findings
10
+ - `cesium_styleguide` — fetch the full block reference (call before writing anything complex)
11
+ - `cesium_critique` — analyze a draft artifact; returns a 0-100 score and findings
10
12
  - `cesium_stop` — stop the running cesium HTTP server
11
13
 
12
- ## When to publish (vs. reply in terminal)
13
-
14
- Publish when:
15
-
16
- - Your response would be 400 words
17
- - It contains a comparison, decision matrix, or multi-section plan/PRD/RFC
18
- - It is a code review with more than 3 findings
19
- - It is a design proposal, audit, post-mortem, or explainer
20
- - The user is likely to re-read, share, or come back to it
21
-
22
- Stay in terminal for:
23
-
24
- - Short factual answers
25
- - Status updates ("done", "running tests", "fixed")
26
- - Mid-tool-call chatter
27
- - Single-paragraph replies
28
- - Acknowledgements
29
-
30
- User overrides:
14
+ ## Two input modes
15
+
16
+ `cesium_publish` accepts either `blocks: Block[]` (preferred) or `html: string` (escape valve). Provide exactly one.
17
+
18
+ **Prefer `blocks`** for plans, reviews, reports, explainers, comparisons, audits, design docs. Blocks are token-efficient (no structural boilerplate), server-templated, and machine-checkable. Use `html` only when the whole document needs bespoke art-direction. For isolated bespoke regions, use `raw_html` or `diagram` blocks.
19
+
20
+ ### Example
21
+
22
+ ```json
23
+ { "title": "Migration Guide", "kind": "plan", "blocks": [
24
+ { "type": "hero", "eyebrow": "v2", "title": "Migration Guide",
25
+ "meta": [{ "k": "Status", "v": "Draft" }, { "k": "Owner", "v": "platform" }] },
26
+ { "type": "tldr", "markdown": "**Summary:** Update one import path and bump the SDK." },
27
+ { "type": "section", "title": "What Changed", "children": [
28
+ { "type": "prose", "markdown": "The `auth` module is now a standalone package." },
29
+ { "type": "callout", "variant": "warn", "markdown": "Change `sdk/auth` imports before upgrading." }
30
+ ]},
31
+ { "type": "risk_table", "rows": [
32
+ { "risk": "Missed imports", "likelihood": "medium", "impact": "high", "mitigation": "Run codemods." }
33
+ ]},
34
+ { "type": "timeline", "items": [
35
+ { "label": "Phase 1", "text": "Audit existing imports", "date": "2026-06-01" },
36
+ { "label": "Phase 2", "text": "Run migration script" }
37
+ ]}
38
+ ]}
39
+ ```
31
40
 
32
- - "/cesium", "publish this", "make me an HTML report" → publish
33
- - "in terminal", "just tell me", "don't make a doc" → don't publish
41
+ ## Quick block reference
34
42
 
35
- When uncertain: publish AND emit a 2-3 line terminal summary pointing at the doc. Cheap to over-publish, expensive to under-publish.
43
+ Call `cesium_styleguide` for full schemas and rendered examples.
36
44
 
37
- ## How to write the body
45
+ - `hero` page-title header (eyebrow, subtitle, meta pairs)
46
+ - `tldr` — summary box; at most one per document
47
+ - `section` — numbered section with child blocks (depth ≤ 3)
48
+ - `prose` — free-form markdown; `list` — bullet/numbered/checklist
49
+ - `callout` — aside with variant: note/warn/risk; `divider` — rule
50
+ - `code` — fenced code with lang; `timeline` — milestone list
51
+ - `compare_table` — comparison grid; `risk_table` — risk grid
52
+ - `kv` — key-value pairs; `pill_row` — pill/tag chips
53
+ - `diagram` — SVG/HTML visual (scrubbed)
54
+ - `raw_html` — custom HTML escape hatch (scrubbed; add `purpose`)
38
55
 
39
- The `html` argument is body-only (no `<!doctype>`, `<html>`, `<head>`, `<body>` wrappers — the plugin adds them).
56
+ {{BLOCK_FIELD_REFERENCE}}
40
57
 
41
- Use these classes (full reference via `cesium_styleguide`):
58
+ ## When to use raw_html / diagram
42
59
 
43
- - `.eyebrow` `.h-display` `.h-section` `.section-num`
44
- - `.card` `.tldr` `.callout` (`.note`/`.warn`/`.risk`)
45
- - `.code` (with `.kw` `.str` `.cm` `.fn` highlights)
46
- - `.timeline` `.diagram` `.compare-table` `.risk-table`
47
- - `.kbd` `.pill` `.tag`
60
+ - `diagram` SVG visualizations, bespoke layouts.
61
+ - `raw_html` anything no typed block covers. Critique flags overuse (>2 blocks or >30% of body characters).
48
62
 
49
- Inline `style="..."` and inline `<svg>` are encouraged for bespoke diagrams. NEVER reference external resources (no `<script src>`, no remote fonts, no CDN images).
63
+ ## When to publish (vs. reply in terminal)
50
64
 
51
- ## Tone
65
+ Publish when: ≥ 400 words; comparison/matrix/plan/PRD/RFC; code review with >3 findings; design proposal/audit/explainer; or the user will re-read or share it. Stay in terminal for short answers and status updates.
52
66
 
53
- Warm, considered, not flashy. Match the aesthetic of a thoughtful design document, not marketing material.
67
+ User overrides: "/cesium" or "publish this" publish; "in terminal" don't.
54
68
 
55
69
  ## Self-check before publishing
56
70
 
57
- For substantial artifacts (plans, reviews, comparisons, explainers > 500 words),
58
- call `cesium_critique` with your draft body BEFORE calling `cesium_publish`. Act
59
- on warn-level findings; consider suggest-level. info-level is FYI.
60
-
61
- If critique reports score < 70, revise the body before publishing.
71
+ Call `cesium_critique` before `cesium_publish` on substantive artifacts. Mode is auto-detected (pass `html` or `blocks`). Act on warn-level findings; consider suggest-level. If score < 70, revise.
62
72
 
63
73
  ## After publishing
64
74
 
65
- The tool returns URLs (file://, http://). Print a short 2-line terminal summary like:
66
-
67
- ```
68
- Cesium · <Title> (<kind>)
69
- http://localhost:3030/projects/.../...
70
- file:///.../...html
71
- ```
72
-
73
- Do not paste the full document content into the terminal after publishing.
74
-
75
- ## Stopping the server
76
-
77
- If the user asks to stop, restart, or recycle the cesium server (e.g. after a
78
- config change), call `cesium_stop`. The next `cesium_publish` will lazy-start
79
- a fresh server with the latest config.
75
+ Print a 2-line terminal summary: `Cesium · <Title> (<kind>)` + the HTTP URL. Do not paste the full document content into the terminal.
80
76
 
81
77
  ## Interactive Q&A: cesium_ask + cesium_wait
82
78
 
83
- When you need structured user input before producing a final artifact (design tradeoffs,
84
- plan branches, confirmation gates), publish an interactive artifact:
85
-
86
79
  1. `cesium_ask({ title, body, questions: [...] })` → returns `{ id, httpUrl, ... }`
87
80
  2. Print the terminalSummary so the user knows where to click.
88
81
  3. `cesium_wait({ id })` → blocks until user finishes (or 10-min timeout).
89
- 4. Decide next step from `result.answers` — typically `cesium_publish` with the chosen path.
82
+ 4. Decide next step from `result.answers`.
90
83
 
91
- Question types: pick_one, pick_many, confirm, ask_text, slider, react. The artifact
92
- is a permanent record of the conversation; once answered, controls freeze into a static
93
- markup that captures the user's decisions. Set `optional: true` on an `ask_text` question
94
- to add a Skip button (useful for "anything else?"-type follow-ups).
84
+ Question types: pick_one, pick_many, confirm, ask_text, slider, react. Set `optional: true` on an `ask_text` question to add a Skip button. Don't use cesium_ask for trivial yes/no questions — use it when the question deserves to live on disk as a decision record.
85
+
86
+ ## Stopping the server
95
87
 
96
- Don't use cesium_ask for trivial yes/no questions you can ask in the terminal. Use it
97
- when the question deserves to live on disk as a decision record.
88
+ Call `cesium_stop` to stop or restart. The next `cesium_publish` will lazy-start a fresh server.
@@ -0,0 +1,39 @@
1
+ // Catalog — source of truth: aggregates meta from every renderer module.
2
+ // src/render/blocks/catalog.ts
3
+
4
+ import type { Block, BlockMeta } from "./types.ts";
5
+ import { meta as heroMeta } from "./renderers/hero.ts";
6
+ import { meta as tldrMeta } from "./renderers/tldr.ts";
7
+ import { meta as sectionMeta } from "./renderers/section.ts";
8
+ import { meta as proseMeta } from "./renderers/prose.ts";
9
+ import { meta as listMeta } from "./renderers/list.ts";
10
+ import { meta as calloutMeta } from "./renderers/callout.ts";
11
+ import { meta as codeMeta } from "./renderers/code.ts";
12
+ import { meta as timelineMeta } from "./renderers/timeline.ts";
13
+ import { meta as compareTableMeta } from "./renderers/compare-table.ts";
14
+ import { meta as riskTableMeta } from "./renderers/risk-table.ts";
15
+ import { meta as kvMeta } from "./renderers/kv.ts";
16
+ import { meta as pillRowMeta } from "./renderers/pill-row.ts";
17
+ import { meta as dividerMeta } from "./renderers/divider.ts";
18
+ import { meta as diagramMeta } from "./renderers/diagram.ts";
19
+ import { meta as rawHtmlMeta } from "./renderers/raw-html.ts";
20
+
21
+ export const blockCatalog: Record<Block["type"], BlockMeta> = {
22
+ hero: heroMeta,
23
+ tldr: tldrMeta,
24
+ section: sectionMeta,
25
+ prose: proseMeta,
26
+ list: listMeta,
27
+ callout: calloutMeta,
28
+ code: codeMeta,
29
+ timeline: timelineMeta,
30
+ compare_table: compareTableMeta,
31
+ risk_table: riskTableMeta,
32
+ kv: kvMeta,
33
+ pill_row: pillRowMeta,
34
+ divider: dividerMeta,
35
+ diagram: diagramMeta,
36
+ raw_html: rawHtmlMeta,
37
+ };
38
+
39
+ export const blockTypes = Object.keys(blockCatalog) as Array<Block["type"]>;
@@ -0,0 +1,27 @@
1
+ // Pure HTML escape helpers — no dependencies.
2
+ // src/render/blocks/escape.ts
3
+
4
+ /** Escapes a string for safe insertion as HTML text content. */
5
+ export function escapeHtml(s: string): string {
6
+ if (typeof s !== "string") {
7
+ throw new Error(`escapeHtml expected string, got ${typeof s}: ${String(s)}`);
8
+ }
9
+ return s
10
+ .replace(/&/g, "&amp;")
11
+ .replace(/</g, "&lt;")
12
+ .replace(/>/g, "&gt;")
13
+ .replace(/"/g, "&quot;")
14
+ .replace(/'/g, "&#39;");
15
+ }
16
+
17
+ /** Escapes a string for safe insertion as an HTML attribute value (double-quoted). */
18
+ export function escapeAttr(s: string): string {
19
+ if (typeof s !== "string") {
20
+ throw new Error(`escapeAttr expected string, got ${typeof s}: ${String(s)}`);
21
+ }
22
+ return s
23
+ .replace(/&/g, "&amp;")
24
+ .replace(/"/g, "&quot;")
25
+ .replace(/</g, "&lt;")
26
+ .replace(/>/g, "&gt;");
27
+ }