@cfbender/cesium 0.6.1 → 0.7.0

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 (35) hide show
  1. package/CHANGELOG.md +100 -1
  2. package/package.json +1 -1
  3. package/src/index.ts +2 -0
  4. package/src/prompt/system-fragment.md +73 -8
  5. package/src/render/annotate-frozen.ts +90 -0
  6. package/src/render/blocks/render.ts +20 -0
  7. package/src/render/blocks/renderers/callout.ts +3 -2
  8. package/src/render/blocks/renderers/code.ts +17 -2
  9. package/src/render/blocks/renderers/compare-table.ts +3 -2
  10. package/src/render/blocks/renderers/diagram.ts +3 -2
  11. package/src/render/blocks/renderers/diff.ts +23 -9
  12. package/src/render/blocks/renderers/hero.ts +3 -2
  13. package/src/render/blocks/renderers/kv.ts +3 -2
  14. package/src/render/blocks/renderers/list.ts +5 -4
  15. package/src/render/blocks/renderers/pill-row.ts +3 -2
  16. package/src/render/blocks/renderers/prose.ts +8 -2
  17. package/src/render/blocks/renderers/raw-html.ts +8 -2
  18. package/src/render/blocks/renderers/risk-table.ts +3 -2
  19. package/src/render/blocks/renderers/section.ts +4 -2
  20. package/src/render/blocks/renderers/timeline.ts +3 -2
  21. package/src/render/blocks/renderers/tldr.ts +3 -2
  22. package/src/render/client-js.ts +804 -6
  23. package/src/render/critique.ts +5 -335
  24. package/src/render/theme.ts +431 -6
  25. package/src/render/validate.ts +353 -97
  26. package/src/render/wrap.ts +67 -9
  27. package/src/server/api.ts +162 -3
  28. package/src/storage/index-gen.ts +4 -2
  29. package/src/storage/mutate.ts +433 -27
  30. package/src/tools/annotate.ts +336 -0
  31. package/src/tools/ask.ts +2 -6
  32. package/src/tools/critique.ts +15 -45
  33. package/src/tools/publish.ts +16 -56
  34. package/src/tools/styleguide.ts +7 -1
  35. package/src/tools/wait.ts +77 -24
