@cfbender/cesium 0.3.5

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 (44) hide show
  1. package/ARCHITECTURE.md +304 -0
  2. package/CHANGELOG.md +335 -0
  3. package/LICENSE +21 -0
  4. package/README.md +479 -0
  5. package/agents/cesium.md +39 -0
  6. package/assets/styleguide.html +857 -0
  7. package/package.json +61 -0
  8. package/src/cli/commands/ls.ts +186 -0
  9. package/src/cli/commands/open.ts +208 -0
  10. package/src/cli/commands/prune.ts +348 -0
  11. package/src/cli/commands/restart.ts +38 -0
  12. package/src/cli/commands/serve.ts +214 -0
  13. package/src/cli/commands/stop.ts +130 -0
  14. package/src/cli/commands/theme.ts +333 -0
  15. package/src/cli/index.ts +78 -0
  16. package/src/config.ts +94 -0
  17. package/src/index.ts +35 -0
  18. package/src/prompt/system-fragment.md +97 -0
  19. package/src/render/client-js.ts +316 -0
  20. package/src/render/controls.ts +302 -0
  21. package/src/render/critique.ts +360 -0
  22. package/src/render/extract.ts +83 -0
  23. package/src/render/scrub.ts +141 -0
  24. package/src/render/theme.ts +712 -0
  25. package/src/render/validate.ts +524 -0
  26. package/src/render/wrap.ts +165 -0
  27. package/src/server/api.ts +166 -0
  28. package/src/server/http.ts +195 -0
  29. package/src/server/lifecycle.ts +331 -0
  30. package/src/server/stop.ts +124 -0
  31. package/src/storage/index-cache.ts +71 -0
  32. package/src/storage/index-gen.ts +447 -0
  33. package/src/storage/lock.ts +108 -0
  34. package/src/storage/mutate.ts +396 -0
  35. package/src/storage/paths.ts +159 -0
  36. package/src/storage/project-summaries.ts +19 -0
  37. package/src/storage/theme-write.ts +19 -0
  38. package/src/storage/write.ts +75 -0
  39. package/src/tools/ask.ts +353 -0
  40. package/src/tools/critique.ts +66 -0
  41. package/src/tools/publish.ts +404 -0
  42. package/src/tools/stop.ts +53 -0
  43. package/src/tools/styleguide.ts +23 -0
  44. package/src/tools/wait.ts +192 -0
