@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.
- package/assets/helps/billing-clients-worklog.md +215 -0
- package/assets/helps/billing-invoice.md +458 -0
- package/assets/helps/business.md +104 -0
- package/assets/helps/collection-skills.md +810 -0
- package/assets/helps/custom-view.md +433 -0
- package/assets/helps/feeds.md +114 -0
- package/assets/helps/gemini.md +57 -0
- package/assets/helps/github.md +23 -0
- package/assets/helps/guide.md +61 -0
- package/assets/helps/index.md +89 -0
- package/assets/helps/lessons-collection.md +400 -0
- package/assets/helps/mulmoscript.md +249 -0
- package/assets/helps/portfolio-tracker.md +211 -0
- package/assets/helps/presentation-deck.md +828 -0
- package/assets/helps/presenthtml.md +89 -0
- package/assets/helps/sandbox.md +97 -0
- package/assets/helps/spreadsheet.md +43 -0
- package/assets/helps/storyteller.md +101 -0
- package/assets/helps/telegram.md +136 -0
- package/assets/helps/todo-collection.md +140 -0
- package/assets/helps/vocabulary.md +109 -0
- package/assets/helps/wiki.md +168 -0
- package/assets/skills-preset/mc-cooking-coach/SKILL.md +217 -0
- package/assets/skills-preset/mc-library/SKILL.md +188 -0
- package/assets/skills-preset/mc-manage-automations/SKILL.md +119 -0
- package/assets/skills-preset/mc-manage-skills/SKILL.md +141 -0
- package/assets/skills-preset/mc-wiki-deep-lint/SKILL.md +108 -0
- package/assets/skills-preset/mc-wiki-health-check/SKILL.md +61 -0
- package/assets/skills-preset/mc-wiki-ingest/SKILL.md +182 -0
- package/assets/skills-preset/mc-wiki-promote/SKILL.md +175 -0
- package/assets/skills-preset/mc-zenn/SKILL.md +136 -0
- package/dist/chunk-CKQMccvm.cjs +28 -0
- package/dist/collection/core/actionVisible.d.ts +34 -0
- package/dist/collection/core/calendarGrid.d.ts +120 -0
- package/dist/collection/core/deriveAll.d.ts +38 -0
- package/dist/collection/core/derivedFormula.d.ts +18 -0
- package/dist/collection/core/draft.d.ts +18 -0
- package/dist/collection/core/enumColors.d.ts +33 -0
- package/dist/collection/core/errorMessage.d.ts +4 -0
- package/dist/collection/core/itemLabel.d.ts +12 -0
- package/dist/collection/core/presentCollection.d.ts +13 -0
- package/dist/collection/core/promptSafety.d.ts +1 -0
- package/dist/collection/core/schema.d.ts +355 -0
- package/dist/collection/core/shortHexId.d.ts +8 -0
- package/dist/collection/core/sortItems.d.ts +29 -0
- package/dist/collection/core/uiTypes.d.ts +106 -0
- package/dist/collection/index.cjs +793 -0
- package/dist/collection/index.cjs.map +1 -0
- package/dist/collection/index.d.ts +14 -0
- package/dist/collection/index.js +740 -0
- package/dist/collection/index.js.map +1 -0
- package/dist/collection/paths.cjs +44 -0
- package/dist/collection/paths.cjs.map +1 -0
- package/dist/collection/paths.js +41 -0
- package/dist/collection/paths.js.map +1 -0
- package/dist/collection/server/atomic.d.ts +1 -0
- package/dist/collection/server/delete.d.ts +38 -0
- package/dist/collection/server/derive.d.ts +8 -0
- package/dist/collection/server/discoveredCollection.d.ts +18 -0
- package/dist/collection/server/discovery.d.ts +227 -0
- package/dist/collection/server/host.d.ts +77 -0
- package/dist/collection/server/index.cjs +1721 -0
- package/dist/collection/server/index.cjs.map +1 -0
- package/dist/collection/server/index.d.ts +11 -0
- package/dist/collection/server/index.js +1671 -0
- package/dist/collection/server/index.js.map +1 -0
- package/dist/collection/server/io.d.ts +114 -0
- package/dist/collection/server/paths.d.ts +52 -0
- package/dist/collection/server/spawn.d.ts +55 -0
- package/dist/collection/server/templatePath.d.ts +25 -0
- package/dist/collection/server/util.d.ts +3 -0
- package/dist/collection/server/validate.d.ts +19 -0
- package/dist/collection/server/views.d.ts +20 -0
- package/dist/deriveAll-C15OpM3K.cjs +399 -0
- package/dist/deriveAll-C15OpM3K.cjs.map +1 -0
- package/dist/deriveAll-C6BYnpBL.js +364 -0
- package/dist/deriveAll-C6BYnpBL.js.map +1 -0
- package/dist/file-change/index.cjs +72 -0
- package/dist/file-change/index.cjs.map +1 -0
- package/dist/file-change/index.d.ts +43 -0
- package/dist/file-change/index.js +66 -0
- package/dist/file-change/index.js.map +1 -0
- package/dist/notifier/engine.d.ts +72 -0
- package/dist/notifier/index.cjs +484 -0
- package/dist/notifier/index.cjs.map +1 -0
- package/dist/notifier/index.d.ts +3 -0
- package/dist/notifier/index.js +464 -0
- package/dist/notifier/index.js.map +1 -0
- package/dist/notifier/store.d.ts +18 -0
- package/dist/notifier/types.d.ts +118 -0
- package/dist/notifier/validate.d.ts +17 -0
- package/dist/scheduler/adapter.d.ts +48 -0
- package/dist/scheduler/index.cjs +352 -0
- package/dist/scheduler/index.cjs.map +1 -0
- package/dist/scheduler/index.d.ts +2 -0
- package/dist/scheduler/index.js +343 -0
- package/dist/scheduler/index.js.map +1 -0
- package/dist/scheduler/task-manager.d.ts +51 -0
- package/dist/whisper/client.cjs +241 -0
- package/dist/whisper/client.cjs.map +1 -0
- package/dist/whisper/client.d.ts +35 -0
- package/dist/whisper/client.js +239 -0
- package/dist/whisper/client.js.map +1 -0
- package/dist/whisper/ffmpeg.d.ts +6 -0
- package/dist/whisper/index.cjs +433 -0
- package/dist/whisper/index.cjs.map +1 -0
- package/dist/whisper/index.d.ts +5 -0
- package/dist/whisper/index.js +425 -0
- package/dist/whisper/index.js.map +1 -0
- package/dist/whisper/internal.d.ts +11 -0
- package/dist/whisper/models.d.ts +49 -0
- package/dist/whisper/sidecar.d.ts +8 -0
- package/dist/whisper/whisper.d.ts +28 -0
- package/dist/workspace-setup/assets.d.ts +10 -0
- package/dist/workspace-setup/index.d.ts +3 -0
- package/dist/workspace-setup/index.js +556 -0
- package/dist/workspace-setup/index.js.map +1 -0
- package/dist/workspace-setup/slug.d.ts +6 -0
- package/dist/workspace-setup/slug.js +13 -0
- package/dist/workspace-setup/slug.js.map +1 -0
- package/dist/workspace-setup/sync.d.ts +94 -0
- 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: <prompt>" 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
|
+
```
|