@@ -0,0 +1,336 @@
1
+ // Tool handler for cesium_annotate — publishes an interactive review artifact.
2
+
3
+ import { createHash } from "node:crypto";
4
+ import { join } from "node:path";
5
+ import { tool } from "@opencode-ai/plugin";
6
+ import type { PluginInput } from "@opencode-ai/plugin";
7
+ import { loadConfig, type CesiumConfig } from "../config.ts";
8
+ import { extractTextContent } from "../render/extract.ts";
9
+ import { themeFromPreset, mergeTheme } from "../render/theme.ts";
10
+ import { validateAnnotateInput, validateBlocksArray } from "../render/validate.ts";
11
+ import { renderBlocks } from "../render/blocks/render.ts";
12
+ import { resolveHighlightTheme } from "../render/blocks/highlight.ts";
13
+ import { wrapDocument, type ArtifactMeta } from "../render/wrap.ts";
14
+ import type { InteractiveData } from "../render/validate.ts";
15
+ import { deriveProjectIdentity, artifactFilename, pathsFor } from "../storage/paths.ts";
16
+ import { atomicWrite } from "../storage/write.ts";
17
+ import { ensureThemeCss } from "../storage/assets.ts";
18
+ import { writeFaviconSvg } from "../storage/favicon-write.ts";
19
+ import { loadIndex, writeIndex, appendEntry, type IndexEntry } from "../storage/index-cache.ts";
20
+ import { withLock } from "../storage/lock.ts";
21
+ import { renderProjectIndex, renderGlobalIndex } from "../storage/index-gen.ts";
22
+ import { buildProjectSummaries } from "../storage/project-summaries.ts";
23
+ import {
24
+ ensureServerRunning as defaultEnsureServerRunning,
25
+ type RunningInfo,
26
+ type LifecycleConfig,
27
+ } from "../server/lifecycle.ts";
28
+ import { buildTerminalSummary, resolveDisplayHost } from "./publish.ts";
29
+
30
+ export interface AnnotateToolOverrides {
31
+ loadConfig?: () => CesiumConfig;
32
+ now?: () => Date;
33
+ nanoid?: () => string;
34
+ ensureRunning?: (cfg: LifecycleConfig) => Promise<RunningInfo | null>;
35
+ }
36
+
37
+ const TOOL_DESCRIPTION = `cesium_annotate — Publish an interactive review artifact where the user can leave
38
+ per-line and per-block comments, plus a final verdict (Approve / Request changes / Comment).
39
+
40
+ Use this when reviewing diffs, plans, PRDs, code proposals, RFCs, audits, or design docs —
41
+ any content where chat-based feedback would be lossy. The artifact is a self-contained .html
42
+ file with a comment rail and a sticky verdict footer.
43
+
44
+ When NOT to use:
45
+ - Short yes/no approvals → use cesium_ask with a react question instead.
46
+ - One-way broadcasts with no feedback needed → use cesium_publish.
47
+
48
+ Workflow:
49
+ 1. Call cesium_annotate → get back { id, filePath, fileUrl, httpUrl, terminalSummary }.
50
+ 2. Call cesium_wait with the returned id to block until the user finishes their review.
51
+ 3. If the user requests changes, revise and publish a new cesium_annotate (or cesium_publish)
52
+ with supersedes pointing at the prior id.
53
+
54
+ Arguments:
55
+ - title: descriptive title (3–8 words).
56
+ - blocks: array of structured content blocks — all reviewable content lives here.
57
+ Call cesium_styleguide for the full block catalog.
58
+ - verdictMode: "approve" | "approve-or-reject" | "full" (default: "full").
59
+ "full" exposes Approve / Request changes / Comment buttons.
60
+ - perLineFor: block types that get per-line comment anchors (default: ["diff", "code"]).
61
+ - requireVerdict: whether the user must submit a verdict before completing (default: true).
62
+ - summary, tags, expiresAt: same semantics as cesium_publish.`;
63
+
64
+ export function createAnnotateTool(
65
+ ctx: PluginInput,
66
+ overrides?: AnnotateToolOverrides,
67
+ ): ReturnType<typeof tool> {
68
+ const resolveConfig = overrides?.loadConfig ?? loadConfig;
69
+ const now = overrides?.now ?? (() => new Date());
70
+ const genId = overrides?.nanoid ?? defaultNanoid;
71
+ const runEnsureRunning = overrides?.ensureRunning ?? defaultEnsureServerRunning;
72
+
73
+ return tool({
74
+ description: TOOL_DESCRIPTION,
75
+ args: {
76
+ title: tool.schema.string(),
77
+ blocks: tool.schema.array(tool.schema.any()),
78
+ verdictMode: tool.schema
79
+ .enum(["approve", "approve-or-reject", "full"] as [string, ...string[]])
80
+ .optional(),
81
+ perLineFor: tool.schema.array(tool.schema.string()).optional(),
82
+ requireVerdict: tool.schema.boolean().optional(),
83
+ summary: tool.schema.string().optional(),
84
+ tags: tool.schema.array(tool.schema.string()).optional(),
85
+ expiresAt: tool.schema.string().optional(),
86
+ },
87
+ async execute(args, _context) {
88
+ // 1. Validate top-level input shape
89
+ const validation = validateAnnotateInput(args);
90
+ if (!validation.ok) {
91
+ return `Error: ${validation.error}`;
92
+ }
93
+ const input = validation.value;
94
+
95
+ // 2. Deep-validate block contents
96
+ const blocksValidation = validateBlocksArray(input.blocks);
97
+ if (!blocksValidation.ok) {
98
+ const errorMessages = blocksValidation.errors
99
+ .map((e) => `${e.path}: ${e.message}`)
100
+ .join("; ");
101
+ return `Error: blocks validation failed — ${errorMessages}`;
102
+ }
103
+
104
+ // 3. Load config
105
+ const config = resolveConfig();
106
+
107
+ // 4. Probe git state
108
+ const shell = ctx.$.cwd(ctx.directory).nothrow();
109
+ let gitRemote: string | null = null;
110
+ let gitBranch: string | null = null;
111
+ let gitCommit: string | null = null;
112
+
113
+ try {
114
+ const remoteResult = await shell`git config --get remote.origin.url`.quiet();
115
+ if (remoteResult.exitCode === 0) {
116
+ gitRemote = remoteResult.text().trim() || null;
117
+ }
118
+ } catch {
119
+ // not a git repo or no remote
120
+ }
121
+
122
+ try {
123
+ const branchResult = await shell`git rev-parse --abbrev-ref HEAD`.quiet();
124
+ if (branchResult.exitCode === 0) {
125
+ gitBranch = branchResult.text().trim() || null;
126
+ }
127
+ } catch {
128
+ // not a git repo
129
+ }
130
+
131
+ try {
132
+ const commitResult = await shell`git rev-parse HEAD`.quiet();
133
+ if (commitResult.exitCode === 0) {
134
+ gitCommit = commitResult.text().trim() || null;
135
+ }
136
+ } catch {
137
+ // not a git repo
138
+ }
139
+
140
+ // 5. Derive project identity
141
+ const identity = deriveProjectIdentity({
142
+ cwd: ctx.directory,
143
+ gitRemote,
144
+ worktree: ctx.worktree ?? null,
145
+ });
146
+
147
+ // 6. Generate id + timestamps
148
+ const id = genId();
149
+ const createdAt = now();
150
+
151
+ // 7. Defaults
152
+ const expiresAt =
153
+ input.expiresAt ?? new Date(createdAt.getTime() + 24 * 60 * 60 * 1000).toISOString();
154
+ const requireVerdict = input.requireVerdict ?? true;
155
+ const verdictMode = input.verdictMode ?? "full";
156
+ const perLineFor = input.perLineFor ?? ["diff", "code"];
157
+
158
+ // 8. Render blocks → trusted HTML (do NOT scrub — templated output is trusted)
159
+ const highlightTheme = resolveHighlightTheme(config.themePreset);
160
+ const bodyHtml = await renderBlocks(input.blocks, { highlightTheme });
161
+
162
+ // 8a. Extract body text for full-text search
163
+ const bodyText = extractTextContent(bodyHtml);
164
+
165
+ // 9. Content SHA-256 over the rendered HTML
166
+ const contentSha256 = createHash("sha256").update(bodyHtml).digest("hex");
167
+
168
+ // 10. Compute filename + paths
169
+ const filename = artifactFilename({ title: input.title, id, createdAt });
170
+ const paths = pathsFor({
171
+ stateDir: config.stateDir,
172
+ projectSlug: identity.slug,
173
+ filename,
174
+ });
175
+
176
+ // 11. Build ArtifactMeta
177
+ const meta: ArtifactMeta = {
178
+ id,
179
+ title: input.title,
180
+ kind: "annotate",
181
+ summary: input.summary ?? null,
182
+ tags: input.tags ?? [],
183
+ createdAt: createdAt.toISOString(),
184
+ model: null,
185
+ sessionId: null,
186
+ projectSlug: identity.slug,
187
+ projectName: identity.name,
188
+ cwd: ctx.directory,
189
+ worktree: identity.worktree,
190
+ gitBranch,
191
+ gitCommit,
192
+ supersedes: null,
193
+ supersededBy: null,
194
+ contentSha256,
195
+ };
196
+
197
+ // 12. Build interactive data
198
+ const interactive: InteractiveData = {
199
+ kind: "annotate",
200
+ status: "open",
201
+ expiresAt,
202
+ verdictMode,
203
+ requireVerdict,
204
+ perLineFor,
205
+ comments: [],
206
+ verdict: null,
207
+ };
208
+
209
+ // 13. Build theme + ensure theme.css and favicon
210
+ const theme = mergeTheme(themeFromPreset(config.themePreset), config.theme);
211
+ await ensureThemeCss(config.stateDir, theme);
212
+ await writeFaviconSvg(config.stateDir);
213
+
214
+ // 14. Wrap document
215
+ const fullHtml = wrapDocument({
216
+ body: bodyHtml,
217
+ meta,
218
+ theme,
219
+ warnings: [],
220
+ themeCssHref: "../../../theme.css",
221
+ interactive,
222
+ });
223
+
224
+ // 15. Atomic write
225
+ await atomicWrite(paths.artifactPath, fullHtml);
226
+
227
+ // 16. Build IndexEntry
228
+ const entry: IndexEntry = {
229
+ id: meta.id,
230
+ title: meta.title,
231
+ kind: meta.kind,
232
+ summary: meta.summary,
233
+ tags: meta.tags,
234
+ createdAt: meta.createdAt,
235
+ filename,
236
+ supersedes: null,
237
+ supersededBy: null,
238
+ gitBranch: meta.gitBranch,
239
+ gitCommit: meta.gitCommit,
240
+ contentSha256: meta.contentSha256,
241
+ projectSlug: identity.slug,
242
+ projectName: identity.name,
243
+ bodyText,
244
+ };
245
+
246
+ const lockPath = join(config.stateDir, ".index.lock");
247
+
248
+ await withLock({ lockPath }, async () => {
249
+ // 17. Update per-project index
250
+ const projectEntries = await loadIndex(paths.projectIndexJsonPath);
251
+ const updatedProjectEntries = appendEntry(projectEntries, entry);
252
+ await writeIndex(paths.projectIndexJsonPath, updatedProjectEntries);
253
+
254
+ // 18. Update global index
255
+ const globalEntries = await loadIndex(paths.globalIndexJsonPath);
256
+ const updatedGlobalEntries = appendEntry(globalEntries, entry);
257
+ await writeIndex(paths.globalIndexJsonPath, updatedGlobalEntries);
258
+
259
+ // 19. Render and write project index.html
260
+ const latestProjectEntries = await loadIndex(paths.projectIndexJsonPath);
261
+ const projectIndexHtml = renderProjectIndex({
262
+ projectSlug: identity.slug,
263
+ projectName: identity.name,
264
+ entries: latestProjectEntries,
265
+ theme,
266
+ themeCssHref: "../../theme.css",
267
+ });
268
+ await atomicWrite(paths.projectIndexPath, projectIndexHtml);
269
+
270
+ // 20. Render and write global index.html
271
+ const latestGlobalEntries = await loadIndex(paths.globalIndexJsonPath);
272
+ const projectSummaries = buildProjectSummaries(latestGlobalEntries);
273
+ const globalIndexHtml = renderGlobalIndex({
274
+ projects: projectSummaries,
275
+ theme,
276
+ themeCssHref: "theme.css",
277
+ });
278
+ await atomicWrite(paths.globalIndexPath, globalIndexHtml);
279
+ });
280
+
281
+ // 21. Start server (best-effort) and build URLs
282
+ let httpUrl: string | null = null;
283
+ let serverInfo: RunningInfo | null = null;
284
+
285
+ try {
286
+ const maybeInfo = await runEnsureRunning({
287
+ stateDir: config.stateDir,
288
+ port: config.port,
289
+ portMax: config.portMax,
290
+ idleTimeoutMs: config.idleTimeoutMs,
291
+ hostname: config.hostname,
292
+ theme,
293
+ });
294
+ if (maybeInfo !== null) {
295
+ serverInfo = maybeInfo;
296
+ const liveDisplay = resolveDisplayHost(config.hostname);
297
+ httpUrl = `http://${liveDisplay}:${serverInfo.port}${paths.serverPath}`;
298
+ }
299
+ } catch {
300
+ httpUrl = null;
301
+ }
302
+
303
+ const terminalSummary = buildTerminalSummary({
304
+ title: input.title,
305
+ kind: "annotate",
306
+ httpUrl: httpUrl ?? paths.fileUrl,
307
+ fileUrl: paths.fileUrl,
308
+ isSsh: Boolean(process.env["SSH_CONNECTION"]),
309
+ port: serverInfo?.port ?? config.port,
310
+ });
311
+
312
+ // 22. Return result
313
+ const result = {
314
+ id,
315
+ filePath: paths.artifactPath,
316
+ fileUrl: paths.fileUrl,
317
+ httpUrl,
318
+ terminalSummary,
319
+ };
320
+
321
+ return JSON.stringify(result, null, 2);
322
+ },
323
+ });
324
+ }
325
+
326
+ // Default nanoid implementation using built-in crypto for alphanumeric IDs
327
+ function defaultNanoid(): string {
328
+ const alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
329
+ const bytes = new Uint8Array(6);
330
+ crypto.getRandomValues(bytes);
331
+ let result = "";
332
+ for (const byte of bytes) {
333
+ result += alphabet[byte % alphabet.length];
334
+ }
335
+ return result;
336
+ }
package/src/tools/ask.ts CHANGED
@@ -8,7 +8,7 @@ import { loadConfig, type CesiumConfig } from "../config.ts";
8
8
  import { scrub } from "../render/scrub.ts";