@@ -0,0 +1,97 @@
1
+ # Cesium — beautiful HTML artifacts
2
+
3
+ You have access to six tools:
4
+
5
+ - `cesium_publish` — write a substantive response as a self-contained HTML document
6
+ - `cesium_ask` — publish an interactive Q&A artifact; returns `{ id, httpUrl, ... }`
7
+ - `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_stop` — stop the running cesium HTTP server
11
+
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:
31
+
32
+ - "/cesium", "publish this", "make me an HTML report" → publish
33
+ - "in terminal", "just tell me", "don't make a doc" → don't publish
34
+
35
+ When uncertain: publish AND emit a 2-3 line terminal summary pointing at the doc. Cheap to over-publish, expensive to under-publish.
36
+
37
+ ## How to write the body
38
+
39
+ The `html` argument is body-only (no `<!doctype>`, `<html>`, `<head>`, `<body>` wrappers — the plugin adds them).
40
+
41
+ Use these classes (full reference via `cesium_styleguide`):
42
+
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`
48
+
49
+ Inline `style="..."` and inline `<svg>` are encouraged for bespoke diagrams. NEVER reference external resources (no `<script src>`, no remote fonts, no CDN images).
50
+
51
+ ## Tone
52
+
53
+ Warm, considered, not flashy. Match the aesthetic of a thoughtful design document, not marketing material.
54
+
55
+ ## Self-check before publishing
56
+
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.
62
+
63
+ ## After publishing
64
+
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.
80
+
81
+ ## Interactive Q&A: cesium_ask + cesium_wait
82
+
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
+ 1. `cesium_ask({ title, body, questions: [...] })` → returns `{ id, httpUrl, ... }`
87
+ 2. Print the terminalSummary so the user knows where to click.
88
+ 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.
90
+
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).
95
+
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.
@@ -0,0 +1,316 @@
1
+ // Inline client JS bundle for interactive artifacts.
2
+ // This script is embedded in artifacts with interactive.status === "open".
3
+ // It POSTs answers to /api/sessions/:id/answers/:qid and handles UI state.
4
+ //
5
+ // Server response shape (Phase C will implement):
6
+ // { ok: boolean, status: "open" | "complete" | "expired" | "cancelled", remaining: string[], replacementHtml: string }
7
+
8
+ /** Returns the standalone JS string to embed in interactive artifacts. */
9
+ export function getClientJs(): string {
10
+ return `(function cesiumClient() {
11
+ "use strict";
12
+
13
+ // ─── API base URL derived from window.location.pathname ────────────────────
14
+ // Artifacts are served at /projects/<projectSlug>/artifacts/<filename>.html
15
+ // If not served via cesium HTTP server (e.g. file://), apiBase will be null.
16
+ var m = window.location.pathname.match(/^\\/projects\\/([^\\/]+)\\/artifacts\\/([^\\/]+)$/);
17
+ var apiBase = m ? "/api/sessions/" + m[1] + "/" + m[2] : null;
18
+
19
+ // ─── File:// / offline banner ───────────────────────────────────────────────
20
+ if (!apiBase) {
21
+ document.addEventListener("DOMContentLoaded", function () {
22
+ if (document.querySelector(".cs-banner-offline")) return;
23
+ var banner = document.createElement("div");
24
+ banner.className = "cs-banner cs-banner-offline";
25
+ banner.textContent =
26
+ "Interactive controls require viewing this artifact via the cesium HTTP server. " +
27
+ "Run cesium open or visit localhost:3030";
28
+ document.body.insertBefore(banner, document.body.firstChild);
29
+ });
30
+ }
31
+
32
+ // ─── Session-ended banner ───────────────────────────────────────────────────
33
+ function showSessionEndedBanner() {
34
+ if (document.querySelector(".cs-banner-ended")) return;
35
+ var banner = document.createElement("div");
36
+ banner.className = "cs-banner cs-banner-ended";
37
+ banner.textContent = "Session ended — answers can no longer be submitted.";
38
+ document.body.insertBefore(banner, document.body.firstChild);
39
+ // Disable all interactive controls
40
+ var disabled = document.querySelectorAll(
41
+ ".cs-submit, .cs-skip, .cs-pick, .cs-confirm, .cs-react"
42
+ );
43
+ for (var i = 0; i < disabled.length; i++) {
44
+ var el = disabled[i];
45
+ if (el instanceof HTMLButtonElement || el instanceof HTMLInputElement) {
46
+ el.disabled = true;
47
+ }
48
+ }
49
+ }
50
+
51
+ // ─── POST answer ────────────────────────────────────────────────────────────
52
+ function postAnswer(qid, value) {
53
+ if (!apiBase) {
54
+ return Promise.reject(new Error("not served via cesium HTTP server"));
55
+ }
56
+ return fetch(apiBase + "/answers/" + qid, {
57
+ method: "POST",
58
+ headers: { "Content-Type": "application/json" },
59
+ body: JSON.stringify({ value: value }),
60
+ }).then(function (r) {
61
+ if (r.status === 410) {
62
+ showSessionEndedBanner();
63
+ throw new Error("session ended");
64
+ }
65
+ if (!r.ok) throw new Error("HTTP " + r.status);
66
+ return r.json();
67
+ });
68
+ }
69
+
70
+ // ─── Section replacement ────────────────────────────────────────────────────
71
+ function replaceSection(qid, replacementHtml) {
72
+ var section = document.querySelector(
73
+ "section[data-question-id=\\"" + qid + "\\"]"
74
+ );
75
+ if (!section) return;
76
+ var tmp = document.createElement("div");
77
+ tmp.innerHTML = replacementHtml;
78
+ var newSection = tmp.firstElementChild;
79
+ if (newSection && section.parentNode) {
80
+ section.parentNode.replaceChild(newSection, section);
81
+ }
82
+ }
83
+
84
+ // ─── Pending / error state ──────────────────────────────────────────────────
85
+ function setPending(section, pending) {
86
+ if (pending) {
87
+ section.dataset["pending"] = "true";
88
+ section.classList.add("cs-saving");
89
+ var btns = section.querySelectorAll("button, input");
90
+ for (var i = 0; i < btns.length; i++) {
91
+ var b = btns[i];
92
+ if (b instanceof HTMLButtonElement || b instanceof HTMLInputElement) {
93
+ b.disabled = true;
94
+ }
95
+ }
96
+ } else {
97
+ delete section.dataset["pending"];
98
+ section.classList.remove("cs-saving");
99
+ var btns2 = section.querySelectorAll("button, input");
100
+ for (var i2 = 0; i2 < btns2.length; i2++) {
101
+ var b2 = btns2[i2];
102
+ if (b2 instanceof HTMLButtonElement || b2 instanceof HTMLInputElement) {
103
+ b2.disabled = false;
104
+ }
105
+ }
106
+ }
107
+ }
108
+
109
+ function showError(section, message) {
110
+ var existing = section.querySelector(".cs-error");
111
+ if (existing) existing.remove();
112
+ var errEl = document.createElement("p");
113
+ errEl.className = "cs-error";
114
+ errEl.setAttribute("role", "alert");
115
+ errEl.textContent = "Failed to save: " + message;
116
+ section.appendChild(errEl);
117
+ }
118
+
119
+ function clearError(section) {
120
+ var existing = section.querySelector(".cs-error");
121
+ if (existing) existing.remove();
122
+ }
123
+
124
+ // ─── Submit helper ──────────────────────────────────────────────────────────
125
+ function submitAnswer(section, qid, value) {
126
+ clearError(section);
127
+ setPending(section, true);
128
+ postAnswer(qid, value)
129
+ .then(function (resp) {
130
+ if (resp && resp.replacementHtml) {
131
+ replaceSection(qid, resp.replacementHtml);
132
+ } else {
133
+ setPending(section, false);
134
+ }
135
+ })
136
+ .catch(function (err) {
137
+ setPending(section, false);
138
+ showError(section, err instanceof Error ? err.message : String(err));
139
+ });
140
+ }
141
+
142
+ // ─── pick_one wiring ─────────────────────────────────────────────────────────
143
+ function wirePickOne(section) {
144
+ var qid = section.dataset["questionId"] || "";
145
+ var btns = section.querySelectorAll("button.cs-pick");
146
+ for (var i = 0; i < btns.length; i++) {
147
+ (function (btn) {
148
+ btn.addEventListener("click", function () {
149
+ var dataValue = btn.dataset["value"] || "";
150
+ submitAnswer(section, qid, { type: "pick_one", selected: dataValue });
151
+ });
152
+ })(btns[i]);
153
+ }
154
+ }
155
+
156
+ // ─── pick_many wiring ────────────────────────────────────────────────────────
157
+ function wirePickMany(section) {
158
+ var qid = section.dataset["questionId"] || "";
159
+ var submitBtn = section.querySelector("button.cs-submit");
160
+ var minAttr = section.dataset["min"];
161
+ var maxAttr = section.dataset["max"];
162
+ var min = minAttr ? parseInt(minAttr, 10) : 1;
163
+ var max = maxAttr ? parseInt(maxAttr, 10) : Infinity;
164
+
165
+ function updateSubmitState() {
166
+ var checked = section.querySelectorAll("input[type=checkbox]:checked");
167
+ var count = checked.length;
168
+ if (submitBtn instanceof HTMLButtonElement) {
169
+ submitBtn.disabled = count < min || count > max;
170
+ }
171
+ }
172
+
173
+ var checkboxes = section.querySelectorAll("input[type=checkbox]");
174
+ for (var i = 0; i < checkboxes.length; i++) {
175
+ checkboxes[i].addEventListener("change", updateSubmitState);
176
+ }
177
+
178
+ if (submitBtn) {
179
+ submitBtn.addEventListener("click", function () {
180
+ var checked = section.querySelectorAll("input[type=checkbox]:checked");
181
+ var selected = [];
182
+ for (var i2 = 0; i2 < checked.length; i2++) {
183
+ var cb = checked[i2];
184
+ if (cb instanceof HTMLInputElement) {
185
+ selected.push(cb.dataset["value"] || "");
186
+ }
187
+ }
188
+ submitAnswer(section, qid, { type: "pick_many", selected: selected });
189
+ });
190
+ }
191
+ }
192
+
193
+ // ─── confirm wiring ──────────────────────────────────────────────────────────
194
+ function wireConfirm(section) {
195
+ var qid = section.dataset["questionId"] || "";
196
+ var btns = section.querySelectorAll("button.cs-confirm");
197
+ for (var i = 0; i < btns.length; i++) {
198
+ (function (btn) {
199
+ btn.addEventListener("click", function () {
200
+ var choice = btn.dataset["value"] || "";
201
+ submitAnswer(section, qid, { type: "confirm", choice: choice });
202
+ });
203
+ })(btns[i]);
204
+ }
205
+ }
206
+
207
+ // ─── ask_text wiring ─────────────────────────────────────────────────────────
208
+ function wireAskText(section) {
209
+ var qid = section.dataset["questionId"] || "";
210
+ var input = section.querySelector("input.cs-text, textarea.cs-text");
211
+ var submitBtn = section.querySelector("button.cs-submit");
212
+ var skipBtn = section.querySelector("button.cs-skip");
213
+
214
+ function updateSubmitState() {
215
+ if (submitBtn instanceof HTMLButtonElement && input) {
216
+ var val = input instanceof HTMLInputElement
217
+ ? input.value
218
+ : input instanceof HTMLTextAreaElement
219
+ ? input.value
220
+ : "";
221
+ submitBtn.disabled = val.trim() === "";
222
+ }
223
+ }
224
+
225
+ if (input) {
226
+ input.addEventListener("input", updateSubmitState);
227
+ }
228
+
229
+ if (submitBtn) {
230
+ submitBtn.addEventListener("click", function () {
231
+ var text = "";
232
+ if (input instanceof HTMLInputElement || input instanceof HTMLTextAreaElement) {
233
+ text = input.value;
234
+ }
235
+ submitAnswer(section, qid, { type: "ask_text", text: text });
236
+ });
237
+ }
238
+
239
+ if (skipBtn) {
240
+ skipBtn.addEventListener("click", function () {
241
+ submitAnswer(section, qid, { type: "ask_text", text: "" });
242
+ });
243
+ }
244
+ }
245
+
246
+ // ─── slider wiring ───────────────────────────────────────────────────────────
247
+ function wireSlider(section) {
248
+ var qid = section.dataset["questionId"] || "";
249
+ var sliderInput = section.querySelector("input.cs-slider");
250
+ var outputEl = section.querySelector("output.cs-slider-out");
251
+ var submitBtn = section.querySelector("button.cs-submit");
252
+
253
+ if (sliderInput) {
254
+ sliderInput.addEventListener("input", function () {
255
+ if (outputEl && sliderInput instanceof HTMLInputElement) {
256
+ outputEl.textContent = sliderInput.value;
257
+ }
258
+ });
259
+ }
260
+
261
+ if (submitBtn) {
262
+ submitBtn.addEventListener("click", function () {
263
+ var value = sliderInput instanceof HTMLInputElement
264
+ ? Number(sliderInput.value)
265
+ : 0;
266
+ submitAnswer(section, qid, { type: "slider", value: value });
267
+ });
268
+ }
269
+ }
270
+
271
+ // ─── react wiring ────────────────────────────────────────────────────────────
272
+ function wireReact(section) {
273
+ var qid = section.dataset["questionId"] || "";
274
+ var btns = section.querySelectorAll("button.cs-react");
275
+ var commentArea = section.querySelector("textarea.cs-react-comment");
276
+
277
+ for (var i = 0; i < btns.length; i++) {
278
+ (function (btn) {
279
+ btn.addEventListener("click", function () {
280
+ var decision = btn.dataset["value"] || "";
281
+ var comment = commentArea instanceof HTMLTextAreaElement && commentArea.value !== ""
282
+ ? commentArea.value
283
+ : undefined;
284
+ var value = comment !== undefined
285
+ ? { type: "react", decision: decision, comment: comment }
286
+ : { type: "react", decision: decision };
287
+ submitAnswer(section, qid, value);
288
+ });
289
+ })(btns[i]);
290
+ }
291
+ }
292
+
293
+ // ─── Wire all sections on DOMContentLoaded ───────────────────────────────────
294
+ document.addEventListener("DOMContentLoaded", function () {
295
+ var sections = document.querySelectorAll("section[data-question-id]");
296
+ for (var i = 0; i < sections.length; i++) {
297
+ var section = sections[i];
298
+ if (!(section instanceof HTMLElement)) continue;
299
+ var cls = section.className || "";
300
+ if (cls.indexOf("cs-control-pick_one") !== -1) {
301
+ wirePickOne(section);
302
+ } else if (cls.indexOf("cs-control-pick_many") !== -1) {
303
+ wirePickMany(section);
304
+ } else if (cls.indexOf("cs-control-confirm") !== -1) {
305
+ wireConfirm(section);
306
+ } else if (cls.indexOf("cs-control-ask_text") !== -1) {
307
+ wireAskText(section);
308
+ } else if (cls.indexOf("cs-control-slider") !== -1) {
309
+ wireSlider(section);
310
+ } else if (cls.indexOf("cs-control-react") !== -1) {
311
+ wireReact(section);
312
+ }
313
+ }
314
+ });
315
+ })();`;
316
+ }