@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,810 @@
|
|
|
1
|
+
# Collection skills — build a data app from a schema
|
|
2
|
+
|
|
3
|
+
A **collection skill** is a skill directory that ships a `schema.json` next to
|
|
4
|
+
its `SKILL.md`. The schema declares an entire data-driven app — its data model,
|
|
5
|
+
cross-record relations, rendered UI, computed fields, and per-record action
|
|
6
|
+
buttons — in one small JSON file. You author the schema, you write the records
|
|
7
|
+
(one JSON file each), and you are the runtime for any behaviour the schema
|
|
8
|
+
can't express declaratively. The host contains **zero** knowledge of any
|
|
9
|
+
specific collection: it just reads the DSL and renders a table / calendar /
|
|
10
|
+
form / detail view, and serves a REST surface. No database, no migration tool, no ORM — a
|
|
11
|
+
`schema.json` plus a folder of `<id>.json` records **is** the app.
|
|
12
|
+
|
|
13
|
+
This is the project philosophy made concrete: _the workspace is the database;
|
|
14
|
+
files are the source of truth; you are the intelligent interface._
|
|
15
|
+
|
|
16
|
+
## Anatomy of a collection skill
|
|
17
|
+
|
|
18
|
+
You **author** the skill under `data/skills/<slug>/` (a plain, writable data
|
|
19
|
+
dir). A host-side hook then **mirrors** the files into `.claude/skills/<slug>/`,
|
|
20
|
+
which is where the host actually discovers and renders the collection from:
|
|
21
|
+
|
|
22
|
+
```
|
|
23
|
+
data/skills/<slug>/ ← YOU write here (Write / Edit)
|
|
24
|
+
SKILL.md ← instructions you read later (how to CRUD the records)
|
|
25
|
+
schema.json ← the DSL: data model + relations + UI + actions
|
|
26
|
+
templates/*.md ← natural-language bodies for actions (only if it has actions)
|
|
27
|
+
│
|
|
28
|
+
│ host's skill-bridge hook mirrors these three file kinds
|
|
29
|
+
▼
|
|
30
|
+
.claude/skills/<slug>/ ← host reads here (do NOT write here directly)
|
|
31
|
+
SKILL.md · schema.json · templates/*.md
|
|
32
|
+
|
|
33
|
+
data/<name>/items/ ← the records (separate from the skill dir)
|
|
34
|
+
<id>.json ← one record per file (you write; host reads + renders)
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
- **Author under `data/skills/<slug>/`, NEVER `.claude/skills/<slug>/`
|
|
38
|
+
directly.** Claude Code gates writes into `.claude/` (it's the agent's own
|
|
39
|
+
config surface) and the host GUI can't answer that prompt, so a direct write
|
|
40
|
+
hangs/fails. Writing under `data/skills/` has no such gate; the bridge hook
|
|
41
|
+
copies `SKILL.md`, `schema.json`, and `templates/*.md` across for you and
|
|
42
|
+
triggers a re-scan, so the collection appears at `/collections/<slug>`
|
|
43
|
+
without a restart. (Other files you drop in `data/skills/<slug>/` — a README,
|
|
44
|
+
scratch notes — stay put and are NOT mirrored.)
|
|
45
|
+
- **To CHANGE an existing collection's schema, use `manageCollection` — not raw
|
|
46
|
+
file edits.** Call `schemaDocs` for this very reference, `getSchema` to read
|
|
47
|
+
the current `schema.json` (you don't need to know its path), then `putSchema`
|
|
48
|
+
to write it back. `putSchema` validates the whole schema against the same rules
|
|
49
|
+
discovery enforces and reports the exact problem, where a hand-edit can
|
|
50
|
+
silently fail validation and make the collection vanish from the UI. It writes
|
|
51
|
+
the canonical `data/skills/<slug>/schema.json` and mirrors it for you — same
|
|
52
|
+
destination as authoring, just validated. (Creating a *new* collection still
|
|
53
|
+
means writing `SKILL.md` + `schema.json` under `data/skills/<slug>/`, since
|
|
54
|
+
there's nothing to `getSchema` yet.)
|
|
55
|
+
- **Do NOT use the `mc-` prefix** for skills you create. `mc-*` is reserved for
|
|
56
|
+
the bundled presets (`mc-cooking-coach`, `mc-library`, `mc-wiki-*`,
|
|
57
|
+
`mc-manage-*`); the server overwrites those on every boot, so your edits would
|
|
58
|
+
be lost. (The billing collections — `clients`, `worklog`, `invoice`, `profile`
|
|
59
|
+
— are now recipe-authored under these plain slugs, NOT `mc-` presets; see the
|
|
60
|
+
worked reference below.)
|
|
61
|
+
- **`<slug>` rules**: lowercase letters, digits, hyphens; no leading / trailing
|
|
62
|
+
hyphen; 1–64 chars (e.g. `recipes`, `book-club`, `gym-log`). It doubles as the
|
|
63
|
+
URL (`/collections/<slug>`) and the directory name.
|
|
64
|
+
- The user opens the collection at **`/collections/<slug>`**. Link a specific
|
|
65
|
+
record with `?selected=<id>` (e.g. `/collections/recipes?selected=carbonara`).
|
|
66
|
+
|
|
67
|
+
## SKILL.md
|
|
68
|
+
|
|
69
|
+
Standard skill front-matter plus prose teaching _future-you_ how to maintain the
|
|
70
|
+
records. Keep it short and operational:
|
|
71
|
+
|
|
72
|
+
```markdown
|
|
73
|
+
---
|
|
74
|
+
name: recipes
|
|
75
|
+
description: A personal recipe box. Use whenever the user asks to add, list,
|
|
76
|
+
edit, or remove a recipe. Records live at `data/recipes/items/<id>.json`
|
|
77
|
+
(one JSON per recipe); the user views them at `/collections/recipes`,
|
|
78
|
+
rendered from `schema.json` by the host. Record I/O via the
|
|
79
|
+
`manageCollection` tool (raw Read / Write / Edit on the JSON files is the
|
|
80
|
+
escape hatch); schema/structure edits via `manageCollection`
|
|
81
|
+
`schemaDocs` / `getSchema` / `putSchema`.
|
|
82
|
+
---
|
|
83
|
+
|
|
84
|
+
# Recipes (schema-driven collection)
|
|
85
|
+
|
|
86
|
+
## Record shape
|
|
87
|
+
|
|
88
|
+
- `id` — kebab-case slug, primary key (the filename, no extension)
|
|
89
|
+
- `title` — string, required
|
|
90
|
+
- ... (one bullet per field; note which are host-computed and must NOT be written)
|
|
91
|
+
|
|
92
|
+
## What to do
|
|
93
|
+
|
|
94
|
+
**Add / Update** — `manageCollection` putItems: each row is validated against
|
|
95
|
+
the schema BEFORE the write; fix any `rejected` row from its `problem` text
|
|
96
|
+
and retry just those. Use `mode: "create"` when adding so an id collision is
|
|
97
|
+
rejected instead of silently overwritten, and `mode: "merge"` with a partial
|
|
98
|
+
row (`{ id, <changed fields> }`) when updating — the default upsert replaces
|
|
99
|
+
the WHOLE record and would erase every optional field the row omits.
|
|
100
|
+
**List / Read** — `manageCollection` getItems: the only way to see
|
|
101
|
+
host-computed `derived` / `toggle` / `embed` values (the stored JSON never
|
|
102
|
+
contains them); pass `ids` / `fields` on large collections.
|
|
103
|
+
**Delete** — remove the record file.
|
|
104
|
+
**Change the schema** (add / rename / remove a field, view, or action) —
|
|
105
|
+
`manageCollection` `schemaDocs` for the field DSL, `getSchema` to read the
|
|
106
|
+
current schema, then `putSchema` to validate-and-write it. Do NOT hand-edit
|
|
107
|
+
`schema.json` with Read / Write / Edit — `putSchema` validates the whole schema
|
|
108
|
+
first and tells you exactly what's wrong, where a raw edit can silently fail
|
|
109
|
+
discovery's validation and make the collection vanish.
|
|
110
|
+
Don't recite the whole table in chat. After adding or updating a record,
|
|
111
|
+
call `presentCollection` (with the collection slug and the record's id) to
|
|
112
|
+
show it inline; for a plain "show/list" request, call `presentCollection`
|
|
113
|
+
with just the slug.
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
Write the `description` so it tells _you_ (in a future session) exactly when to
|
|
117
|
+
reach for this skill and where the records live — that text is what gets matched
|
|
118
|
+
when the user makes a request.
|
|
119
|
+
|
|
120
|
+
## schema.json — the DSL
|
|
121
|
+
|
|
122
|
+
Top-level shape (validated on discovery; a malformed schema is logged and
|
|
123
|
+
skipped, never crashes the host):
|
|
124
|
+
|
|
125
|
+
| Key | Meaning |
|
|
126
|
+
| ---------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
127
|
+
| `title` | Human name shown in the sidebar / header. Required. |
|
|
128
|
+
| `icon` | A **Material Symbols** name (`receipt_long`, `people`, `schedule`, `menu_book`). Required. |
|
|
129
|
+
| `dataPath` | Workspace-relative records folder, e.g. `data/recipes/items`. Must stay under the workspace. Required. |
|
|
130
|
+
| `primaryKey` | The field name whose value is the filename. That field MUST set `primary: true`. The value must be a valid record id (see the **Records** section's id-charset rule). Required. |
|
|
131
|
+
| `singleton` | Optional. When set, at most one record exists, pinned to this exact id (e.g. `me`). Host pre-fills + locks the create form and hides Add once it exists. |
|
|
132
|
+
| `fields` | Ordered map of field-name → field spec. **Insertion order = column order** in the table. Required. |
|
|
133
|
+
| `actions` | Optional array of per-record buttons (see below). |
|
|
134
|
+
| `completionField` | Optional. Name of the field whose value marks an item as "done" — when set, item-create fires a bell notification that clears once the field reaches one of `completionDoneValues`. Must name a real field in `fields`. Paired with `completionDoneValues` (both set, or both omitted). |
|
|
135
|
+
| `completionDoneValues` | Optional. Non-empty array of values that count as "done" for `completionField` (e.g. `["Done"]`, `["paid", "void"]`). Compared as strings. |
|
|
136
|
+
| `notifyWhen` | Optional. A `when` predicate (`{ "field": "...", "in": [...] }`) that **gates** the completion bell: fire it only for records matching the predicate (e.g. `{ "field": "priority", "in": ["high", "urgent"] }`). Requires `completionField`; `field` must name a real field. Absent ⇒ notify for every open record. |
|
|
137
|
+
| `displayField` | Optional. Name of a field whose value is shown as the human-readable label in the completion notification's title (e.g. `Contacts: Jane Doe` instead of the opaque primaryKey). Must name a real field in `fields`. Falls back to the primaryKey value when unset or when the record's value is empty. |
|
|
138
|
+
| `triggerField` | Optional. Name of a `date` field that **delays** the completion bell until that date arrives (instead of firing on create). Requires `completionField` / `completionDoneValues` (the bell still clears via the done value). Must name a real `date` field. See "Time-gated bells" below. |
|
|
139
|
+
| `triggerLeadDays` | Optional. Non-negative integer: fire the bell this many days **before** `triggerField` (e.g. `10` = "remind me 10 days early"). Requires `triggerField`. Default `0` (fire on the trigger date). |
|
|
140
|
+
| `spawn` | Optional. Host-driven **recurrence**: when a record reaches a configured value (e.g. `status: paid`), the host auto-creates the next record with a forward-advanced `triggerField` date. Requires `triggerField`. See "Recurring obligations" below. |
|
|
141
|
+
| `calendarField` | Optional. Name of a `date` (or `datetime`) field that anchors the **calendar view** (a month grid; each record lands on its date cell). When unset, the table↔calendar toggle still appears if the schema has any `date`/`datetime` field — the first one is used, switchable in-view. Set this to pin a specific anchor. Must name a real `date`/`datetime` field. See "Calendar view" below. |
|
|
142
|
+
| `calendarEndField` | Optional. A second `date`/`datetime` field marking the END of a multi-day span on the calendar (the record renders across `calendarField` → this date). Requires `calendarField`. Must name a real `date`/`datetime` field. |
|
|
143
|
+
| `calendarTimeField` | Optional. Name of a string field holding a free-form time or time-range (`"14:00-17:00"`, `"17:00-"`, `"16:30"`) used to place records on the calendar's **day (time-allocation) view**. Consulted only when the date fields are date-only — a `datetime` anchor/end pair carries its own clock and takes precedence. Requires `calendarField`. See "Calendar view" below. |
|
|
144
|
+
| `kanbanField` | Optional. Name of an `enum` field that groups records into columns on the **Kanban board** (one column per declared value). When unset, the Kanban toggle still appears if the schema has any `enum` field — the first one is used, switchable in-view. Set this to pin a specific group field. Must name a real `enum` field. See "Kanban view" below. |
|
|
145
|
+
| `views` | Optional. Custom (LLM-authored) HTML views: `[{ id, label, icon?, file, capabilities? }]`. Each renders an HTML file under `views/*.html` in a sandboxed iframe over the records, for layouts the built-ins don't cover (year/quarter overview, Gantt, report). `capabilities` is `["read"]` (default) or `["read","write"]`. See "Custom views" below and **`config/helps/custom-view.md`** for the full authoring contract. |
|
|
146
|
+
|
|
147
|
+
### Field types
|
|
148
|
+
|
|
149
|
+
`string` · `text` (multi-line) · `email` · `number` · `date` (`YYYY-MM-DD`) ·
|
|
150
|
+
`datetime` (`YYYY-MM-DDTHH:MM`) · `boolean` · `markdown` · `money` · `enum` ·
|
|
151
|
+
`ref` · `embed` · `table` · `derived` · `image` · `file` · `toggle`
|
|
152
|
+
|
|
153
|
+
Every field spec needs a `type` and a `label`. Extra keys by type:
|
|
154
|
+
|
|
155
|
+
- **`datetime`** — no extra keys. Stored as a `YYYY-MM-DDTHH:MM` string and
|
|
156
|
+
edited with a native date+time picker. Use it (as `calendarField` /
|
|
157
|
+
`calendarEndField`) when an event has a real start/end clock — the calendar's
|
|
158
|
+
day view then draws each record as a proportional time block. For the common
|
|
159
|
+
"date column + separate time column" shape, keep `date` and point
|
|
160
|
+
`calendarTimeField` at the time string instead.
|
|
161
|
+
- **`enum`** — `values: ["draft", "sent", "paid"]` (non-empty strings). Renders
|
|
162
|
+
a `<select>`; stored as a plain string.
|
|
163
|
+
- **`money`** — `currency: "USD"` (ISO 4217, defaults to USD). Stored as a plain
|
|
164
|
+
decimal; currency is display-only.
|
|
165
|
+
- **`ref`** — `to: "<target-slug>"`. Stores the target record's primary-key
|
|
166
|
+
slug; host renders a clickable link + a dropdown picker populated from the
|
|
167
|
+
target collection. Example: `{ "type": "ref", "to": "clients", "label": "Client" }`.
|
|
168
|
+
A `derived` field on the same record can also **dereference** a `ref` to read
|
|
169
|
+
a numeric column off the record it points at — see the cross-collection
|
|
170
|
+
formula syntax below.
|
|
171
|
+
- **`embed`** — `to: "<target-slug>"`, `id: "<record-id>"`. Pulls a _fixed_
|
|
172
|
+
record from another collection into the read-only detail view (display-only,
|
|
173
|
+
**nothing is stored** on this record). Example: an invoice embedding the
|
|
174
|
+
user's own profile: `{ "type": "embed", "to": "profile", "id": "me" }`.
|
|
175
|
+
- **`table`** — `of: { <col>: <sub-field-spec>, ... }`. An array of rows. Each
|
|
176
|
+
sub-field is a flat spec; sub-fields **cannot** be `table` or `derived`
|
|
177
|
+
(no nested tables, no computed columns).
|
|
178
|
+
- **`derived`** — `formula: "<expr>"`, optional `display` (`number` default, or
|
|
179
|
+
`money` / `string` / `date`) and `currency`. **Read-only, host-computed** —
|
|
180
|
+
you NEVER write derived values into the JSON; the host recomputes them on
|
|
181
|
+
every render and the form refuses to persist them.
|
|
182
|
+
- **`image`** — stores a **workspace-relative image path** as a plain string
|
|
183
|
+
(e.g. `data/attachments/2026/05/<id>.jpg` — the exact path from an
|
|
184
|
+
`[Attached file: ...]` marker when the user attaches a photo). The host
|
|
185
|
+
renders it as an `<img>` in the **detail view** (it is intentionally not a
|
|
186
|
+
list-table column — a per-row image fetch is too expensive for a large
|
|
187
|
+
collection). No extra keys. Great for photos like a business card: read the
|
|
188
|
+
details off the attached image and write its path into the image field.
|
|
189
|
+
Write the bare workspace-relative path — never an `/api/files/raw?...` URL.
|
|
190
|
+
- **`file`** — stores a **workspace-relative file path** as a plain string (e.g.
|
|
191
|
+
`artifacts/html/the-solar-system-1777158558023.html`). Rendered as a
|
|
192
|
+
**clickable link** in both the list table and the detail view (unlike `image`,
|
|
193
|
+
which is detail-only — a link is cheap per-row). Clicking an HTML or SVG
|
|
194
|
+
artifact opens its **rendered** form in a new browser tab (the live app /
|
|
195
|
+
drawing); any other path opens in the **File Explorer**. No extra keys. Ideal
|
|
196
|
+
for a "my apps" collection where each record points at a generated HTML app —
|
|
197
|
+
the user launches it straight from the row. Write the bare workspace-relative
|
|
198
|
+
path — never an `/artifacts/...` or `/api/files/raw?...` URL.
|
|
199
|
+
- **`toggle`** — `field: "<enum-field>"`, `onValue`, `offValue`. A checkbox that
|
|
200
|
+
is a pure **projection** of an `enum` field — it **stores nothing** of its own
|
|
201
|
+
(like `derived`/`embed`). Checked when the projected enum equals `onValue`;
|
|
202
|
+
toggling writes `onValue` / `offValue` back to that enum. `onValue`/`offValue`
|
|
203
|
+
must be members of the enum's `values`. Use it to front a kanban `status` with
|
|
204
|
+
a "done" checkbox while keeping the enum as the single source of truth — e.g.
|
|
205
|
+
`{ "type": "toggle", "label": "Done", "field": "status", "onValue": "Done", "offValue": "Todo" }`.
|
|
206
|
+
Renders an interactive checkbox in the list table and on the kanban card (when
|
|
207
|
+
it projects the board's group field); read-only in the detail view.
|
|
208
|
+
**A todo / task list should almost always include one** — it's the row/card
|
|
209
|
+
"done" checkbox. Without it, `status` only shows as a dropdown and a kanban
|
|
210
|
+
column, with no checkbox to tick. `offValue` is the status to return to on
|
|
211
|
+
uncheck (the default open column, e.g. `"Todo"`).
|
|
212
|
+
|
|
213
|
+
### Conditional field visibility (`when`)
|
|
214
|
+
|
|
215
|
+
Any field may carry an optional `when: { field, in: [...] }` predicate to hide
|
|
216
|
+
itself until another field on the same record matches — the same shape used to
|
|
217
|
+
gate `actions`. The field shows only when `String(record[when.field])` is one of
|
|
218
|
+
`in`; absent ⇒ always shown. `when.field` MUST name another top-level field
|
|
219
|
+
(validated on discovery).
|
|
220
|
+
|
|
221
|
+
```json
|
|
222
|
+
"visited": { "type": "boolean", "label": "Visited" },
|
|
223
|
+
"rating": { "type": "number", "label": "Rating", "when": { "field": "visited", "in": ["true"] } }
|
|
224
|
+
```
|
|
225
|
+
|
|
226
|
+
Here `rating` stays hidden until `visited` is checked (booleans stringify, so
|
|
227
|
+
match `"true"` / `"false"`). The gate applies everywhere the field renders: the
|
|
228
|
+
list cell goes **blank**, the edit-form input hides/shows **live** as the gating
|
|
229
|
+
field changes, and the detail view omits it. It is **purely presentational** —
|
|
230
|
+
a hidden field's stored value is never cleared, so re-matching the gate restores
|
|
231
|
+
it. Use it for fields that only make sense in a given state (a rating before
|
|
232
|
+
you've visited, a shipped-date before an order ships). Only honoured on
|
|
233
|
+
top-level fields, not inside a `table`'s `of`.
|
|
234
|
+
|
|
235
|
+
### Derived-formula syntax
|
|
236
|
+
|
|
237
|
+
A tiny expression evaluated against the record (pure evaluator, no `eval`;
|
|
238
|
+
returns `null` on any failure). Supported:
|
|
239
|
+
|
|
240
|
+
- arithmetic `+ - * /` and parentheses
|
|
241
|
+
- identifier references to **top-level** fields (`subtotal * taxRate`)
|
|
242
|
+
- `sum(tableField[].col)` — sum a column across table rows
|
|
243
|
+
- `sum(tableField[].col * tableField[].col)` — sum a per-row product
|
|
244
|
+
- `<refField>.<col>` — **dereference a `ref` field** and read a numeric column
|
|
245
|
+
off the record it points at (a live cross-collection lookup)
|
|
246
|
+
|
|
247
|
+
Example: `subtotal` = `sum(lineItems[].quantity * lineItems[].rate)`,
|
|
248
|
+
`tax` = `subtotal * taxRate`, `total` = `subtotal + tax`.
|
|
249
|
+
|
|
250
|
+
#### Cross-collection lookups (`<refField>.<col>`)
|
|
251
|
+
|
|
252
|
+
When a field is a `ref` to another collection, a `derived` formula can reach
|
|
253
|
+
into the referenced record and pull a numeric column out of it. This lets one
|
|
254
|
+
collection compute against data **owned by another** without copying that data.
|
|
255
|
+
|
|
256
|
+
Canonical use — a portfolio that values holdings against a separate price list:
|
|
257
|
+
|
|
258
|
+
```jsonc
|
|
259
|
+
// my-portfolio/schema.json (one record per holding)
|
|
260
|
+
"fields": {
|
|
261
|
+
"ticker": { "type": "ref", "to": "stock-quotes", "label": "Stock" }, // stores e.g. "AAPL"
|
|
262
|
+
"shares": { "type": "number", "label": "Shares" },
|
|
263
|
+
"value": { "type": "derived", "formula": "shares * ticker.price", // ← reads price from the quotes row
|
|
264
|
+
"display": "money", "currency": "USD", "label": "Value" }
|
|
265
|
+
}
|
|
266
|
+
```
|
|
267
|
+
|
|
268
|
+
Here `ticker.price` resolves the `ticker` ref to its `stock-quotes` record and
|
|
269
|
+
reads that record's `price`. `price` lives **only** in `stock-quotes`; the
|
|
270
|
+
portfolio never stores a copy, so a quote refresh in `stock-quotes` is the
|
|
271
|
+
single source of truth for every holding's value.
|
|
272
|
+
|
|
273
|
+
Rules and limits:
|
|
274
|
+
|
|
275
|
+
- The left side must be a `ref` field **on this same record**; the right side is
|
|
276
|
+
a single column name. Only the `<field>.<col>` shape — no `a.b.c` chains, no
|
|
277
|
+
dereferencing inside `sum(...)`.
|
|
278
|
+
- The referenced column must hold a number (or a numeric string). A missing
|
|
279
|
+
column, a non-numeric value, or a **dangling ref** (the slug points at a row
|
|
280
|
+
that doesn't exist) makes the whole formula fail soft to an em-dash (`—`),
|
|
281
|
+
same as any other formula error.
|
|
282
|
+
- The referenced column may itself be a **`derived`** field in the target
|
|
283
|
+
collection (the host computes the target's own derived fields before the
|
|
284
|
+
lookup) — _as long as_ that target formula is target-local. A target derived
|
|
285
|
+
field that in turn derefs a **third** collection won't resolve (only one hop
|
|
286
|
+
is loaded) and reads as `—`.
|
|
287
|
+
- The target collection is loaded **when the page opens**. If a value changes in
|
|
288
|
+
the target while the viewing collection is already open (e.g. you refresh a
|
|
289
|
+
price in `stock-quotes` in another tab), the derived value updates on the next
|
|
290
|
+
reload — not instantly across tabs.
|
|
291
|
+
- It's still per-record: each holding computes `shares * ticker.price` from its
|
|
292
|
+
own `ticker`/`shares`. To total the portfolio, add a one-record summary
|
|
293
|
+
collection or read the values off the list view — there is no cross-row sum
|
|
294
|
+
over a joined column.
|
|
295
|
+
|
|
296
|
+
### Actions (per-record buttons)
|
|
297
|
+
|
|
298
|
+
Each entry in `actions` renders a button in the read-only detail view. The only
|
|
299
|
+
`kind` today is `"chat"`: clicking it starts a **new chat in a role**, seeded
|
|
300
|
+
with a template + the record data — the role then does the work with its tools.
|
|
301
|
+
This is how hard logic the schema can't express (PDF generation, bookkeeping
|
|
302
|
+
journals, drafting an email) gets delegated to natural language.
|
|
303
|
+
|
|
304
|
+
```json
|
|
305
|
+
{
|
|
306
|
+
"id": "pdf", // unique within the schema
|
|
307
|
+
"label": "Generate PDF", // button text (English)
|
|
308
|
+
"icon": "picture_as_pdf", // Material Symbols name
|
|
309
|
+
"kind": "chat",
|
|
310
|
+
"role": "accounting", // which role the new chat runs in
|
|
311
|
+
"template": "templates/invoice.md", // skill-relative; no `..`, no leading `/`
|
|
312
|
+
"when": { "field": "status", "in": ["paid"] } // optional: show only when record.status ∈ {paid}
|
|
313
|
+
}
|
|
314
|
+
```
|
|
315
|
+
|
|
316
|
+
- `template` is a path **inside the skill dir** (host reads it path-safely).
|
|
317
|
+
Write the action's instructions there in plain English; the host prepends the
|
|
318
|
+
record JSON as sanitized, passive data and hands the whole thing to the role.
|
|
319
|
+
- `when` is both the visibility rule **and** the authorization rule — the host
|
|
320
|
+
re-checks it server-side, so a button gated on `status: paid` can't be invoked
|
|
321
|
+
for a draft. Omit `when` ⇒ always shown.
|
|
322
|
+
- You do **not** trigger actions yourself; point the user at the button.
|
|
323
|
+
|
|
324
|
+
### Collection-level actions (header buttons)
|
|
325
|
+
|
|
326
|
+
`collectionActions` is a second array, same entry shape as `actions`, but the
|
|
327
|
+
buttons render in the **collection header** instead of a record's detail view.
|
|
328
|
+
Use one when the work spans the whole collection rather than a single record —
|
|
329
|
+
e.g. a course-level "Learn / continue" button on a lessons collection that picks
|
|
330
|
+
the next lesson, or "Monthly close" on an invoice ledger.
|
|
331
|
+
|
|
332
|
+
The difference is what the seed prompt carries: a per-record action injects that
|
|
333
|
+
one record's JSON; a collection-level action injects a **compact progress
|
|
334
|
+
summary of every record** — each record projected down to the schema's
|
|
335
|
+
`primaryKey`, `displayField`, `completionField`, and `kanbanField` values (long
|
|
336
|
+
text / markdown / html / file fields are left out, so the prompt stays small).
|
|
337
|
+
|
|
338
|
+
```json
|
|
339
|
+
"collectionActions": [
|
|
340
|
+
{ "id": "continue", "label": "Continue", "icon": "play_arrow",
|
|
341
|
+
"kind": "chat", "role": "tutor", "template": "templates/continue.md" }
|
|
342
|
+
]
|
|
343
|
+
```
|
|
344
|
+
|
|
345
|
+
- Same `id` uniqueness rule (within `collectionActions`); same path-safe
|
|
346
|
+
`template`; same `role`-seeds-a-new-chat behavior.
|
|
347
|
+
- `when` is **ignored** here — there is no record to gate on. Always shown.
|
|
348
|
+
|
|
349
|
+
### Completion tracking (bell notifications)
|
|
350
|
+
|
|
351
|
+
Declare `completionField` + `completionDoneValues` at the top level of the
|
|
352
|
+
schema to wire a record's lifecycle into the bell:
|
|
353
|
+
|
|
354
|
+
```json
|
|
355
|
+
{
|
|
356
|
+
"title": "Todos",
|
|
357
|
+
"icon": "check_circle",
|
|
358
|
+
"dataPath": "data/todos/items",
|
|
359
|
+
"primaryKey": "id",
|
|
360
|
+
"fields": {
|
|
361
|
+
"id": { "type": "string", "label": "ID", "primary": true, "required": true },
|
|
362
|
+
"title": { "type": "string", "label": "Title", "required": true },
|
|
363
|
+
"status": { "type": "enum", "values": ["Todo", "Doing", "Done"], "label": "Status", "required": true }
|
|
364
|
+
},
|
|
365
|
+
"completionField": "status",
|
|
366
|
+
"completionDoneValues": ["Done"],
|
|
367
|
+
"displayField": "title"
|
|
368
|
+
}
|
|
369
|
+
```
|
|
370
|
+
|
|
371
|
+
Behaviour:
|
|
372
|
+
|
|
373
|
+
- **On create** the host fires a bell notification (titled
|
|
374
|
+
`<schema.title>: <label>`, where `<label>` is the record's `displayField`
|
|
375
|
+
value when declared — falling back to the primaryKey `<id>` otherwise;
|
|
376
|
+
click-navigates to `/collections/<slug>?selected=<id>` so the item's detail
|
|
377
|
+
opens) — unless the new record is **born done** (its `completionField` value
|
|
378
|
+
is already in `completionDoneValues`), in which case nothing fires. The entry
|
|
379
|
+
is published with `lifecycle: "action"` so it persists prominently in the
|
|
380
|
+
bell until the obligation resolves.
|
|
381
|
+
- **On update** the host clears the notification when `completionField`
|
|
382
|
+
transitions **into** a done value. Un-completing (Done → Todo) does NOT
|
|
383
|
+
re-fire; firing is bound to create, by design.
|
|
384
|
+
- **On delete** the host clears any matching notification so a removed record
|
|
385
|
+
can't leak a stale entry.
|
|
386
|
+
|
|
387
|
+
The pair is bundled — declaring one without the other fails schema validation.
|
|
388
|
+
`completionField` must name a real field; a typo is rejected at load. Works
|
|
389
|
+
with any field type whose stringified value is comparable (`enum`, `string`,
|
|
390
|
+
`boolean`, …) — e.g. `completionField: "status"` + `completionDoneValues:
|
|
391
|
+
["paid", "void"]` on an invoice, or `completionField: "shipped"` +
|
|
392
|
+
`completionDoneValues: ["true"]` on an order.
|
|
393
|
+
|
|
394
|
+
Set `displayField` to make the bell title readable: with `displayField:
|
|
395
|
+
"title"` the notification reads `Todos: Buy milk` instead of `Todos: t-0042`.
|
|
396
|
+
It must name a real field; an empty value on a given record falls back to the
|
|
397
|
+
primaryKey for that record.
|
|
398
|
+
|
|
399
|
+
> **Building a todo / task list?** When your `completionField` is a status enum,
|
|
400
|
+
> also add a `toggle` field (the row/card "done" checkbox) and `notifyWhen`
|
|
401
|
+
> (fire the bell only for high-priority items, not every record). See the full
|
|
402
|
+
> recipe in **"Worked example: a Todo list"** below — that's the canonical
|
|
403
|
+
> template; don't reinvent it from these fragments.
|
|
404
|
+
|
|
405
|
+
### Time-gated bells (`triggerField`)
|
|
406
|
+
|
|
407
|
+
By default the completion bell fires **on create**. Add `triggerField` — the
|
|
408
|
+
name of a `date` field — to instead **hold the bell until that date arrives**.
|
|
409
|
+
The item is still tracked, but its bell stays silent while the trigger date is
|
|
410
|
+
in the future and appears once the clock reaches it (compared at day
|
|
411
|
+
granularity in the server's local timezone). It clears the same way as any
|
|
412
|
+
completion bell — when `completionField` reaches a `completionDoneValues` value.
|
|
413
|
+
|
|
414
|
+
```json
|
|
415
|
+
{
|
|
416
|
+
"title": "Reminders",
|
|
417
|
+
"icon": "event",
|
|
418
|
+
"dataPath": "data/reminders/items",
|
|
419
|
+
"primaryKey": "id",
|
|
420
|
+
"fields": {
|
|
421
|
+
"id": { "type": "string", "label": "ID", "primary": true, "required": true },
|
|
422
|
+
"what": { "type": "string", "label": "What", "required": true },
|
|
423
|
+
"dueOn": { "type": "date", "label": "Remind on", "required": true },
|
|
424
|
+
"status": { "type": "enum", "values": ["pending", "done"], "label": "Status", "required": true }
|
|
425
|
+
},
|
|
426
|
+
"completionField": "status",
|
|
427
|
+
"completionDoneValues": ["done"],
|
|
428
|
+
"displayField": "what",
|
|
429
|
+
"triggerField": "dueOn"
|
|
430
|
+
}
|
|
431
|
+
```
|
|
432
|
+
|
|
433
|
+
This is the "nudge me about this on date X, until I mark it done" pattern. Notes:
|
|
434
|
+
|
|
435
|
+
- `triggerField` **requires** the completion pair (validation rejects it
|
|
436
|
+
otherwise — there'd be no bell to gate or clear).
|
|
437
|
+
- The named field must be type `date`; its value is parsed as `YYYY-MM-DD`.
|
|
438
|
+
- Firing is **derived from the clock, not stored** — so if the server was down
|
|
439
|
+
when the date passed, the bell simply appears at the next boot/check. Pushing
|
|
440
|
+
the date back into the future retracts a bell that already fired.
|
|
441
|
+
- Granularity is whole days (no time-of-day).
|
|
442
|
+
|
|
443
|
+
#### Lead time — fire it early (`triggerLeadDays`)
|
|
444
|
+
|
|
445
|
+
Keep `triggerField` as the **real** due date and add `triggerLeadDays` to fire
|
|
446
|
+
the bell some days ahead of it. "Remind me 10 days before rent is due":
|
|
447
|
+
|
|
448
|
+
```json
|
|
449
|
+
"triggerField": "dueOn",
|
|
450
|
+
"triggerLeadDays": 10
|
|
451
|
+
```
|
|
452
|
+
|
|
453
|
+
The bell now appears once the clock reaches `dueOn − 10 days`, and still clears
|
|
454
|
+
when the item is marked done. The lead is applied at fire time (not stored), so
|
|
455
|
+
it **composes with `spawn`**: every recurred month fires 10 days before its own
|
|
456
|
+
`dueOn`, with no extra bookkeeping. It's a non-negative whole number of days and
|
|
457
|
+
requires `triggerField`. This is a single earlier bell — not an escalating
|
|
458
|
+
multi-stage reminder (info → warning → urgent), which is intentionally out of
|
|
459
|
+
scope for collections.
|
|
460
|
+
|
|
461
|
+
### Recurring obligations (`spawn`)
|
|
462
|
+
|
|
463
|
+
Add a `spawn` block to make a collection **recur**: when a record satisfies a
|
|
464
|
+
predicate (by default, when it becomes "done"), the host automatically creates
|
|
465
|
+
the **next** record with its `triggerField` advanced. Combined with
|
|
466
|
+
`triggerField`, this expresses periodic obligations — rent, subscriptions,
|
|
467
|
+
renewals, recurring payments — with no work from you per cycle: mark this
|
|
468
|
+
month's rent `paid`, and next month's pending record appears on its own.
|
|
469
|
+
|
|
470
|
+
```json
|
|
471
|
+
"triggerField": "dueOn",
|
|
472
|
+
"spawn": {
|
|
473
|
+
"when": { "field": "status", "in": ["paid"] },
|
|
474
|
+
"every": { "unit": "month", "interval": 1, "dayOfMonth": 10 },
|
|
475
|
+
"carry": ["amount", "payee"],
|
|
476
|
+
"set": { "status": "pending" }
|
|
477
|
+
}
|
|
478
|
+
```
|
|
479
|
+
|
|
480
|
+
- **`when`** — a `{ field, in: [...] }` predicate (same shape as field/action
|
|
481
|
+
`when`) that fires the spawn. Omit it to default to "the completion-done
|
|
482
|
+
condition" (i.e. spawn when this record is done).
|
|
483
|
+
- **`every`** — how to advance `triggerField` from this record to the next:
|
|
484
|
+
- `unit`: `day` · `week` · `month` · `year`; `interval`: a positive integer
|
|
485
|
+
(so `unit: "month", interval: 3` = quarterly, `unit: "year", interval: 1`
|
|
486
|
+
= annual).
|
|
487
|
+
- `dayOfMonth` (month/year only): the **canonical** day-of-month anchor
|
|
488
|
+
(1–31, or `"last"` for the month's last day). Use it for day ≥ 29 so
|
|
489
|
+
short months don't cause drift — "31st of every month" yields
|
|
490
|
+
31 → 28/29 → 31 → 30 … correctly. Omit it for days ≤ 28 and the source
|
|
491
|
+
date's day is preserved.
|
|
492
|
+
- **Field-driven interval** (one list, mixed cadences): instead of a single
|
|
493
|
+
`{ unit, interval }`, `every` may select the interval **per record** from
|
|
494
|
+
an `enum` field. Use `{ "fromField": "<enum field>", "map": { <value>:
|
|
495
|
+
{ unit, interval, … } } }` — the host reads the record's value and
|
|
496
|
+
advances by the matching entry. This lets one collection carry daily,
|
|
497
|
+
weekly, and monthly obligations together:
|
|
498
|
+
|
|
499
|
+
```json
|
|
500
|
+
"every": {
|
|
501
|
+
"fromField": "frequency",
|
|
502
|
+
"map": {
|
|
503
|
+
"daily": { "unit": "day", "interval": 1 },
|
|
504
|
+
"weekly": { "unit": "week", "interval": 1 },
|
|
505
|
+
"monthly": { "unit": "month", "interval": 1, "dayOfMonth": 1 }
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
```
|
|
509
|
+
|
|
510
|
+
Rules (all enforced at write time): `fromField` must name a top-level
|
|
511
|
+
`enum` field; the `map` keys must **exactly cover** that enum's `values`
|
|
512
|
+
(no missing or extra keys); and `fromField` must be in `carry` (or written
|
|
513
|
+
by `set`) so the successor keeps its frequency and the chain keeps
|
|
514
|
+
recurring. Each `map` value is a literal `every` (same `unit` / `interval`
|
|
515
|
+
/ `dayOfMonth` rules as above).
|
|
516
|
+
- **`carry`** — record fields copied verbatim onto the successor (must name
|
|
517
|
+
real fields). Fields not in `carry` / `set` / the trigger+primary keys start
|
|
518
|
+
blank.
|
|
519
|
+
- **`set`** — fields forced to fixed values on the successor (typically
|
|
520
|
+
resetting the status to its pending value).
|
|
521
|
+
|
|
522
|
+
How it behaves (worth understanding so it doesn't surprise you):
|
|
523
|
+
|
|
524
|
+
- The successor's id is **deterministic**: `<stem>-<YYYYMMDD>` (the source id
|
|
525
|
+
with any trailing `-YYYYMMDD` replaced). So `rent` → `rent-20260610` →
|
|
526
|
+
`rent-20260710`. Creation is **create-if-absent** — it never overwrites, so
|
|
527
|
+
re-running is harmless and any edits you make to a successor are preserved.
|
|
528
|
+
- **Forward-only**: un-doing the source (e.g. `paid` → `pending`) does NOT
|
|
529
|
+
delete an already-created successor. And because spawning is convergent,
|
|
530
|
+
deleting the successor while the source still matches `when` will **re-create
|
|
531
|
+
it**. To genuinely **stop a recurrence**, move the source to a status that is
|
|
532
|
+
_not_ in `spawn.when` (e.g. an `archived` value) — that's the supported "end
|
|
533
|
+
it" gesture.
|
|
534
|
+
- `spawn` **requires** `triggerField` (the successor's date is `triggerField`
|
|
535
|
+
advanced by `every`).
|
|
536
|
+
|
|
537
|
+
This covers _periodic_ obligations. It does **not** do escalating, multi-stage
|
|
538
|
+
reminders over a long prep window (info → warning → urgent) — that is
|
|
539
|
+
intentionally out of scope for collections.
|
|
540
|
+
|
|
541
|
+
### Calendar view
|
|
542
|
+
|
|
543
|
+
Any collection that has at least one `date` (or `datetime`) field gains a
|
|
544
|
+
**table ↔ calendar** toggle in its header — **zero config**. The calendar is a
|
|
545
|
+
month grid where each record lands on the day cell matching its date. Clicking a
|
|
546
|
+
record **chip** opens the same detail/edit panel the table uses; clicking
|
|
547
|
+
anywhere else in a day cell opens the **day (time-allocation) view** — a popup
|
|
548
|
+
vertical timeline of that day (see below), whose **+** button starts a new record
|
|
549
|
+
prefilled to that day.
|
|
550
|
+
|
|
551
|
+
```json
|
|
552
|
+
{
|
|
553
|
+
"title": "Events",
|
|
554
|
+
"icon": "event",
|
|
555
|
+
"dataPath": "data/events/items",
|
|
556
|
+
"primaryKey": "id",
|
|
557
|
+
"fields": {
|
|
558
|
+
"id": { "type": "string", "label": "ID", "primary": true, "required": true },
|
|
559
|
+
"name": { "type": "string", "label": "Name", "required": true },
|
|
560
|
+
"on": { "type": "date", "label": "Date", "required": true },
|
|
561
|
+
"until": { "type": "date", "label": "End" }
|
|
562
|
+
},
|
|
563
|
+
"displayField": "name",
|
|
564
|
+
"calendarField": "on",
|
|
565
|
+
"calendarEndField": "until"
|
|
566
|
+
}
|
|
567
|
+
```
|
|
568
|
+
|
|
569
|
+
Notes:
|
|
570
|
+
|
|
571
|
+
- **No schema change is needed to get the toggle** — it appears whenever a `date`
|
|
572
|
+
field exists. The two keys only _tune_ it: `calendarField` pins which date
|
|
573
|
+
anchors the grid (otherwise the first `date` field is used, and the user can
|
|
574
|
+
switch in-view when there are several); `calendarEndField` makes a record span
|
|
575
|
+
multiple days (`calendarField` → `calendarEndField`, inclusive).
|
|
576
|
+
- `displayField` sets the chip label (falls back to the primary key).
|
|
577
|
+
- Records whose anchor date is missing or unparseable are listed in a small
|
|
578
|
+
"No date" tray under the grid — never silently dropped.
|
|
579
|
+
- The calendar is purely a **rendering** of the records: it adds no storage and
|
|
580
|
+
fires nothing. It composes with `triggerField` / `spawn` (which drive bells and
|
|
581
|
+
recurrence) but is independent of them.
|
|
582
|
+
- This is the collection-native calendar — the way to give the user a
|
|
583
|
+
calendar of dated records. (The old standalone Calendar view +
|
|
584
|
+
`manageCalendar` tool were removed; `calendarField` is its replacement.)
|
|
585
|
+
|
|
586
|
+
#### Day view (time allocation)
|
|
587
|
+
|
|
588
|
+
Clicking a day's number badge opens a popup vertical timeline (a 24-hour grid)
|
|
589
|
+
showing how that day's records are allocated across the clock. Records need a
|
|
590
|
+
**time of day** to draw as time blocks; supply it one of two ways:
|
|
591
|
+
|
|
592
|
+
- A `datetime` `calendarField` (and optionally `calendarEndField`) — the clock
|
|
593
|
+
comes from the field value itself (`2026-06-11T14:00`).
|
|
594
|
+
- A `date` `calendarField` **plus** `calendarTimeField` naming a string field
|
|
595
|
+
with a free-form time or range. Recognised shapes:
|
|
596
|
+
|
|
597
|
+
| Time value | Day-view rendering |
|
|
598
|
+
| ------------------------------------ | ------------------------------------------------- |
|
|
599
|
+
| `"14:00-17:00"` (a range) | a proportional **time block** from 14:00 to 17:00 |
|
|
600
|
+
| `"17:00-"` or `"16:30"` (start only) | a **single line** at that time (no known end) |
|
|
601
|
+
| `"終日"`, blank, or unparseable | a chip in the **all-day strip** at the bottom |
|
|
602
|
+
|
|
603
|
+
Separators `-`, `–`, `—`, `~`, `〜`, `~` are all accepted. Overlapping blocks
|
|
604
|
+
split into side-by-side lanes; a multi-day `datetime` span is clamped to each
|
|
605
|
+
day with ▲/▼ arrows marking where it continues. Example (`date` + `time`
|
|
606
|
+
column):
|
|
607
|
+
|
|
608
|
+
```json
|
|
609
|
+
{
|
|
610
|
+
"title": "Engagements",
|
|
611
|
+
"icon": "event",
|
|
612
|
+
"dataPath": "data/engagements/items",
|
|
613
|
+
"primaryKey": "id",
|
|
614
|
+
"fields": {
|
|
615
|
+
"id": { "type": "string", "label": "ID", "primary": true, "required": true },
|
|
616
|
+
"title": { "type": "string", "label": "Title", "required": true },
|
|
617
|
+
"date": { "type": "date", "label": "Date", "required": true },
|
|
618
|
+
"time": { "type": "string", "label": "Time" }
|
|
619
|
+
},
|
|
620
|
+
"displayField": "title",
|
|
621
|
+
"calendarField": "date",
|
|
622
|
+
"calendarTimeField": "time"
|
|
623
|
+
}
|
|
624
|
+
```
|
|
625
|
+
|
|
626
|
+
### Kanban view
|
|
627
|
+
|
|
628
|
+
Any collection that has at least one `enum` field gains a **Kanban board** toggle
|
|
629
|
+
in its header — **zero config**. The board renders one column per declared enum
|
|
630
|
+
value (in `values` order), plus a trailing **Uncategorized** column for
|
|
631
|
+
empty/unknown values (omitted when the group enum is `required`). Dragging a card
|
|
632
|
+
between columns writes that enum field; clicking a card opens the same
|
|
633
|
+
detail/edit panel.
|
|
634
|
+
|
|
635
|
+
```json
|
|
636
|
+
{
|
|
637
|
+
"title": "Tasks",
|
|
638
|
+
"icon": "checklist",
|
|
639
|
+
"dataPath": "data/tasks/items",
|
|
640
|
+
"primaryKey": "id",
|
|
641
|
+
"fields": {
|
|
642
|
+
"id": { "type": "string", "label": "ID", "primary": true, "required": true },
|
|
643
|
+
"title": { "type": "string", "label": "Title", "required": true },
|
|
644
|
+
"status": { "type": "enum", "label": "Status", "values": ["Backlog", "Todo", "In Progress", "Done"] },
|
|
645
|
+
"done": { "type": "toggle", "label": "Done", "field": "status", "onValue": "Done", "offValue": "Todo" }
|
|
646
|
+
},
|
|
647
|
+
"displayField": "title",
|
|
648
|
+
"kanbanField": "status"
|
|
649
|
+
}
|
|
650
|
+
```
|
|
651
|
+
|
|
652
|
+
Notes:
|
|
653
|
+
|
|
654
|
+
- **No schema change is needed to get the toggle** — it appears whenever an
|
|
655
|
+
`enum` field exists. `kanbanField` only _tunes_ which enum groups the board
|
|
656
|
+
(otherwise the first `enum` field is used, switchable in-view).
|
|
657
|
+
- **The enum is the single source of truth.** For a todo-style "done" checkbox,
|
|
658
|
+
use a `toggle` field projecting the status enum (above) — do NOT add a separate
|
|
659
|
+
stored boolean. Checking the box sets `status` to the done value (and moves the
|
|
660
|
+
card to that column); dragging the card to the Done column checks the box. They
|
|
661
|
+
are the same write.
|
|
662
|
+
- Columns are not draggable (order comes from the enum's `values`) and there is
|
|
663
|
+
no manual ordering within a column — a drop only changes the enum value.
|
|
664
|
+
- Like the calendar, the board is purely a **rendering** of the records: it adds
|
|
665
|
+
no storage. `completionField` / `completionDoneValues` (bells) are independent
|
|
666
|
+
but pair naturally with the Done column.
|
|
667
|
+
- **Building a todo / task list?** Read `config/helps/todo-collection.md` — the
|
|
668
|
+
complete, copy-pasteable recipe (status enum + `done` toggle + priority bells +
|
|
669
|
+
calendar) plus the legacy-`todo-plugin` migration steps.
|
|
670
|
+
|
|
671
|
+
### Custom views
|
|
672
|
+
|
|
673
|
+
When the built-in views (table / calendar / kanban / dashboard) don't fit what
|
|
674
|
+
the user wants to _see_ — a year/quarter overview, a Gantt bar, a printable
|
|
675
|
+
report — author a **custom view**: an HTML file the host renders in a sandboxed
|
|
676
|
+
iframe over the records. Register it in `views[]` (above); it becomes a button
|
|
677
|
+
in the collection's view-mode selector.
|
|
678
|
+
|
|
679
|
+
- The view reads (and, with `["read","write"]`, writes) records through a
|
|
680
|
+
scoped token injected as `window.__MC_VIEW` — it never touches files directly.
|
|
681
|
+
- It is sandboxed: inline scripts + a CDN allowlist only, and `fetch` is
|
|
682
|
+
limited to the collection's own data endpoint (no third-party calls).
|
|
683
|
+
- Least privilege: declare `["read"]` unless the view edits records.
|
|
684
|
+
- It can stay **live**: call `window.__MC_VIEW.onChange(reload)` and the view
|
|
685
|
+
re-fetches whenever the records change — the assistant editing them in chat,
|
|
686
|
+
another tab, a feed refresh, or an auto-spawned record (like the built-in
|
|
687
|
+
views). One line; no polling.
|
|
688
|
+
|
|
689
|
+
The host holds **zero** view-specific code — the view is data, like the rest of
|
|
690
|
+
the collection. Full authoring contract (the `window.__MC_VIEW` shape, the
|
|
691
|
+
read/write API, the sandbox rules, and two complete sample views):
|
|
692
|
+
**`config/helps/custom-view.md`**. Read it before authoring a view.
|
|
693
|
+
|
|
694
|
+
### Worked example: a Todo list
|
|
695
|
+
|
|
696
|
+
The full todo recipe — complete `schema.json`, `SKILL.md`, a sample record, and
|
|
697
|
+
the legacy-`todo-plugin` migration steps — has its own file:
|
|
698
|
+
**`config/helps/todo-collection.md`**. Read it whenever you create or migrate a
|
|
699
|
+
todo / task list; it's the canonical template, so copy it rather than assembling
|
|
700
|
+
one from the fragments above. The one rule to remember: the `status` enum is the
|
|
701
|
+
single source of truth and the "done" checkbox is a `toggle` field projecting it
|
|
702
|
+
— **omit the toggle and the list has no checkbox.**
|
|
703
|
+
|
|
704
|
+
## Records — one JSON object per file
|
|
705
|
+
|
|
706
|
+
- Write each record to `<dataPath>/<id>.json` via the **Write** tool; the `id`
|
|
707
|
+
field's value is the filename (no extension).
|
|
708
|
+
- **Id charset** (enforced by `safeRecordId` in
|
|
709
|
+
`packages/plugins/collection-plugin/src/server/paths.ts` — the single source of
|
|
710
|
+
truth; `manageCollection` rejects ids that fail it): start and end with a
|
|
711
|
+
letter or digit; inside, also `-`, `_`, and `.` are allowed (so natural keys
|
|
712
|
+
like a Slack ts `1718900000.123456` or a SemVer `1.2.3` work). **No** path
|
|
713
|
+
separators, **no** leading/trailing dot, and **no** `..` substring. If your
|
|
714
|
+
natural key contains anything else (a space, `/`, `:`, a leading dot), sanitise
|
|
715
|
+
it first — e.g. replace each illegal run with `_`. Note `manageCollection`
|
|
716
|
+
enforces this on every targeted read/write, so an id that only *looks* fine in
|
|
717
|
+
a full `getItems` listing but violates the rule can't be updated or deleted by
|
|
718
|
+
id — fix the id, don't work around it with raw file I/O.
|
|
719
|
+
- **The file MUST be valid JSON.** A malformed record is **silently skipped** at
|
|
720
|
+
read time (logged server-side, but invisible in the UI) — so one bad file out
|
|
721
|
+
of fifteen looks like "fourteen records vanished." The #1 cause is an
|
|
722
|
+
**unescaped double-quote inside a string value**: writing `"title": "がんは"細胞のバグ""`
|
|
723
|
+
closes the string early and corrupts the file. In free-text / prose fields
|
|
724
|
+
(`text`, `markdown`, a long `objective`), either escape every inner ASCII quote
|
|
725
|
+
as `\"`, or — better — use the language's own quotation marks (`「」`/`『』` for
|
|
726
|
+
Japanese, `‘ ’`/`“ ”` or `'…'` for English) so no escaping is needed.
|
|
727
|
+
`presentCollection` re-validates the records and reports any unreadable /
|
|
728
|
+
malformed / schema-violating files back to you (a `⚠️` in its result) — so
|
|
729
|
+
always follow a batch of writes with a `presentCollection` call and **act on
|
|
730
|
+
any ⚠️ it returns** (Read → fix → Write), rather than assuming every record
|
|
731
|
+
landed.
|
|
732
|
+
- **List the directory first** and pick a fresh id rather than silently
|
|
733
|
+
overwriting. Update = Read, merge, Write back (preserve fields you weren't
|
|
734
|
+
asked to change). Delete = remove the file.
|
|
735
|
+
- **Never write `derived` fields**, and never write an `embed` field — both are
|
|
736
|
+
display-only / host-computed.
|
|
737
|
+
- Leave optional fields out of the JSON entirely rather than writing empty
|
|
738
|
+
strings.
|
|
739
|
+
- For a `ref` field, write the raw target slug, and make sure that record
|
|
740
|
+
actually exists in the target collection — an invalid slug renders as a broken
|
|
741
|
+
link. The host enforces structure and safety; **you own semantic correctness**
|
|
742
|
+
(valid refs, sane values).
|
|
743
|
+
|
|
744
|
+
## End-to-end: creating a new collection skill
|
|
745
|
+
|
|
746
|
+
1. Pick a `<slug>` (lowercase-hyphen, no `mc-` prefix) and a `dataPath`
|
|
747
|
+
(`data/<name>/items`).
|
|
748
|
+
2. Write `data/skills/<slug>/schema.json` — `title`, `icon`, `dataPath`,
|
|
749
|
+
`primaryKey` (with the matching field flagged `primary: true`), and the
|
|
750
|
+
`fields` map in the order you want columns. Add `actions` +
|
|
751
|
+
`data/skills/<slug>/templates/*.md` only if the collection needs delegated
|
|
752
|
+
behaviour. (The bridge mirrors these into `.claude/skills/<slug>/`.)
|
|
753
|
+
3. Write `data/skills/<slug>/SKILL.md` — front-matter `name` + `description`,
|
|
754
|
+
then the record-shape bullets and CRUD conventions.
|
|
755
|
+
4. Tell the user it's ready at `/collections/<slug>`. The bridge mirrors the
|
|
756
|
+
files and triggers a re-scan, so the host discovers it without a restart and
|
|
757
|
+
with no host code. If it doesn't appear: first confirm you wrote under
|
|
758
|
+
`data/skills/<slug>/` (NOT `.claude/skills/…`, which is gated and won't
|
|
759
|
+
mirror); then check your `schema.json` passed validation — primary key
|
|
760
|
+
flagged `primary: true`, `ref`/`embed` have a valid `to`, `enum` has
|
|
761
|
+
`values`, `table` has `of`, `derived` has `formula`, action ids unique,
|
|
762
|
+
`dataPath` under the workspace, `triggerField` names a real `date` field and
|
|
763
|
+
has the completion pair, `spawn` has `triggerField` and a valid `every`,
|
|
764
|
+
`calendarField` / `calendarEndField` name real `date`/`datetime` fields (and
|
|
765
|
+
`calendarEndField` requires `calendarField`), `calendarTimeField` names a real
|
|
766
|
+
field and requires `calendarField`, `kanbanField` names a real
|
|
767
|
+
`enum` field, any `toggle` field names a real `enum` `field` with its
|
|
768
|
+
`onValue` / `offValue` among that enum's `values`, and `notifyWhen` (if set)
|
|
769
|
+
requires `completionField` and names a real field.
|
|
770
|
+
(A schema that fails validation is logged server-side and silently skipped
|
|
771
|
+
at discovery.)
|
|
772
|
+
|
|
773
|
+
## Editing an existing collection's schema
|
|
774
|
+
|
|
775
|
+
To change the structure of a collection that already exists (add a field,
|
|
776
|
+
rename a label, add a view or action), go through `manageCollection` rather than
|
|
777
|
+
hand-editing the file:
|
|
778
|
+
|
|
779
|
+
1. `manageCollection` `schemaDocs` — reload this reference for the field DSL.
|
|
780
|
+
2. `manageCollection` `getSchema` (slug) — read the current `schema.json`
|
|
781
|
+
verbatim. You don't need to know where the file lives.
|
|
782
|
+
3. Apply your change to that object, then `manageCollection` `putSchema`
|
|
783
|
+
(slug, schema) — it validates the whole schema against the same rules
|
|
784
|
+
discovery enforces and either writes it (canonical `data/skills/<slug>/`,
|
|
785
|
+
mirrored for you) or returns the exact field + problem to fix and retry.
|
|
786
|
+
4. Call `presentCollection` to show the updated collection.
|
|
787
|
+
|
|
788
|
+
Why not raw Read / Write / Edit on `schema.json`? A hand-edit that fails
|
|
789
|
+
validation is **silently skipped** at discovery — the collection disappears from
|
|
790
|
+
the UI with no error. `putSchema` catches the mistake before the write and hands
|
|
791
|
+
you an actionable message. (`putSchema` is edit-only and refuses user-scope and
|
|
792
|
+
`mc-*` preset collections; create a new collection with the Write flow above.)
|
|
793
|
+
|
|
794
|
+
## Worked reference: the billing suite
|
|
795
|
+
|
|
796
|
+
The billing collections are the canonical examples. They ship as **recipes**
|
|
797
|
+
(copy-paste schemas + SKILL bodies), not as boot-overwritten presets — read the
|
|
798
|
+
recipe when the user wants any of them, and copy the schema verbatim:
|
|
799
|
+
|
|
800
|
+
- **`config/helps/billing-clients-worklog.md`** (Bundle A):
|
|
801
|
+
- **`clients`** — flat table (`string` / `email` / `text` / `markdown`). The
|
|
802
|
+
simplest possible collection; everything else `ref`s into it.
|
|
803
|
+
- **`worklog`** — adds a `ref` (`clientId → clients`), a `date`, a `number`, a
|
|
804
|
+
`boolean`. A companion data source.
|
|
805
|
+
- **`config/helps/billing-invoice.md`** (Bundle B):
|
|
806
|
+
- **`profile`** — a `singleton` (one record, id `me`): the issuer identity.
|
|
807
|
+
- **`invoice`** — the full toolkit in one schema: an `embed` issuer
|
|
808
|
+
(`profile/me`), a `ref` client (`clients`), a `table` of line items, three
|
|
809
|
+
`derived` money fields, an `enum` status, and four `actions` (PDF always-on;
|
|
810
|
+
sale / payment / void gated by `status` via `when`).
|