@mulmoclaude/core 0.1.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 (122) hide show
  1. package/assets/helps/billing-clients-worklog.md +215 -0
  2. package/assets/helps/billing-invoice.md +458 -0
  3. package/assets/helps/business.md +104 -0
  4. package/assets/helps/collection-skills.md +810 -0
  5. package/assets/helps/custom-view.md +433 -0
  6. package/assets/helps/feeds.md +114 -0
  7. package/assets/helps/gemini.md +57 -0
  8. package/assets/helps/github.md +23 -0
  9. package/assets/helps/guide.md +61 -0
  10. package/assets/helps/index.md +89 -0
  11. package/assets/helps/lessons-collection.md +400 -0
  12. package/assets/helps/mulmoscript.md +249 -0
  13. package/assets/helps/portfolio-tracker.md +211 -0
  14. package/assets/helps/presentation-deck.md +828 -0
  15. package/assets/helps/presenthtml.md +89 -0
  16. package/assets/helps/sandbox.md +97 -0
  17. package/assets/helps/spreadsheet.md +43 -0
  18. package/assets/helps/storyteller.md +101 -0
  19. package/assets/helps/telegram.md +136 -0
  20. package/assets/helps/todo-collection.md +140 -0
  21. package/assets/helps/vocabulary.md +109 -0
  22. package/assets/helps/wiki.md +168 -0
  23. package/assets/skills-preset/mc-cooking-coach/SKILL.md +217 -0
  24. package/assets/skills-preset/mc-library/SKILL.md +188 -0
  25. package/assets/skills-preset/mc-manage-automations/SKILL.md +119 -0
  26. package/assets/skills-preset/mc-manage-skills/SKILL.md +141 -0
  27. package/assets/skills-preset/mc-wiki-deep-lint/SKILL.md +108 -0
  28. package/assets/skills-preset/mc-wiki-health-check/SKILL.md +61 -0
  29. package/assets/skills-preset/mc-wiki-ingest/SKILL.md +182 -0
  30. package/assets/skills-preset/mc-wiki-promote/SKILL.md +175 -0
  31. package/assets/skills-preset/mc-zenn/SKILL.md +136 -0
  32. package/dist/chunk-CKQMccvm.cjs +28 -0
  33. package/dist/collection/core/actionVisible.d.ts +34 -0
  34. package/dist/collection/core/calendarGrid.d.ts +120 -0
  35. package/dist/collection/core/deriveAll.d.ts +38 -0
  36. package/dist/collection/core/derivedFormula.d.ts +18 -0
  37. package/dist/collection/core/draft.d.ts +18 -0
  38. package/dist/collection/core/enumColors.d.ts +33 -0
  39. package/dist/collection/core/errorMessage.d.ts +4 -0
  40. package/dist/collection/core/itemLabel.d.ts +12 -0
  41. package/dist/collection/core/presentCollection.d.ts +13 -0
  42. package/dist/collection/core/promptSafety.d.ts +1 -0
  43. package/dist/collection/core/schema.d.ts +355 -0
  44. package/dist/collection/core/shortHexId.d.ts +8 -0
  45. package/dist/collection/core/sortItems.d.ts +29 -0
  46. package/dist/collection/core/uiTypes.d.ts +106 -0
  47. package/dist/collection/index.cjs +793 -0
  48. package/dist/collection/index.cjs.map +1 -0
  49. package/dist/collection/index.d.ts +14 -0
  50. package/dist/collection/index.js +740 -0
  51. package/dist/collection/index.js.map +1 -0
  52. package/dist/collection/paths.cjs +44 -0
  53. package/dist/collection/paths.cjs.map +1 -0
  54. package/dist/collection/paths.js +41 -0
  55. package/dist/collection/paths.js.map +1 -0
  56. package/dist/collection/server/atomic.d.ts +1 -0
  57. package/dist/collection/server/delete.d.ts +38 -0
  58. package/dist/collection/server/derive.d.ts +8 -0
  59. package/dist/collection/server/discoveredCollection.d.ts +18 -0
  60. package/dist/collection/server/discovery.d.ts +227 -0
  61. package/dist/collection/server/host.d.ts +77 -0
  62. package/dist/collection/server/index.cjs +1721 -0
  63. package/dist/collection/server/index.cjs.map +1 -0
  64. package/dist/collection/server/index.d.ts +11 -0
  65. package/dist/collection/server/index.js +1671 -0
  66. package/dist/collection/server/index.js.map +1 -0
  67. package/dist/collection/server/io.d.ts +114 -0
  68. package/dist/collection/server/paths.d.ts +52 -0
  69. package/dist/collection/server/spawn.d.ts +55 -0
  70. package/dist/collection/server/templatePath.d.ts +25 -0
  71. package/dist/collection/server/util.d.ts +3 -0
  72. package/dist/collection/server/validate.d.ts +19 -0
  73. package/dist/collection/server/views.d.ts +20 -0
  74. package/dist/deriveAll-C15OpM3K.cjs +399 -0
  75. package/dist/deriveAll-C15OpM3K.cjs.map +1 -0
  76. package/dist/deriveAll-C6BYnpBL.js +364 -0
  77. package/dist/deriveAll-C6BYnpBL.js.map +1 -0
  78. package/dist/file-change/index.cjs +72 -0
  79. package/dist/file-change/index.cjs.map +1 -0
  80. package/dist/file-change/index.d.ts +43 -0
  81. package/dist/file-change/index.js +66 -0
  82. package/dist/file-change/index.js.map +1 -0
  83. package/dist/notifier/engine.d.ts +72 -0
  84. package/dist/notifier/index.cjs +484 -0
  85. package/dist/notifier/index.cjs.map +1 -0
  86. package/dist/notifier/index.d.ts +3 -0
  87. package/dist/notifier/index.js +464 -0
  88. package/dist/notifier/index.js.map +1 -0
  89. package/dist/notifier/store.d.ts +18 -0
  90. package/dist/notifier/types.d.ts +118 -0
  91. package/dist/notifier/validate.d.ts +17 -0
  92. package/dist/scheduler/adapter.d.ts +48 -0
  93. package/dist/scheduler/index.cjs +352 -0
  94. package/dist/scheduler/index.cjs.map +1 -0
  95. package/dist/scheduler/index.d.ts +2 -0
  96. package/dist/scheduler/index.js +343 -0
  97. package/dist/scheduler/index.js.map +1 -0
  98. package/dist/scheduler/task-manager.d.ts +51 -0
  99. package/dist/whisper/client.cjs +241 -0
  100. package/dist/whisper/client.cjs.map +1 -0
  101. package/dist/whisper/client.d.ts +35 -0
  102. package/dist/whisper/client.js +239 -0
  103. package/dist/whisper/client.js.map +1 -0
  104. package/dist/whisper/ffmpeg.d.ts +6 -0
  105. package/dist/whisper/index.cjs +433 -0
  106. package/dist/whisper/index.cjs.map +1 -0
  107. package/dist/whisper/index.d.ts +5 -0
  108. package/dist/whisper/index.js +425 -0
  109. package/dist/whisper/index.js.map +1 -0
  110. package/dist/whisper/internal.d.ts +11 -0
  111. package/dist/whisper/models.d.ts +49 -0
  112. package/dist/whisper/sidecar.d.ts +8 -0
  113. package/dist/whisper/whisper.d.ts +28 -0
  114. package/dist/workspace-setup/assets.d.ts +10 -0
  115. package/dist/workspace-setup/index.d.ts +3 -0
  116. package/dist/workspace-setup/index.js +556 -0
  117. package/dist/workspace-setup/index.js.map +1 -0
  118. package/dist/workspace-setup/slug.d.ts +6 -0
  119. package/dist/workspace-setup/slug.js +13 -0
  120. package/dist/workspace-setup/slug.js.map +1 -0
  121. package/dist/workspace-setup/sync.d.ts +94 -0
  122. package/package.json +95 -0