9
9
  import { extractTextContent } from "../render/extract.ts";
10
10
  import { themeFromPreset, mergeTheme } from "../render/theme.ts";
11
- import { validateAskInput, htmlBodyWarnings } from "../render/validate.ts";
11
+ import { validateAskInput } from "../render/validate.ts";
12
12
  import { wrapDocument, type ArtifactMeta } from "../render/wrap.ts";
13
13
  import type { InteractiveData } from "../render/validate.ts";
14
14
  import { deriveProjectIdentity, artifactFilename, pathsFor } from "../storage/paths.ts";
@@ -204,11 +204,11 @@ export function createAskTool(
204
204
  supersedes: null,
205
205
  supersededBy: null,
206
206
  contentSha256,
207
- inputMode: "html",
208
207
  };
209
208
 
210
209
  // 12. Build interactive data
211
210
  const interactive: InteractiveData = {
211
+ kind: "ask",
212
212
  status: "open",
213
213
  requireAll,
214
214
  expiresAt,
@@ -221,10 +221,6 @@ export function createAskTool(
221
221
  if (scrubbed.removed.length > 0) {
222
222
  warnings.push(`Removed ${scrubbed.removed.length} external resource(s) during scrub.`);
223
223
  }
224
- const bodyWarnings = htmlBodyWarnings(scrubbed.html);
225
- for (const w of bodyWarnings) {
226
- warnings.push(w);
227
- }
228
224
 
229
225
  // 14. Build theme + wrap document
230
226
  const theme = mergeTheme(themeFromPreset(config.themePreset), config.theme);
@@ -1,43 +1,34 @@
1
- // Tool handler for cesium_critique — mode-aware body analyzer.
2
- // Accepts either { html: string } (html mode) or { blocks: Block[] } (blocks mode). Exactly one required.
1
+ // Tool handler for cesium_critique — analyzes a draft blocks array.
3
2
 
4
3
  import { tool } from "@opencode-ai/plugin";
5
4
  import type { PluginInput } from "@opencode-ai/plugin";
6
- import {
7
- critiqueHtml,
8
- critiqueBlocks,
9
- type CritiqueResult,
10
- type CritiqueSeverity,
11
- } from "../render/critique.ts";
5
+ import { critique, type CritiqueResult, type CritiqueSeverity } from "../render/critique.ts";
12
6
  import type { Block } from "../render/blocks/types.ts";
13
7
 
14
- const TOOL_DESCRIPTION = `Analyze a draft HTML body for adherence to the cesium design
15
- system before publishing. Returns a 0-100 score and findings (warn/suggest/info).
8
+ const TOOL_DESCRIPTION = `Analyze a draft cesium artifact (blocks array) for adherence to the
9
+ cesium design system before publishing. Returns a 0-100 score and findings (warn/suggest/info).
16
10
 
17
- Call this on complex artifacts (>500 words, plans/comparisons/explainers) BEFORE
18
- calling cesium_publish. Address warn-level findings; suggest-level findings are
19
- optional but usually worth applying. info-level findings are FYI.
11
+ Call this on substantive artifacts before \`cesium_publish\`. Address warn-level findings;
12
+ suggest-level findings are optional but usually worth applying. info-level findings are FYI.
20
13
 
21
- The 'html' argument is the same body you'd pass to cesium_publish — body inner
22
- HTML only, no <!doctype>/<html>/<head>/<body> wrappers.`;
14
+ The 'blocks' argument is the same array you'd pass to cesium_publish.`;
23
15
 
24
16
  /**
25
17
  * Format a CritiqueResult into a concise human-readable string the agent can parse.
26
18
  * Format:
27
19
  * score: 87/100
28
- * mode: html
29
20
  *
30
21
  * warn:
31
- * - [external-resource] External resource will be stripped...
22
+ * - [raw-html-overuse] raw_html overuse: 3 raw_html blocks...
32
23
  *
33
24
  * suggest:
34
- * - [no-tldr] Long artifact with no .tldr summary...
25
+ * - [missing-tldr] Document has 6 sections but no tldr block...
35
26
  *
36
27
  * info:
37
- * - [code-without-highlights] Code blocks render without...
28
+ * - [code-without-meaningful-lang] Code block at ... uses lang "text"...
38
29
  */
39
30
  export function formatCritiqueForAgent(result: CritiqueResult): string {
40
- const lines: string[] = [`score: ${result.score}/100`, `mode: ${result.mode}`];
31
+ const lines: string[] = [`score: ${result.score}/100`];
41
32
 
42
33
  const bySeverity: Record<CritiqueSeverity, typeof result.findings> = {
43
34
  warn: [],
@@ -67,34 +58,13 @@ export function createCritiqueTool(_ctx: PluginInput): ReturnType<typeof tool> {
67
58
  return tool({
68
59
  description: TOOL_DESCRIPTION,
69
60
  args: {
70
- html: tool.schema.string().optional(),
71
- blocks: tool.schema.any().optional(),
61
+ blocks: tool.schema.any(),
72
62
  },
73
63
  async execute(args) {
74
- const hasHtml = args.html !== undefined && args.html !== null;
75
- const hasBlocks = args.blocks !== undefined && args.blocks !== null;
76
-
77
- if (hasHtml && hasBlocks) {
78
- return "error: provide exactly one of html or blocks, not both";
79
- }
80
- if (!hasHtml && !hasBlocks) {
81
- return "error: provide exactly one of html or blocks";
82
- }
83
-
84
- let result: CritiqueResult;
85
-
86
- if (hasHtml) {
87
- if (typeof args.html !== "string") {
88
- return "error: html must be a string";
89
- }
90
- result = critiqueHtml(args.html);
91
- } else {
92
- if (!Array.isArray(args.blocks)) {
93
- return "error: blocks must be an array";
94
- }
95
- result = critiqueBlocks(args.blocks as Block[]);
64
+ if (!Array.isArray(args.blocks)) {
65
+ return "error: blocks must be an array";
96
66
  }
97
-
67
+ const result = critique(args.blocks as Block[]);
98
68
  return formatCritiqueForAgent(result);
99
69
  },
100
70
  });
@@ -6,10 +6,9 @@ import { join } from "node:path";
6
6
  import { tool } from "@opencode-ai/plugin";
7
7
  import type { PluginInput } from "@opencode-ai/plugin";
8
8
  import { loadConfig, type CesiumConfig } from "../config.ts";
9
- import { scrub } from "../render/scrub.ts";
10
9
  import { extractTextContent } from "../render/extract.ts";
11
10
  import { themeFromPreset, mergeTheme } from "../render/theme.ts";
12
- import { validatePublishInput, htmlBodyWarnings, PUBLISH_KINDS } from "../render/validate.ts";
11
+ import { validatePublishInput, PUBLISH_KINDS } from "../render/validate.ts";
13
12
  import { renderBlocks } from "../render/blocks/render.ts";
14
13
  import { resolveHighlightTheme } from "../render/blocks/highlight.ts";
15
14
  import { wrapDocument, type ArtifactMeta } from "../render/wrap.ts";
@@ -74,29 +73,14 @@ When to stay in the terminal: short factual answers, status updates ("done", "fi
74
73
 
75
74
  User overrides win: "/cesium" or "publish this" → publish; "in terminal" → don't.
76
75
 
77
- The \`html\` argument is the BODY ONLYdo NOT include <!doctype>, <html>, <head>, <body>.
78
- The plugin wraps your body with the design system.
79
-
80
- Available CSS classes (call cesium_styleguide for full reference + examples):
81
- - .eyebrow uppercase mono micro-label above headings
82
- - .h-display page-title heading
83
- - .h-section section heading paired with .section-num
84
- - .section-num numbered chip ("01", "02") next to .h-section
85
- - .card bordered surface block (1.5px border, 12px radius)
86
- - .tldr clay-bordered summary box (use ONE per doc, near top)
87
- - .callout info box; modifiers: .callout.note .callout.warn .callout.risk
88
- - .code block-level code panel; inline highlights via .kw .str .cm .fn
89
- - .timeline milestone list with dots and connectors
90
- - .diagram wraps inline SVG with a caption below
91
- - .compare-table bordered comparison grid
92
- - .risk-table bordered risk-grid (likelihood/impact/mitigation columns)
93
- - .kbd .pill .tag inline chips
94
- - .byline rendered automatically as the footer
95
-
96
- Inline \`style="..."\` and inline \`<svg>\` are encouraged for bespoke diagrams. NEVER reference
97
- external resources: no <script src=>, no <link rel=stylesheet href=http>, no remote fonts,
98
- no remote images. The plugin will silently strip external resources but the artifact will
99
- look broken in the resulting render.
76
+ Content is described as a \`blocks\` arraya closed set of typed building blocks
77
+ (hero, tldr, section, prose, list, callout, code, diff, timeline, compare_table,
78
+ risk_table, kv, pill_row, divider, diagram, raw_html). Call \`cesium_styleguide\` for
79
+ the full block catalog with schemas and rendered examples.
80
+
81
+ The \`raw_html\` block is an escape hatch for content that doesn't fit the available
82
+ blocks; \`diagram\` is for inline SVG/HTML visualizations. Both have their payloads
83
+ scrubbed of external resources at publish time.
100
84
 
101
85
  Title aim: 3-8 words, descriptive. Kind: pick the closest match.`;
102
86
 
@@ -114,15 +98,10 @@ export function createPublishTool(
114
98
  args: {
115
99
  title: tool.schema.string(),
116
100
  kind: tool.schema.enum([...PUBLISH_KINDS] as [string, ...string[]]),
117
- html: tool.schema
118
- .string()
119
- .optional()
120
- .describe("Body HTML — escape valve / legacy mode. Provide exactly one of html or blocks."),
121
101
  blocks: tool.schema
122
102
  .array(tool.schema.any())
123
- .optional()
124
103
  .describe(
125
- "Structured block array preferred for token efficiency. Provide exactly one of html or blocks.",
104
+ "Structured block array describing the artifact's content. Call cesium_styleguide for the block catalog.",
126
105
  ),
127
106
  summary: tool.schema.string().optional(),
128
107
  tags: tool.schema.array(tool.schema.string()).optional(),
@@ -185,21 +164,9 @@ export function createPublishTool(
185
164
  // 6. Timestamps
186
165
  const createdAt = now();
187
166
 
188
- // 7. Render body (blocks path or html path)
189
- let bodyHtml: string;
190
- let scrubRemovedCount = 0;
191
- const inputMode: "html" | "blocks" = input.blocks !== undefined ? "blocks" : "html";
192
-
193
- if (input.blocks !== undefined) {
194
- // Blocks path: render structured blocks → trusted HTML
195
- const highlightTheme = resolveHighlightTheme(config.themePreset);
196
- bodyHtml = await renderBlocks(input.blocks, { highlightTheme });
197
- } else {
198
- // HTML path: scrub agent-supplied HTML
199
- const scrubbed = scrub(input.html);
200
- bodyHtml = scrubbed.html;
201
- scrubRemovedCount = scrubbed.removed.length;
202
- }
167
+ // 7. Render body from structured blocks
168
+ const highlightTheme = resolveHighlightTheme(config.themePreset);
169
+ const bodyHtml = await renderBlocks(input.blocks, { highlightTheme });
203
170
 
204
171
  // 7a. Extract body text for full-text search
205
172
  const bodyText = extractTextContent(bodyHtml);
@@ -234,18 +201,11 @@ export function createPublishTool(
234
201
  supersedes: input.supersedes ?? null,
235
202
  supersededBy: null,
236
203
  contentSha256,
237
- inputMode,
238
204
  };
239
205
 
240
- // 11. Build warnings
206
+ // 11. Build warnings (reserved for future use — block-mode publishes
207
+ // produce trusted templated output so there is nothing to warn about today).
241
208
  const warnings: string[] = [];
242
- if (scrubRemovedCount > 0) {
243
- warnings.push(`Removed ${scrubRemovedCount} external resource(s) during scrub.`);
244
- }
245
- const bodyWarnings = input.html !== undefined ? htmlBodyWarnings(bodyHtml) : [];
246
- for (const w of bodyWarnings) {
247
- warnings.push(w);
248
- }
249
209
 
250
210
  // 12. Build theme + wrap document
251
211
  const theme = mergeTheme(themeFromPreset(config.themePreset), config.theme);
@@ -282,7 +242,7 @@ export function createPublishTool(
282
242
  projectSlug: identity.slug,
283
243
  projectName: identity.name,
284
244
  bodyText,
285
- inputMode,
245
+ inputMode: "blocks",
286
246
  };
287
247
 
288
248
  const lockPath = join(config.stateDir, ".index.lock");
@@ -8,7 +8,13 @@ import type { RenderCtx, SectionCounter } from "../render/blocks/render.ts";
8
8
 
9
9
  function makeCtx(): RenderCtx {
10
10
  const counter: SectionCounter = { value: 1 };
11
- return { sectionCounter: counter, depth: 0, path: "blocks[0]", highlightTheme: "claret-dark" };
11
+ return {
12
+ sectionCounter: counter,
13
+ depth: 0,
14
+ path: "blocks[0]",
15
+ highlightTheme: "claret-dark",
16
+ anchor: null,
17
+ };
12
18
  }
13
19
 
14
20
  /** Escape a string for safe insertion inside a markdown fenced code block. */