@@ -0,0 +1,433 @@
1
+ # Custom collection views (LLM-authored HTML)
2
+
3
+ A **custom view** is an HTML file you write that renders a collection's records
4
+ however the user wants — a year-at-a-glance planner, a Gantt bar, a heat-map,
5
+ a printable report. The host renders it in a sandboxed iframe over the
6
+ collection's data. The user asks for it in plain language ("give me a view that
7
+ shows my whole year"); you author the HTML and register it.
8
+
9
+ Read `config/helps/collection-skills.md` first for the collection/schema DSL.
10
+ This file is only about the **view** layer.
11
+
12
+ ## Where the files go
13
+
14
+ ```text
15
+ data/skills/<slug>/
16
+ schema.json ← register the view here, under `views[]`
17
+ views/
18
+ <name>.html ← the view you author (Write/Edit)
19
+ ```
20
+
21
+ **Feed collections** keep their skill files under `feeds/<slug>/` instead of
22
+ `data/skills/<slug>/`, so author a feed's view at `feeds/<slug>/views/<name>.html`
23
+ and register it in `feeds/<slug>/schema.json`. Everything else below — the
24
+ `views[]` entry shape, the runtime contract, the sandbox rules — is identical.
25
+
26
+ The HTML lives under `views/` and must end in `.html`. Register each view in
27
+ the collection's `schema.json`:
28
+
29
+ ```jsonc
30
+ {
31
+ "title": "Annual Plan",
32
+ "icon": "calendar_month",
33
+ "dataPath": "data/annual-plan/items",
34
+ "primaryKey": "id",
35
+ "fields": {
36
+ /* … */
37
+ },
38
+ "views": [
39
+ { "id": "year", "label": "Year", "icon": "grid_view", "file": "views/year.html", "capabilities": ["read"] },
40
+ { "id": "planner", "label": "Planner", "icon": "edit_calendar", "file": "views/planner.html", "capabilities": ["read", "write"] },
41
+ ],
42
+ }
43
+ ```
44
+
45
+ - **`id`** — a slug (letters/digits/`-`/`_`). The selector shows one button per view.
46
+ - **`label`** — the button text (author it; it is not run through translation).
47
+ - **`icon`** — optional Material Symbols icon name.
48
+ - **`file`** — `views/<name>.html`, path-safe.
49
+ - **`capabilities`** — least privilege. `["read"]` (default) for a view that
50
+ only displays data; `["read","write"]` only if the view edits records.
51
+
52
+ After you Write the HTML and register it, the view's button appears in the
53
+ collection's view-mode selector automatically.
54
+
55
+ ## The runtime contract — `window.__MC_VIEW`
56
+
57
+ The host injects a bootstrap into your page **before any of your scripts run**:
58
+
59
+ ```js
60
+ window.__MC_VIEW = {
61
+ slug: "annual-plan", // this collection
62
+ token: "<scoped capability token>", // Authorization bearer
63
+ dataUrl: "http://localhost:3001/api/collections/annual-plan/view-data",
64
+ onChange: (cb) => unsubscribe, // live refresh — see "Staying live" below
65
+ openItem: (id, mode) => void, // open a record in the host's panel — see "Opening a record"
66
+ startChat: (prompt, role) => void, // draft a new chat for the user — see "Starting a chat"
67
+ };
68
+ ```
69
+
70
+ ### Reading records
71
+
72
+ ```js
73
+ const { token, dataUrl } = window.__MC_VIEW;
74
+ const res = await fetch(dataUrl, { headers: { Authorization: "Bearer " + token } });
75
+ if (!res.ok) throw new Error("load failed: " + res.status);
76
+ const { items } = await res.json(); // { collection, count, items: [...] }
77
+ ```
78
+
79
+ Records come back **with computed fields already resolved** (derived formulas,
80
+ toggles, embeds) — the same numbers the user sees elsewhere.
81
+
82
+ - Narrow large reads: `dataUrl + "?fields=title,start,end"` or `?ids=a,b,c`
83
+ (comma-separated). A read of **more than 200 records** without `ids`/`fields`
84
+ is refused — always project `fields` for big collections.
85
+
86
+ ### Writing records (only with the `write` capability)
87
+
88
+ ```js
89
+ const res = await fetch(dataUrl, {
90
+ method: "PUT",
91
+ headers: { Authorization: "Bearer " + token, "Content-Type": "application/json" },
92
+ body: JSON.stringify({ items: [{ id: "task-3", start: "2026-07-01" }], mode: "merge" }),
93
+ });
94
+ const { written, rejected } = await res.json(); // fix & re-send any rejected rows
95
+ ```
96
+
97
+ - `mode`: `"merge"` (update only the fields you send — use this for edits),
98
+ `"upsert"` (replace the whole record), `"create"` (fail if the id exists).
99
+ - Each row must carry the collection's `primaryKey`.
100
+ - **Never** send computed fields (`derived` / `toggle` / `embed`) — they are
101
+ rejected. Write the underlying enum for a toggle.
102
+ - **There is no delete** from a view. A view can never do more than the agent's
103
+ own data tools.
104
+
105
+ Surface any `rejected` rows to the user with their `problem` text — don't fail
106
+ silently.
107
+
108
+ ### Staying live — `onChange`
109
+
110
+ By default a view paints once, on load. To keep it fresh, register a callback —
111
+ it runs whenever the collection's data changes on the server:
112
+
113
+ ```js
114
+ async function render() {
115
+ const res = await fetch(dataUrl, { headers: { Authorization: "Bearer " + token } });
116
+ if (!res.ok) return; // handle the error per your UI
117
+ const { items } = await res.json();
118
+ // …draw items…
119
+ }
120
+ render(); // initial paint
121
+ window.__MC_VIEW.onChange(render); // live refresh on every server-side change
122
+ ```
123
+
124
+ What you need to know:
125
+
126
+ - **It fires for every writer** — the assistant adding/editing a record in chat,
127
+ an edit from another browser tab, a feed refresh, and auto-generated recurring
128
+ records. Your view stays in sync no matter who changed the data.
129
+ - **Make the callback a full re-fetch + re-render** (like `render` above). It is
130
+ a "something changed, reload" signal — it carries no record data — and one
131
+ callback may stand in for several rapid changes.
132
+ - **It is already debounced** (a burst of changes collapses to one call) — do
133
+ **not** add your own throttle.
134
+ - **No extra capability needed** — a read-only view can use `onChange`.
135
+ - It returns an **unsubscribe** function; you rarely need it (the view is torn
136
+ down with the iframe), but it's there for fine-grained control.
137
+
138
+ ### Opening a record — `openItem`
139
+
140
+ Your view owns the _layout_ (a grid, a chart, a board); it doesn't have to
141
+ rebuild a form to view or edit one record. Call `openItem` to hand a record to
142
+ the host's own panel — the same detail/edit modal the user gets clicking a row
143
+ in the table view — centred over your view:
144
+
145
+ ```js
146
+ window.__MC_VIEW.openItem("task-3"); // read-only detail (default)
147
+ window.__MC_VIEW.openItem("task-3", "edit"); // jump straight into the editor
148
+ ```
149
+
150
+ - **`id`** — the record's `primaryKey` value. If it isn't a loaded record,
151
+ nothing happens (fire-and-forget; returns nothing).
152
+ - **`mode`** — `"view"` (default) opens read-only detail with the panel's own
153
+ Edit button; `"edit"` opens the editor directly.
154
+ - **No `write` capability required — even for `"edit"`.** Opening the host's
155
+ panel is a _user_ action in the host's trusted UI: the user still has to press
156
+ Save, and the write goes through the host, not your scoped token. So a
157
+ `["read"]` view can offer a full "edit this record" affordance without
158
+ widening its own capabilities. (Capabilities gate what your view's _code_ may
159
+ do to the data; they don't restrict what the user may do through the host.)
160
+ - **Pair it with `onChange`.** After the user saves in the panel, your
161
+ `onChange` callback fires (the data changed), so a live view repaints itself —
162
+ no extra wiring needed.
163
+
164
+ This is the right tool whenever a record's full detail is richer than your
165
+ view's summary, or whenever the user wants to edit but you don't want a
166
+ `write`-capable view doing its own PUTs.
167
+
168
+ ### Starting a chat — `startChat`
169
+
170
+ Your view can't reach external services or run a skill on its own (the sandbox
171
+ blocks it). Instead, hand the work to a chat: `startChat` opens a **new chat
172
+ session with your prompt prefilled in the composer** — as an editable draft. It
173
+ does **not** send. The user reads it, edits if they want, and presses Send (or
174
+ clears it). The agent in that approved chat does the real work — file a GitHub
175
+ issue and write the URL back, fetch a link's title/image and save them, or just
176
+ start from a task record.
177
+
178
+ ```js
179
+ const task = items.find((r) => r.id === id);
180
+ window.__MC_VIEW.startChat(`Create a GitHub issue for this task and write the URL back to record ${id}:\n\n` + `Title: ${task.title}\n${task.body}`);
181
+ ```
182
+
183
+ - **`prompt`** — the seed text. Empty / whitespace-only is ignored (no empty
184
+ chat). Build it from your records — that's the whole point.
185
+ - **`role`** _(optional second argument)_ — a built-in role id to preselect for
186
+ the new session (e.g. `"office"`, `"investor"`); validated by the host. Omit it
187
+ and the chat opens in **General** — which is what you usually want.
188
+ - **No capability required.** Your view's code only _proposes text into an input
189
+ field_ — nothing is created, fetched, or written until the **user** presses
190
+ Send, at which point it's an ordinary agent run they authored. So a `["read"]`
191
+ view can offer "start work on this" buttons freely.
192
+ - **Pair it with `onChange`.** When the chat's agent later writes back to a
193
+ record, your `onChange` callback fires and a live view repaints.
194
+
195
+ Use this — not a hidden flag the user has to reconcile later — whenever a button
196
+ should _start backend work_: the user stays in the loop through trusted first-
197
+ party UI, and the action runs as a normal, visible, cancellable agent turn.
198
+
199
+ ## Sandbox rules (what the view may and may not do)
200
+
201
+ The view runs in a `sandbox="allow-scripts"` iframe with a strict CSP:
202
+
203
+ - **Inline `<script>` and `<style>` only.** External scripts/styles/fonts must
204
+ come from the allowed CDNs: `cdn.jsdelivr.net`, `unpkg.com`,
205
+ `cdnjs.cloudflare.com`, `fonts.googleapis.com`, `fonts.gstatic.com`,
206
+ `cdn.plot.ly` — so charting libraries (Chart.js, Plotly, D3) load fine from
207
+ those CDNs. No other external hosts.
208
+ - **`<img>` may load from any `https:` host** (plus `data:` / `blob:`), so an
209
+ image URL stored in a record — a feed's article thumbnail, a poster, an
210
+ avatar — renders directly. (Images are the one resource type not pinned to the
211
+ CDN allowlist; everything else above still is.)
212
+ - **`<audio>` / `<video>` may load from any `https:` host** (plus the origin and
213
+ `data:` / `blob:`), so a record's media URL — a podcast feed's `.mp3`, a
214
+ video enclosure — plays directly.
215
+ - **`fetch` (and XHR / WebSocket / `sendBeacon`) is allowed ONLY to
216
+ `window.__MC_VIEW.dataUrl`'s origin.** All other origins are blocked — no
217
+ phone-home, no third-party analytics, no fetching weather / prices / etc.
218
+ directly from the view. If the user needs external data, put it in a (feed)
219
+ collection and read it through `dataUrl`. (This is the channel that actually
220
+ matters for keeping the scoped token and records from leaking off-box.) When a
221
+ button should _do_ outside work — file an issue, fetch a link's metadata, run a
222
+ skill — don't try to reach out from view code; use **`startChat`** (above) to
223
+ hand the task to a user-approved chat.
224
+ - No access to cookies, `localStorage`, or the parent page — the iframe has an
225
+ opaque origin. The token is the only credential, and it is scoped to this one
226
+ collection.
227
+ - **Opening external links is allowed** — use `<a href="…" target="_blank"
228
+ rel="noopener">` (or `window.open(url, "_blank")`) to open a record's URL in a
229
+ new browser tab, e.g. a feed card linking to its article. The link opens as a
230
+ normal tab. (A plain same-tab `<a href>` would try to navigate the sandboxed
231
+ frame itself and is blocked, so always use `target="_blank"` for outbound
232
+ links.)
233
+ - Build a full HTML document with a `<head>` (the host injects its bootstrap at
234
+ the start of `<head>`).
235
+
236
+ ## Editing / iterating
237
+
238
+ To change a view later, just Read and Edit its `views/<name>.html` file under the
239
+ collection's skill dir (`data/skills/<slug>/` — or `feeds/<slug>/` for a feed);
240
+ its path is in the schema's `views[]`. To remove one, delete the file and its
241
+ `views[]` entry — or use the collection's settings gear (the per-collection
242
+ config modal) in the UI, which does both for you.
243
+
244
+ ---
245
+
246
+ ## Example 1 — Year overview (read-only)
247
+
248
+ A 12-month grid that plots each record on its start month. Schema has `date`
249
+ fields `start` / `end` and a `title`. `capabilities: ["read"]`.
250
+
251
+ `data/skills/annual-plan/views/year.html`:
252
+
253
+ ```html
254
+ <!doctype html>
255
+ <html>
256
+ <head>
257
+ <meta charset="utf-8" />
258
+ <style>
259
+ body {
260
+ font-family: system-ui, sans-serif;
261
+ margin: 0;
262
+ padding: 16px;
263
+ color: #1e293b;
264
+ }
265
+ h1 {
266
+ font-size: 16px;
267
+ margin: 0 0 12px;
268
+ }
269
+ .grid {
270
+ display: grid;
271
+ grid-template-columns: repeat(4, 1fr);
272
+ gap: 8px;
273
+ }
274
+ .month {
275
+ border: 1px solid #e2e8f0;
276
+ border-radius: 8px;
277
+ padding: 8px;
278
+ min-height: 90px;
279
+ }
280
+ .month h2 {
281
+ font-size: 12px;
282
+ color: #64748b;
283
+ margin: 0 0 6px;
284
+ }
285
+ .chip {
286
+ font-size: 12px;
287
+ background: #eef2ff;
288
+ color: #3730a3;
289
+ border-radius: 4px;
290
+ padding: 2px 6px;
291
+ margin-bottom: 4px;
292
+ cursor: pointer;
293
+ }
294
+ .err {
295
+ color: #b91c1c;
296
+ }
297
+ </style>
298
+ </head>
299
+ <body>
300
+ <h1>Year overview</h1>
301
+ <div id="root">Loading…</div>
302
+ <script>
303
+ const MONTHS = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"];
304
+ async function main() {
305
+ const { token, dataUrl } = window.__MC_VIEW;
306
+ const res = await fetch(dataUrl, { headers: { Authorization: "Bearer " + token } });
307
+ if (!res.ok) throw new Error("HTTP " + res.status);
308
+ const { items } = await res.json();
309
+ const buckets = MONTHS.map(() => []);
310
+ for (const it of items) {
311
+ const m = it.start ? new Date(it.start).getMonth() : -1;
312
+ if (m >= 0 && m < 12) buckets[m].push(it); // keep the item, not just its title — we need the id
313
+ }
314
+ const root = document.getElementById("root");
315
+ root.className = "grid";
316
+ root.innerHTML = MONTHS.map(
317
+ (name, i) =>
318
+ '<div class="month"><h2>' +
319
+ name +
320
+ "</h2>" +
321
+ buckets[i].map((it) => '<div class="chip" data-id="' + it.id + '">' + (it.title || it.id) + "</div>").join("") +
322
+ "</div>",
323
+ ).join("");
324
+ // Click a chip → open that record in the host's panel (read-only detail;
325
+ // the panel's own Edit button takes it from there). Pass "edit" to jump
326
+ // straight into the editor — no `write` capability needed.
327
+ root.onclick = (e) => {
328
+ const chip = e.target.closest(".chip");
329
+ if (chip) window.__MC_VIEW.openItem(chip.dataset.id);
330
+ };
331
+ }
332
+ main().catch((e) => {
333
+ document.getElementById("root").innerHTML = '<span class="err">Could not load: ' + e.message + "</span>";
334
+ });
335
+ // Live refresh: re-run whenever the collection's data changes server-side.
336
+ window.__MC_VIEW.onChange(() => main().catch(() => {}));
337
+ </script>
338
+ </body>
339
+ </html>
340
+ ```
341
+
342
+ ## Example 2 — Weekly planner (read + write)
343
+
344
+ Seven day columns; clicking a record bumps its `start` date forward a day and
345
+ writes it back. `capabilities: ["read","write"]`.
346
+
347
+ ```html
348
+ <!doctype html>
349
+ <html>
350
+ <head>
351
+ <meta charset="utf-8" />
352
+ <style>
353
+ body {
354
+ font-family: system-ui, sans-serif;
355
+ margin: 0;
356
+ padding: 16px;
357
+ }
358
+ .row {
359
+ display: grid;
360
+ grid-template-columns: repeat(7, 1fr);
361
+ gap: 6px;
362
+ }
363
+ .day {
364
+ border: 1px solid #e2e8f0;
365
+ border-radius: 8px;
366
+ padding: 6px;
367
+ min-height: 80px;
368
+ }
369
+ .task {
370
+ font-size: 12px;
371
+ background: #f1f5f9;
372
+ border-radius: 4px;
373
+ padding: 3px 6px;
374
+ margin-bottom: 4px;
375
+ cursor: pointer;
376
+ }
377
+ </style>
378
+ </head>
379
+ <body>
380
+ <div id="root">Loading…</div>
381
+ <script>
382
+ const DAYS = ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"];
383
+ const { token, dataUrl } = window.__MC_VIEW;
384
+ const auth = { Authorization: "Bearer " + token, "Content-Type": "application/json" };
385
+
386
+ async function read() {
387
+ const res = await fetch(dataUrl, { headers: { Authorization: "Bearer " + token } });
388
+ if (!res.ok) throw new Error("HTTP " + res.status);
389
+ return (await res.json()).items;
390
+ }
391
+ async function bump(item) {
392
+ const next = new Date(item.start || Date.now());
393
+ next.setDate(next.getDate() + 1);
394
+ const res = await fetch(dataUrl, {
395
+ method: "PUT",
396
+ headers: auth,
397
+ body: JSON.stringify({ items: [{ id: item.id, start: next.toISOString().slice(0, 10) }], mode: "merge" }),
398
+ });
399
+ const { rejected } = await res.json();
400
+ if (rejected && rejected.length) alert(rejected[0].problem);
401
+ render();
402
+ }
403
+ async function render() {
404
+ const items = await read();
405
+ const cols = DAYS.map(() => []);
406
+ for (const it of items) {
407
+ const d = it.start ? (new Date(it.start).getDay() + 6) % 7 : 0;
408
+ cols[d].push(it);
409
+ }
410
+ const root = document.getElementById("root");
411
+ root.className = "row";
412
+ root.innerHTML = DAYS.map((d, i) => '<div class="day" data-i="' + i + '"><b>' + d + "</b></div>").join("");
413
+ DAYS.forEach((_, i) => {
414
+ const cell = root.querySelector('[data-i="' + i + '"]');
415
+ for (const it of cols[i]) {
416
+ const el = document.createElement("div");
417
+ el.className = "task";
418
+ el.textContent = it.title || it.id;
419
+ el.onclick = () => bump(it);
420
+ cell.appendChild(el);
421
+ }
422
+ });
423
+ }
424
+ render().catch((e) => {
425
+ document.getElementById("root").textContent = "Could not load: " + e.message;
426
+ });
427
+ // Live refresh: re-render on any server-side change (incl. our own writes
428
+ // landing, and edits from the assistant or another tab).
429
+ window.__MC_VIEW.onChange(() => render().catch(() => {}));
430
+ </script>
431
+ </body>
432
+ </html>
433
+ ```
@@ -0,0 +1,114 @@
1
+ # Feeds — pull internet data into a self-refreshing collection
2
+
3
+ A **feed** is a data source you register once; the host then fetches it on a
4
+ schedule and stores each item as a record. A feed is a `CollectionSchema` plus
5
+ an `ingest` block, written as a single file:
6
+
7
+ ```
8
+ feeds/<slug>/schema.json ← YOU write this (Write)
9
+ feeds/<slug>/_state.json ← the host writes this (fetch cursor/state)
10
+ data/feeds/<slug>/<id>.json ← the host writes these (one per item)
11
+ ```
12
+
13
+ It is NOT a skill (no `SKILL.md`, nothing under `.claude/skills/`), so it never
14
+ enters the prompt. You don't call any tool to fetch — the host's retrieval
15
+ engine does that automatically: the first time the feed's view is opened, on an
16
+ hourly schedule thereafter, and when the user clicks **Refresh feed**. Records
17
+ render in the standard collection view at `/feeds/<slug>`.
18
+
19
+ This is the project philosophy: *the workspace is the database; you are the
20
+ intelligent interface.* Adding a feed = **fetch the URL, look at its real
21
+ fields, and write one `schema.json`.**
22
+
23
+ ## Workflow to add a feed
24
+
25
+ 1. **Fetch and inspect the URL yourself** (a web/fetch tool, or `curl`). Look at
26
+ the actual structure — the item tags/fields it carries. Do NOT guess or ask
27
+ the user design questions; infer everything from the data.
28
+ 2. **Write `feeds/<slug>/schema.json`** (see the shape below). Pick a short
29
+ `slug` (lowercase letters/digits/hyphens).
30
+ 3. Tell the user the feed is registered and that opening `/feeds/<slug>` will
31
+ load its items (the host fetches automatically on first open). Do NOT tell
32
+ them to click Refresh — that's not needed for a new feed.
33
+
34
+ To **remove** a feed completely, delete BOTH its `feeds/<slug>/` directory
35
+ (schema + state) and its records under `data/feeds/<slug>/`. The `/feeds` page
36
+ lists all registered feeds, and its delete button does the same.
37
+
38
+ ## Schema shape (STRICT — follow exactly)
39
+
40
+ ```json
41
+ {
42
+ "title": "Example Feed",
43
+ "icon": "dynamic_feed",
44
+ "dataPath": "data/feeds/<slug>",
45
+ "primaryKey": "id",
46
+ "displayField": "headline",
47
+ "fields": {
48
+ "id": { "type": "string", "label": "ID", "primary": true },
49
+ "headline": { "type": "string", "label": "Headline" },
50
+ "url": { "type": "string", "label": "URL" },
51
+ "published": { "type": "date", "label": "Published" },
52
+ "summary": { "type": "markdown", "label": "Summary" }
53
+ },
54
+ "ingest": {
55
+ "kind": "rss",
56
+ "url": "https://example.com/feed.xml",
57
+ "schedule": "hourly",
58
+ "idFrom": "guid",
59
+ "map": { "id": "guid", "headline": "title", "url": "link", "published": "pubDate", "summary": "description" }
60
+ }
61
+ }
62
+ ```
63
+
64
+ - `title` (required), `icon` (required — a Material Symbols name; `dynamic_feed` is
65
+ a good default). `dataPath` is set by the host to `data/feeds/<slug>` — include
66
+ it (must equal that) or omit it; any other value is ignored. A feed can only
67
+ ever store records under its own `data/feeds/<slug>` folder.
68
+ - `primaryKey` (required): names the id field. That field must set
69
+ `primary: true`. The host derives a safe, stable filename from its value, so
70
+ map it from a STABLE unique value (an item guid/id/link, or a snapshot date)
71
+ — that's what makes re-fetches upsert in place instead of duplicating.
72
+ - `displayField` (recommended): the field whose value labels each record in the
73
+ calendar and notifications. Set it to the human-readable field (e.g. the
74
+ headline) — otherwise labels fall back to the opaque primaryKey id.
75
+ - `fields` is an OBJECT keyed by field name (NOT an array). Each value is
76
+ `{ type, label, primary? }`. `type` MUST be one of: `string`, `text`, `email`,
77
+ `number`, `date`, `boolean`, `markdown`, `enum` (enum also needs `values`).
78
+ There is no `url`/`datetime`/`textarea` — use `string` for links, `date` for
79
+ timestamps, `text`/`markdown` for bodies.
80
+ - Include a `date` field and map the feed's timestamp into it — values in a
81
+ `date` field are auto-coerced to `YYYY-MM-DD`, which powers the calendar view
82
+ and the `maxItems` cap.
83
+
84
+ ## The `ingest` block
85
+
86
+ - `kind`: `rss` / `atom` (XML feeds) or `http-json` (a JSON API).
87
+ - `url`: the feed / API endpoint (must be public http/https; the host refuses
88
+ private/loopback addresses).
89
+ - `schedule`: `hourly` | `daily` | `weekly` | `on-demand`.
90
+ - `map`: `{ <yourFieldName>: <sourcePath> }` — a dot/bracket path into each
91
+ fetched item. **Map the fields you actually saw when you inspected the feed.**
92
+ - rss/atom: each item is the parsed XML element. Tags are keys (`title`,
93
+ `link`, `pubDate`); attributes are keyed `@_name` (`enclosure.@_url`);
94
+ namespaced tags keep their prefix (`dc:creator`, `itunes:duration`);
95
+ text-bearing tags resolve to their text automatically.
96
+ - http-json: each item is the JSON object (`name`, `data.id`).
97
+ - `itemsAt` (http-json only): dot/bracket path to the items array, e.g.
98
+ `results[]`. **Omit it when the response body is itself the array.**
99
+ - `idFrom` (optional): a sourcePath for a stable id, used when the mapped
100
+ primaryKey value is empty (e.g. `guid`).
101
+ - `maxItems` (optional, default `100`): keep only the newest N records by the
102
+ date field and delete the rest (`0` keeps everything; needs a `date` field).
103
+
104
+ ## Notes
105
+
106
+ - http-json needs an array of objects. Columnar/parallel-array APIs (e.g.
107
+ Open-Meteo weather: `hourly.time[]` + `hourly.temperature_2m[]`) are not
108
+ supported yet.
109
+ - A malformed `schema.json` is skipped at load time (with a diagnostic on the
110
+ notification bell), so double-check the shape above before writing.
111
+ - A feed can carry **custom views** just like any collection — author the HTML at
112
+ `feeds/<slug>/views/<name>.html` and register it in the feed's `schema.json`
113
+ under `views[]`. See `config/helps/custom-view.md` (note the feed path is
114
+ `feeds/<slug>/`, not `data/skills/<slug>/`).
@@ -0,0 +1,57 @@
1
+ # Gemini API Key
2
+
3
+ MulmoClaude uses Google's Gemini API for image, audio, and video generation. Setting the `GEMINI_API_KEY` environment variable is **technically optional**, but we **strongly recommend** it — a large portion of MulmoClaude's visual and multimedia output depends on it.
4
+
5
+ A single key unlocks all three capabilities (images, TTS audio, video) — you don't need separate credentials for each.
6
+
7
+ ## What the Key Unlocks
8
+
9
+ ### Images
10
+
11
+ - **`generateImage`** — creates images from text prompts. The heart of the **Artist** role.
12
+ - **`editImages`** — transforms, restyles, or combines up to 8 existing images per call ("convert to Ghibli style", "remove the background", "merge these two photos into one"). Also **Artist**.
13
+ - **Inline document images** — roles that produce rich documents (**Guide & Planner**, **Tutor**, Recipe Guide, Trip Planner, …) embed generated images directly into the page via `presentDocument`. Without a key, those image slots fall back to italic "🖼️ Image: &lt;prompt&gt;" text markers — the prompt is preserved, but no picture renders.
14
+ - **MulmoScript image beats** — `presentMulmoScript` uses Gemini image models for `imagePrompt` beats. **Storyteller Plus** additionally uses them for consistent-character scenes across a storyboard.
15
+
16
+ ### Audio
17
+
18
+ - **MulmoScript speech** — `presentMulmoScript` synthesizes speaker voices via Gemini TTS (`gemini-2.5-flash-preview-tts`). This is what turns a storyboard into spoken narration, so the **Storyteller** and **Storyteller Plus** roles become near-complete multimedia pieces.
19
+
20
+ ### Video
21
+
22
+ - **MulmoScript movie beats** — beats whose `image.type` is `moviePrompt` are rendered with Google's **Veo** models (`veo-2.0-generate-001`, `veo-3.0-generate-001`, …). Without a key, movie beats can't be produced.
23
+
24
+ ### Bottom line
25
+
26
+ Without a Gemini API key:
27
+
28
+ - The **Artist** role has nothing to generate.
29
+ - **Storyteller** and **Storyteller Plus** still produce a storyboard, but without images or narration.
30
+ - Rich documents from **Guide & Planner**, **Tutor**, and similar roles render as text-only with placeholder markers where images should be.
31
+
32
+ ## How to Get a Key
33
+
34
+ The Gemini API has a **free tier that is sufficient for personal use**. Higher-volume or premium-model use (e.g. Veo video) may require a paid plan.
35
+
36
+ 1. Open [Google AI Studio → API keys](https://aistudio.google.com/apikey) and sign in with a Google account.
37
+ 2. Click **Create API key**. If prompted, select or create a Google Cloud project (any project will do).
38
+ 3. Copy the key — it starts with `AIza…`.
39
+ 4. Open the project's `.env` file in the repository root (copy `.env.example` first if it doesn't exist yet) and add the line:
40
+
41
+ ```
42
+ GEMINI_API_KEY=AIza…your-key…
43
+ ```
44
+
45
+ 5. Restart MulmoClaude so the new environment variable is picked up.
46
+
47
+ ## Verifying It's Active
48
+
49
+ The quickest check: switch to the **Artist** role and ask for _"an image of a red panda"_. If a real image appears in the canvas (instead of an italic text marker or a disabled-role hint), the key is wired up correctly.
50
+
51
+ You can also inspect the server log on startup: messages like `GEMINI_API_KEY not set — image placeholders will render as text markers` indicate the key is missing or misread.
52
+
53
+ ## Security
54
+
55
+ - The key lives in your local `.env` file. MulmoClaude never uploads it to its own servers or to Anthropic — requests go directly from your machine to Google.
56
+ - Treat the key like a password. Anyone who sees it can make billable API calls against your Google account.
57
+ - If you suspect a key has leaked, revoke it from [Google AI Studio → API keys](https://aistudio.google.com/apikey) and generate a new one.
@@ -0,0 +1,23 @@
1
+ # GitHub repositories in the workspace
2
+
3
+ Git repositories cloned for the user live under `github/` in the workspace root.
4
+
5
+ ## Rules
6
+
7
+ 1. **Clone destination**: always clone into `github/<dir-name>/`, never into `/tmp/` or other locations outside the workspace.
8
+ 2. **Existing repo — same remote**: if `github/<dir-name>/` already exists and its `origin` remote matches the requested URL, run `git pull` to update instead of cloning again.
9
+ 3. **Existing repo — different remote**: if a directory with the desired name already exists but points at a different remote, **ask the user** to choose a directory name before proceeding. Never overwrite or re-initialize an existing repo silently.
10
+ 4. **Directory naming**: use the repository name by default (e.g. `github/mulmoclaude/` for `git@github.com:receptron/mulmoclaude.git`). If the user specifies a different name, use that.
11
+
12
+ ## Examples
13
+
14
+ ```bash
15
+ # First clone
16
+ git clone git@github.com:receptron/mulmoclaude.git github/mulmoclaude
17
+
18
+ # Already exists, same remote → update
19
+ cd github/mulmoclaude && git pull
20
+
21
+ # Name conflict, different remote → ask user
22
+ # "github/mulmoclaude already exists with a different remote. What name would you like?"
23
+ ```