mulmoclaude 0.7.0 → 0.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (199) hide show
  1. package/bin/mulmoclaude.js +1 -1
  2. package/client/assets/{JsonEditor-C_RDoefj.js → JsonEditor-o5--tPQH.js} +1 -1
  3. package/client/assets/{index-BG_JJcKI.js → index-9lmYSaus.js} +246 -215
  4. package/client/assets/index-tOu5ArRZ.css +2 -0
  5. package/client/assets/{lib-Dpph7PBN.js → lib-D6Xy0IFc.js} +1 -1
  6. package/client/assets/{marp-cCGismx0.js → marp-D6GXA-EB.js} +67 -67
  7. package/client/assets/purify.es-B27wDFIb-51iYcXuK.js +3 -0
  8. package/client/assets/runtime-protocol-vue-pU0Mw7Zm.js +1 -0
  9. package/client/index.html +6 -6
  10. package/package.json +28 -15
  11. package/server/agent/activeTools.ts +5 -1
  12. package/server/agent/backend/claude-code.ts +22 -3
  13. package/server/agent/backend/fake-echo.ts +2 -1
  14. package/server/agent/backgroundSessions.ts +45 -0
  15. package/server/agent/config.ts +105 -12
  16. package/server/agent/mcp-server.ts +137 -84
  17. package/server/agent/mcp-tools/index.ts +10 -2
  18. package/server/agent/mcp-tools/manageCollection.ts +476 -0
  19. package/server/agent/mcp-tools/spawnBackgroundChat.ts +135 -0
  20. package/server/agent/stdioHttpShim.ts +2 -1
  21. package/server/api/auth/viewToken.ts +146 -0
  22. package/server/api/routes/agent.ts +88 -55
  23. package/server/api/routes/chart.ts +21 -92
  24. package/server/api/routes/collections.ts +269 -7
  25. package/server/api/routes/pdf.ts +38 -68
  26. package/server/api/routes/plugins.ts +36 -5
  27. package/server/api/routes/presentHtml.ts +52 -72
  28. package/server/api/routes/runtime-plugin.ts +18 -1
  29. package/server/api/routes/sessions.ts +7 -1
  30. package/server/api/routes/transcribe.ts +100 -0
  31. package/server/build/dispatcher.mjs +14963 -54
  32. package/server/events/collection-change.ts +70 -0
  33. package/server/events/file-change.ts +41 -70
  34. package/server/events/scheduler-adapter.ts +23 -298
  35. package/server/events/task-manager/index.ts +17 -176
  36. package/server/index.ts +51 -6
  37. package/server/notifier/engine.ts +42 -503
  38. package/server/notifier/types.ts +16 -134
  39. package/server/plugins/builtin-dispatch.ts +28 -0
  40. package/server/plugins/html-builtin.ts +28 -0
  41. package/server/plugins/markdown-builtin.ts +70 -0
  42. package/server/plugins/runtime.ts +12 -1
  43. package/server/services/translation/llm.ts +2 -1
  44. package/server/system/config.ts +32 -0
  45. package/server/system/logs/aaa +737 -0
  46. package/server/system/logs/bb +446 -0
  47. package/server/system/optionalDeps.ts +4 -0
  48. package/server/system/whisper/index.ts +97 -0
  49. package/server/utils/claudeBin.ts +248 -0
  50. package/server/utils/files/attachment-store.ts +33 -57
  51. package/server/utils/files/html-store.ts +11 -10
  52. package/server/utils/files/image-store.ts +8 -41
  53. package/server/utils/files/markdown-image-fill.ts +28 -57
  54. package/server/utils/files/markdown-store.ts +11 -10
  55. package/server/utils/files/path-validator.ts +49 -0
  56. package/server/utils/files/safe.ts +89 -0
  57. package/server/utils/files/spreadsheet-store.ts +6 -35
  58. package/server/utils/files/store-resolvers.ts +58 -0
  59. package/server/utils/files/svg-store.ts +4 -10
  60. package/server/utils/httpError.ts +12 -0
  61. package/server/workspace/chat-index/summarizer.ts +2 -1
  62. package/server/workspace/collections/configure.ts +25 -0
  63. package/server/workspace/collections/index.ts +19 -3
  64. package/server/workspace/collections/notifications.ts +60 -386
  65. package/server/workspace/collections/types.ts +6 -334
  66. package/server/workspace/collections/watcher.ts +18 -397
  67. package/server/workspace/feeds/engine.ts +14 -6
  68. package/server/workspace/feeds/ingestTypes.ts +12 -16
  69. package/server/workspace/feeds/registry.ts +1 -1
  70. package/server/workspace/hooks/handlers/skillBridge.ts +45 -229
  71. package/server/workspace/journal/archivist-cli.ts +9 -8
  72. package/server/workspace/paths.ts +8 -1
  73. package/server/workspace/skills-preset.ts +15 -582
  74. package/server/workspace/workspace.ts +8 -14
  75. package/src/App.vue +122 -38
  76. package/src/components/ChatAttachmentPreview.vue +8 -1
  77. package/src/components/ChatInput.vue +211 -43
  78. package/src/components/ConfirmModal.vue +39 -31
  79. package/src/components/FileContentRenderer.vue +38 -40
  80. package/src/components/NotificationBell.vue +64 -16
  81. package/src/components/SettingsModal.vue +7 -2
  82. package/src/components/SettingsVoiceTab.vue +183 -0
  83. package/src/components/collectionTypes.ts +33 -177
  84. package/src/composables/collections/uiHost.ts +154 -0
  85. package/src/composables/useDynamicFavicon.ts +3 -2
  86. package/src/composables/useFileDropZone.ts +3 -6
  87. package/src/composables/usePluginErrorBoundary.ts +2 -1
  88. package/src/composables/useVoiceInput.ts +108 -0
  89. package/src/config/apiRoutes.ts +27 -0
  90. package/src/config/pubsubChannels.ts +27 -0
  91. package/src/config/roles.ts +9 -10
  92. package/src/config/toolNames.ts +4 -0
  93. package/src/lang/de.ts +27 -96
  94. package/src/lang/en.ts +26 -96
  95. package/src/lang/es.ts +26 -96
  96. package/src/lang/fr.ts +26 -97
  97. package/src/lang/ja.ts +26 -96
  98. package/src/lang/ko.ts +24 -96
  99. package/src/lang/pt-BR.ts +25 -96
  100. package/src/lang/zh.ts +24 -96
  101. package/src/main.ts +3 -0
  102. package/src/plugins/chart/definition.ts +10 -61
  103. package/src/plugins/chart/index.ts +21 -26
  104. package/src/plugins/markdown/definition.ts +11 -78
  105. package/src/plugins/markdown/index.ts +24 -6
  106. package/src/plugins/presentCollection/definition.ts +10 -23
  107. package/src/plugins/presentCollection/index.ts +6 -4
  108. package/src/plugins/presentCollection/plugin.ts +6 -39
  109. package/src/plugins/presentCollection/types.ts +4 -13
  110. package/src/plugins/presentForm/definition.ts +7 -121
  111. package/src/plugins/presentForm/index.ts +20 -11
  112. package/src/plugins/presentForm/plugin.ts +4 -92
  113. package/src/plugins/presentForm/types.ts +18 -85
  114. package/src/plugins/presentHtml/definition.ts +12 -24
  115. package/src/plugins/presentHtml/index.ts +21 -9
  116. package/src/plugins/spreadsheet/View.vue +9 -3
  117. package/src/plugins/spreadsheet/definition.ts +2 -1
  118. package/src/plugins/spreadsheet/engine/calculator.ts +2 -1
  119. package/src/types/session.ts +7 -0
  120. package/src/utils/agent/eventDispatch.ts +4 -0
  121. package/src/utils/collections/notifiedItems.ts +65 -0
  122. package/src/utils/collections/presentSeed.ts +98 -0
  123. package/src/utils/errors.ts +14 -0
  124. package/src/utils/html/customViewSrcdoc.ts +88 -0
  125. package/src/utils/html/previewCsp.ts +63 -8
  126. package/src/utils/id.ts +3 -4
  127. package/src/utils/image/rewriteMarkdownImageRefs.ts +3 -1
  128. package/client/assets/index-DCoo3kpR.css +0 -2
  129. package/client/assets/purify.es-Fx1Nqyry-BufT4RJl.js +0 -2
  130. package/client/assets/runtime-protocol-vue-D6kcV0wa.js +0 -1
  131. package/server/agent/mcp-tools/x.ts +0 -210
  132. package/server/notifier/store.ts +0 -70
  133. package/server/workspace/collections/delete.ts +0 -186
  134. package/server/workspace/collections/discovery.ts +0 -730
  135. package/server/workspace/collections/io.ts +0 -287
  136. package/server/workspace/collections/paths.ts +0 -125
  137. package/server/workspace/collections/spawn.ts +0 -213
  138. package/server/workspace/collections/templatePath.ts +0 -36
  139. package/server/workspace/helps/billing-clients-worklog.md +0 -215
  140. package/server/workspace/helps/billing-invoice.md +0 -457
  141. package/server/workspace/helps/business.md +0 -104
  142. package/server/workspace/helps/collection-skills.md +0 -664
  143. package/server/workspace/helps/feeds.md +0 -110
  144. package/server/workspace/helps/gemini.md +0 -57
  145. package/server/workspace/helps/github.md +0 -23
  146. package/server/workspace/helps/guide.md +0 -61
  147. package/server/workspace/helps/index.md +0 -73
  148. package/server/workspace/helps/mulmoscript.md +0 -249
  149. package/server/workspace/helps/portfolio-tracker.md +0 -211
  150. package/server/workspace/helps/presentation-deck.md +0 -828
  151. package/server/workspace/helps/presenthtml.md +0 -80
  152. package/server/workspace/helps/sandbox.md +0 -97
  153. package/server/workspace/helps/spreadsheet.md +0 -43
  154. package/server/workspace/helps/storyteller.md +0 -101
  155. package/server/workspace/helps/telegram.md +0 -136
  156. package/server/workspace/helps/todo-collection.md +0 -140
  157. package/server/workspace/helps/vocabulary.md +0 -106
  158. package/server/workspace/helps/wiki.md +0 -168
  159. package/server/workspace/skills-preset/mc-cooking-coach/SKILL.md +0 -217
  160. package/server/workspace/skills-preset/mc-library/SKILL.md +0 -188
  161. package/server/workspace/skills-preset/mc-manage-automations/SKILL.md +0 -119
  162. package/server/workspace/skills-preset/mc-manage-skills/SKILL.md +0 -141
  163. package/server/workspace/skills-preset/mc-wiki-deep-lint/SKILL.md +0 -108
  164. package/server/workspace/skills-preset/mc-wiki-health-check/SKILL.md +0 -61
  165. package/server/workspace/skills-preset/mc-wiki-ingest/SKILL.md +0 -182
  166. package/server/workspace/skills-preset/mc-wiki-promote/SKILL.md +0 -175
  167. package/src/components/CollectionCalendarView.vue +0 -243
  168. package/src/components/CollectionDashboardView.vue +0 -181
  169. package/src/components/CollectionDayView.vue +0 -308
  170. package/src/components/CollectionEmbedView.vue +0 -69
  171. package/src/components/CollectionKanbanView.vue +0 -196
  172. package/src/components/CollectionRecordModal.vue +0 -93
  173. package/src/components/CollectionRecordPanel.vue +0 -567
  174. package/src/components/CollectionView.vue +0 -1748
  175. package/src/components/CollectionsIndexView.vue +0 -152
  176. package/src/components/FeedsView.vue +0 -225
  177. package/src/components/collectionEmbed.ts +0 -29
  178. package/src/composables/collections/useCollectionRendering.ts +0 -350
  179. package/src/plugins/chart/Preview.vue +0 -49
  180. package/src/plugins/chart/View.vue +0 -148
  181. package/src/plugins/markdown/MarpView.vue +0 -301
  182. package/src/plugins/markdown/Preview.vue +0 -102
  183. package/src/plugins/markdown/View.vue +0 -869
  184. package/src/plugins/presentCollection/Preview.vue +0 -30
  185. package/src/plugins/presentCollection/View.vue +0 -78
  186. package/src/plugins/presentForm/Preview.vue +0 -88
  187. package/src/plugins/presentForm/View.vue +0 -682
  188. package/src/plugins/presentHtml/Preview.vue +0 -18
  189. package/src/plugins/presentHtml/View.vue +0 -431
  190. package/src/utils/collections/actionVisible.ts +0 -55
  191. package/src/utils/collections/calendarGrid.ts +0 -328
  192. package/src/utils/collections/collectionViewMode.ts +0 -42
  193. package/src/utils/collections/derivedFormula.ts +0 -364
  194. package/src/utils/collections/draft.ts +0 -160
  195. package/src/utils/collections/enumColors.ts +0 -130
  196. package/src/utils/collections/itemLabel.ts +0 -42
  197. /package/client/assets/{_plugin-vue_export-helper-BOai-rQB.js → _plugin-vue_export-helper-B67ILkmu.js} +0 -0
  198. /package/client/assets/{schemas-DuYzyHQc.js → schemas-D_RbFtuQ.js} +0 -0
  199. /package/client/assets/{vue-D4w8THF_.js → vue-UDIWDtr8.js} +0 -0
@@ -1,664 +0,0 @@
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
- - **Do NOT use the `mc-` prefix** for skills you create. `mc-*` is reserved for
46
- the bundled presets (`mc-cooking-coach`, `mc-library`, `mc-wiki-*`,
47
- `mc-manage-*`); the server overwrites those on every boot, so your edits would
48
- be lost. (The billing collections — `clients`, `worklog`, `invoice`, `profile`
49
- — are now recipe-authored under these plain slugs, NOT `mc-` presets; see the
50
- worked reference below.)
51
- - **`<slug>` rules**: lowercase letters, digits, hyphens; no leading / trailing
52
- hyphen; 1–64 chars (e.g. `recipes`, `book-club`, `gym-log`). It doubles as the
53
- URL (`/collections/<slug>`) and the directory name.
54
- - The user opens the collection at **`/collections/<slug>`**. Link a specific
55
- record with `?selected=<id>` (e.g. `/collections/recipes?selected=carbonara`).
56
-
57
- ## SKILL.md
58
-
59
- Standard skill front-matter plus prose teaching *future-you* how to maintain the
60
- records. Keep it short and operational:
61
-
62
- ```markdown
63
- ---
64
- name: recipes
65
- description: A personal recipe box. Use whenever the user asks to add, list,
66
- edit, or remove a recipe. Records live at `data/recipes/items/<id>.json`
67
- (one JSON per recipe); the user views them at `/collections/recipes`,
68
- rendered from `schema.json` by the host. You do all I/O via Read / Write /
69
- Edit on the JSON files.
70
- ---
71
-
72
- # Recipes (schema-driven collection)
73
-
74
- ## Record shape
75
- - `id` — kebab-case slug, primary key (the filename, no extension)
76
- - `title` — string, required
77
- - ... (one bullet per field; note which are host-computed and must NOT be written)
78
-
79
- ## What to do
80
- **Add / List / Update / Delete** — derive an id, Read/Write/Edit the JSON.
81
- List the directory first and pick a fresh slug rather than overwriting.
82
- Don't recite the whole table in chat. After adding or updating a record,
83
- call `presentCollection` (with the collection slug and the record's id) to
84
- show it inline; for a plain "show/list" request, call `presentCollection`
85
- with just the slug.
86
- ```
87
-
88
- Write the `description` so it tells *you* (in a future session) exactly when to
89
- reach for this skill and where the records live — that text is what gets matched
90
- when the user makes a request.
91
-
92
- ## schema.json — the DSL
93
-
94
- Top-level shape (validated on discovery; a malformed schema is logged and
95
- skipped, never crashes the host):
96
-
97
- | Key | Meaning |
98
- |---|---|
99
- | `title` | Human name shown in the sidebar / header. Required. |
100
- | `icon` | A **Material Symbols** name (`receipt_long`, `people`, `schedule`, `menu_book`). Required. |
101
- | `dataPath` | Workspace-relative records folder, e.g. `data/recipes/items`. Must stay under the workspace. Required. |
102
- | `primaryKey` | The field name whose value is the filename. That field MUST set `primary: true`. Required. |
103
- | `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. |
104
- | `fields` | Ordered map of field-name → field spec. **Insertion order = column order** in the table. Required. |
105
- | `actions` | Optional array of per-record buttons (see below). |
106
- | `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). |
107
- | `completionDoneValues` | Optional. Non-empty array of values that count as "done" for `completionField` (e.g. `["Done"]`, `["paid", "void"]`). Compared as strings. |
108
- | `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. |
109
- | `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. |
110
- | `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. |
111
- | `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). |
112
- | `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. |
113
- | `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. |
114
- | `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. |
115
- | `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. |
116
- | `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. |
117
-
118
- ### Field types
119
-
120
- `string` · `text` (multi-line) · `email` · `number` · `date` (`YYYY-MM-DD`) ·
121
- `datetime` (`YYYY-MM-DDTHH:MM`) · `boolean` · `markdown` · `money` · `enum` ·
122
- `ref` · `embed` · `table` · `derived` · `image` · `file` · `toggle`
123
-
124
- Every field spec needs a `type` and a `label`. Extra keys by type:
125
-
126
- - **`datetime`** — no extra keys. Stored as a `YYYY-MM-DDTHH:MM` string and
127
- edited with a native date+time picker. Use it (as `calendarField` /
128
- `calendarEndField`) when an event has a real start/end clock — the calendar's
129
- day view then draws each record as a proportional time block. For the common
130
- "date column + separate time column" shape, keep `date` and point
131
- `calendarTimeField` at the time string instead.
132
- - **`enum`** — `values: ["draft", "sent", "paid"]` (non-empty strings). Renders
133
- a `<select>`; stored as a plain string.
134
- - **`money`** — `currency: "USD"` (ISO 4217, defaults to USD). Stored as a plain
135
- decimal; currency is display-only.
136
- - **`ref`** — `to: "<target-slug>"`. Stores the target record's primary-key
137
- slug; host renders a clickable link + a dropdown picker populated from the
138
- target collection. Example: `{ "type": "ref", "to": "clients", "label": "Client" }`.
139
- A `derived` field on the same record can also **dereference** a `ref` to read
140
- a numeric column off the record it points at — see the cross-collection
141
- formula syntax below.
142
- - **`embed`** — `to: "<target-slug>"`, `id: "<record-id>"`. Pulls a *fixed*
143
- record from another collection into the read-only detail view (display-only,
144
- **nothing is stored** on this record). Example: an invoice embedding the
145
- user's own profile: `{ "type": "embed", "to": "profile", "id": "me" }`.
146
- - **`table`** — `of: { <col>: <sub-field-spec>, ... }`. An array of rows. Each
147
- sub-field is a flat spec; sub-fields **cannot** be `table` or `derived`
148
- (no nested tables, no computed columns).
149
- - **`derived`** — `formula: "<expr>"`, optional `display` (`number` default, or
150
- `money` / `string` / `date`) and `currency`. **Read-only, host-computed** —
151
- you NEVER write derived values into the JSON; the host recomputes them on
152
- every render and the form refuses to persist them.
153
- - **`image`** — stores a **workspace-relative image path** as a plain string
154
- (e.g. `data/attachments/2026/05/<id>.jpg` — the exact path from an
155
- `[Attached file: ...]` marker when the user attaches a photo). The host
156
- renders it as an `<img>` in the **detail view** (it is intentionally not a
157
- list-table column — a per-row image fetch is too expensive for a large
158
- collection). No extra keys. Great for photos like a business card: read the
159
- details off the attached image and write its path into the image field.
160
- Write the bare workspace-relative path — never an `/api/files/raw?...` URL.
161
- - **`file`** — stores a **workspace-relative file path** as a plain string (e.g.
162
- `artifacts/html/the-solar-system-1777158558023.html`). Rendered as a
163
- **clickable link** in both the list table and the detail view (unlike `image`,
164
- which is detail-only — a link is cheap per-row). Clicking an HTML or SVG
165
- artifact opens its **rendered** form in a new browser tab (the live app /
166
- drawing); any other path opens in the **File Explorer**. No extra keys. Ideal
167
- for a "my apps" collection where each record points at a generated HTML app —
168
- the user launches it straight from the row. Write the bare workspace-relative
169
- path — never an `/artifacts/...` or `/api/files/raw?...` URL.
170
- - **`toggle`** — `field: "<enum-field>"`, `onValue`, `offValue`. A checkbox that
171
- is a pure **projection** of an `enum` field — it **stores nothing** of its own
172
- (like `derived`/`embed`). Checked when the projected enum equals `onValue`;
173
- toggling writes `onValue` / `offValue` back to that enum. `onValue`/`offValue`
174
- must be members of the enum's `values`. Use it to front a kanban `status` with
175
- a "done" checkbox while keeping the enum as the single source of truth — e.g.
176
- `{ "type": "toggle", "label": "Done", "field": "status", "onValue": "Done", "offValue": "Todo" }`.
177
- Renders an interactive checkbox in the list table and on the kanban card (when
178
- it projects the board's group field); read-only in the detail view.
179
- **A todo / task list should almost always include one** — it's the row/card
180
- "done" checkbox. Without it, `status` only shows as a dropdown and a kanban
181
- column, with no checkbox to tick. `offValue` is the status to return to on
182
- uncheck (the default open column, e.g. `"Todo"`).
183
-
184
- ### Conditional field visibility (`when`)
185
-
186
- Any field may carry an optional `when: { field, in: [...] }` predicate to hide
187
- itself until another field on the same record matches — the same shape used to
188
- gate `actions`. The field shows only when `String(record[when.field])` is one of
189
- `in`; absent ⇒ always shown. `when.field` MUST name another top-level field
190
- (validated on discovery).
191
-
192
- ```json
193
- "visited": { "type": "boolean", "label": "Visited" },
194
- "rating": { "type": "number", "label": "Rating", "when": { "field": "visited", "in": ["true"] } }
195
- ```
196
-
197
- Here `rating` stays hidden until `visited` is checked (booleans stringify, so
198
- match `"true"` / `"false"`). The gate applies everywhere the field renders: the
199
- list cell goes **blank**, the edit-form input hides/shows **live** as the gating
200
- field changes, and the detail view omits it. It is **purely presentational** —
201
- a hidden field's stored value is never cleared, so re-matching the gate restores
202
- it. Use it for fields that only make sense in a given state (a rating before
203
- you've visited, a shipped-date before an order ships). Only honoured on
204
- top-level fields, not inside a `table`'s `of`.
205
-
206
- ### Derived-formula syntax
207
-
208
- A tiny expression evaluated against the record (pure evaluator, no `eval`;
209
- returns `null` on any failure). Supported:
210
-
211
- - arithmetic `+ - * /` and parentheses
212
- - identifier references to **top-level** fields (`subtotal * taxRate`)
213
- - `sum(tableField[].col)` — sum a column across table rows
214
- - `sum(tableField[].col * tableField[].col)` — sum a per-row product
215
- - `<refField>.<col>` — **dereference a `ref` field** and read a numeric column
216
- off the record it points at (a live cross-collection lookup)
217
-
218
- Example: `subtotal` = `sum(lineItems[].quantity * lineItems[].rate)`,
219
- `tax` = `subtotal * taxRate`, `total` = `subtotal + tax`.
220
-
221
- #### Cross-collection lookups (`<refField>.<col>`)
222
-
223
- When a field is a `ref` to another collection, a `derived` formula can reach
224
- into the referenced record and pull a numeric column out of it. This lets one
225
- collection compute against data **owned by another** without copying that data.
226
-
227
- Canonical use — a portfolio that values holdings against a separate price list:
228
-
229
- ```jsonc
230
- // my-portfolio/schema.json (one record per holding)
231
- "fields": {
232
- "ticker": { "type": "ref", "to": "stock-quotes", "label": "Stock" }, // stores e.g. "AAPL"
233
- "shares": { "type": "number", "label": "Shares" },
234
- "value": { "type": "derived", "formula": "shares * ticker.price", // ← reads price from the quotes row
235
- "display": "money", "currency": "USD", "label": "Value" }
236
- }
237
- ```
238
-
239
- Here `ticker.price` resolves the `ticker` ref to its `stock-quotes` record and
240
- reads that record's `price`. `price` lives **only** in `stock-quotes`; the
241
- portfolio never stores a copy, so a quote refresh in `stock-quotes` is the
242
- single source of truth for every holding's value.
243
-
244
- Rules and limits:
245
-
246
- - The left side must be a `ref` field **on this same record**; the right side is
247
- a single column name. Only the `<field>.<col>` shape — no `a.b.c` chains, no
248
- dereferencing inside `sum(...)`.
249
- - The referenced column must hold a number (or a numeric string). A missing
250
- column, a non-numeric value, or a **dangling ref** (the slug points at a row
251
- that doesn't exist) makes the whole formula fail soft to an em-dash (`—`),
252
- same as any other formula error.
253
- - The referenced column may itself be a **`derived`** field in the target
254
- collection (the host computes the target's own derived fields before the
255
- lookup) — *as long as* that target formula is target-local. A target derived
256
- field that in turn derefs a **third** collection won't resolve (only one hop
257
- is loaded) and reads as `—`.
258
- - The target collection is loaded **when the page opens**. If a value changes in
259
- the target while the viewing collection is already open (e.g. you refresh a
260
- price in `stock-quotes` in another tab), the derived value updates on the next
261
- reload — not instantly across tabs.
262
- - It's still per-record: each holding computes `shares * ticker.price` from its
263
- own `ticker`/`shares`. To total the portfolio, add a one-record summary
264
- collection or read the values off the list view — there is no cross-row sum
265
- over a joined column.
266
-
267
- ### Actions (per-record buttons)
268
-
269
- Each entry in `actions` renders a button in the read-only detail view. The only
270
- `kind` today is `"chat"`: clicking it starts a **new chat in a role**, seeded
271
- with a template + the record data — the role then does the work with its tools.
272
- This is how hard logic the schema can't express (PDF generation, bookkeeping
273
- journals, drafting an email) gets delegated to natural language.
274
-
275
- ```json
276
- {
277
- "id": "pdf", // unique within the schema
278
- "label": "Generate PDF", // button text (English)
279
- "icon": "picture_as_pdf", // Material Symbols name
280
- "kind": "chat",
281
- "role": "accounting", // which role the new chat runs in
282
- "template": "templates/invoice.md", // skill-relative; no `..`, no leading `/`
283
- "when": { "field": "status", "in": ["paid"] } // optional: show only when record.status ∈ {paid}
284
- }
285
- ```
286
-
287
- - `template` is a path **inside the skill dir** (host reads it path-safely).
288
- Write the action's instructions there in plain English; the host prepends the
289
- record JSON as sanitized, passive data and hands the whole thing to the role.
290
- - `when` is both the visibility rule **and** the authorization rule — the host
291
- re-checks it server-side, so a button gated on `status: paid` can't be invoked
292
- for a draft. Omit `when` ⇒ always shown.
293
- - You do **not** trigger actions yourself; point the user at the button.
294
-
295
- ### Completion tracking (bell notifications)
296
-
297
- Declare `completionField` + `completionDoneValues` at the top level of the
298
- schema to wire a record's lifecycle into the bell:
299
-
300
- ```json
301
- {
302
- "title": "Todos",
303
- "icon": "check_circle",
304
- "dataPath": "data/todos/items",
305
- "primaryKey": "id",
306
- "fields": {
307
- "id": { "type": "string", "label": "ID", "primary": true, "required": true },
308
- "title": { "type": "string", "label": "Title", "required": true },
309
- "status": { "type": "enum", "values": ["Todo", "Doing", "Done"], "label": "Status", "required": true }
310
- },
311
- "completionField": "status",
312
- "completionDoneValues": ["Done"],
313
- "displayField": "title"
314
- }
315
- ```
316
-
317
- Behaviour:
318
-
319
- - **On create** the host fires a bell notification (titled
320
- `<schema.title>: <label>`, where `<label>` is the record's `displayField`
321
- value when declared — falling back to the primaryKey `<id>` otherwise;
322
- click-navigates to `/collections/<slug>?selected=<id>` so the item's detail
323
- opens) — unless the new record is **born done** (its `completionField` value
324
- is already in `completionDoneValues`), in which case nothing fires. The entry
325
- is published with `lifecycle: "action"` so it persists prominently in the
326
- bell until the obligation resolves.
327
- - **On update** the host clears the notification when `completionField`
328
- transitions **into** a done value. Un-completing (Done → Todo) does NOT
329
- re-fire; firing is bound to create, by design.
330
- - **On delete** the host clears any matching notification so a removed record
331
- can't leak a stale entry.
332
-
333
- The pair is bundled — declaring one without the other fails schema validation.
334
- `completionField` must name a real field; a typo is rejected at load. Works
335
- with any field type whose stringified value is comparable (`enum`, `string`,
336
- `boolean`, …) — e.g. `completionField: "status"` + `completionDoneValues:
337
- ["paid", "void"]` on an invoice, or `completionField: "shipped"` +
338
- `completionDoneValues: ["true"]` on an order.
339
-
340
- Set `displayField` to make the bell title readable: with `displayField:
341
- "title"` the notification reads `Todos: Buy milk` instead of `Todos: t-0042`.
342
- It must name a real field; an empty value on a given record falls back to the
343
- primaryKey for that record.
344
-
345
- > **Building a todo / task list?** When your `completionField` is a status enum,
346
- > also add a `toggle` field (the row/card "done" checkbox) and `notifyWhen`
347
- > (fire the bell only for high-priority items, not every record). See the full
348
- > recipe in **"Worked example: a Todo list"** below — that's the canonical
349
- > template; don't reinvent it from these fragments.
350
-
351
- ### Time-gated bells (`triggerField`)
352
-
353
- By default the completion bell fires **on create**. Add `triggerField` — the
354
- name of a `date` field — to instead **hold the bell until that date arrives**.
355
- The item is still tracked, but its bell stays silent while the trigger date is
356
- in the future and appears once the clock reaches it (compared at day
357
- granularity in the server's local timezone). It clears the same way as any
358
- completion bell — when `completionField` reaches a `completionDoneValues` value.
359
-
360
- ```json
361
- {
362
- "title": "Reminders",
363
- "icon": "event",
364
- "dataPath": "data/reminders/items",
365
- "primaryKey": "id",
366
- "fields": {
367
- "id": { "type": "string", "label": "ID", "primary": true, "required": true },
368
- "what": { "type": "string", "label": "What", "required": true },
369
- "dueOn": { "type": "date", "label": "Remind on", "required": true },
370
- "status": { "type": "enum", "values": ["pending", "done"], "label": "Status", "required": true }
371
- },
372
- "completionField": "status",
373
- "completionDoneValues": ["done"],
374
- "displayField": "what",
375
- "triggerField": "dueOn"
376
- }
377
- ```
378
-
379
- This is the "nudge me about this on date X, until I mark it done" pattern. Notes:
380
-
381
- - `triggerField` **requires** the completion pair (validation rejects it
382
- otherwise — there'd be no bell to gate or clear).
383
- - The named field must be type `date`; its value is parsed as `YYYY-MM-DD`.
384
- - Firing is **derived from the clock, not stored** — so if the server was down
385
- when the date passed, the bell simply appears at the next boot/check. Pushing
386
- the date back into the future retracts a bell that already fired.
387
- - Granularity is whole days (no time-of-day).
388
-
389
- #### Lead time — fire it early (`triggerLeadDays`)
390
-
391
- Keep `triggerField` as the **real** due date and add `triggerLeadDays` to fire
392
- the bell some days ahead of it. "Remind me 10 days before rent is due":
393
-
394
- ```json
395
- "triggerField": "dueOn",
396
- "triggerLeadDays": 10
397
- ```
398
-
399
- The bell now appears once the clock reaches `dueOn − 10 days`, and still clears
400
- when the item is marked done. The lead is applied at fire time (not stored), so
401
- it **composes with `spawn`**: every recurred month fires 10 days before its own
402
- `dueOn`, with no extra bookkeeping. It's a non-negative whole number of days and
403
- requires `triggerField`. This is a single earlier bell — not an escalating
404
- multi-stage reminder (info → warning → urgent), which is intentionally out of
405
- scope for collections.
406
-
407
- ### Recurring obligations (`spawn`)
408
-
409
- Add a `spawn` block to make a collection **recur**: when a record satisfies a
410
- predicate (by default, when it becomes "done"), the host automatically creates
411
- the **next** record with its `triggerField` advanced. Combined with
412
- `triggerField`, this expresses periodic obligations — rent, subscriptions,
413
- renewals, recurring payments — with no work from you per cycle: mark this
414
- month's rent `paid`, and next month's pending record appears on its own.
415
-
416
- ```json
417
- "triggerField": "dueOn",
418
- "spawn": {
419
- "when": { "field": "status", "in": ["paid"] },
420
- "every": { "unit": "month", "interval": 1, "dayOfMonth": 10 },
421
- "carry": ["amount", "payee"],
422
- "set": { "status": "pending" }
423
- }
424
- ```
425
-
426
- - **`when`** — a `{ field, in: [...] }` predicate (same shape as field/action
427
- `when`) that fires the spawn. Omit it to default to "the completion-done
428
- condition" (i.e. spawn when this record is done).
429
- - **`every`** — how to advance `triggerField` from this record to the next:
430
- - `unit`: `day` · `week` · `month` · `year`; `interval`: a positive integer
431
- (so `unit: "month", interval: 3` = quarterly, `unit: "year", interval: 1`
432
- = annual).
433
- - `dayOfMonth` (month/year only): the **canonical** day-of-month anchor
434
- (1–31, or `"last"` for the month's last day). Use it for day ≥ 29 so
435
- short months don't cause drift — "31st of every month" yields
436
- 31 → 28/29 → 31 → 30 … correctly. Omit it for days ≤ 28 and the source
437
- date's day is preserved.
438
- - **`carry`** — record fields copied verbatim onto the successor (must name
439
- real fields). Fields not in `carry` / `set` / the trigger+primary keys start
440
- blank.
441
- - **`set`** — fields forced to fixed values on the successor (typically
442
- resetting the status to its pending value).
443
-
444
- How it behaves (worth understanding so it doesn't surprise you):
445
-
446
- - The successor's id is **deterministic**: `<stem>-<YYYYMMDD>` (the source id
447
- with any trailing `-YYYYMMDD` replaced). So `rent` → `rent-20260610` →
448
- `rent-20260710`. Creation is **create-if-absent** — it never overwrites, so
449
- re-running is harmless and any edits you make to a successor are preserved.
450
- - **Forward-only**: un-doing the source (e.g. `paid` → `pending`) does NOT
451
- delete an already-created successor. And because spawning is convergent,
452
- deleting the successor while the source still matches `when` will **re-create
453
- it**. To genuinely **stop a recurrence**, move the source to a status that is
454
- *not* in `spawn.when` (e.g. an `archived` value) — that's the supported "end
455
- it" gesture.
456
- - `spawn` **requires** `triggerField` (the successor's date is `triggerField`
457
- advanced by `every`).
458
-
459
- This covers *periodic* obligations. It does **not** do escalating, multi-stage
460
- reminders over a long prep window (info → warning → urgent) — that is
461
- intentionally out of scope for collections.
462
-
463
- ### Calendar view
464
-
465
- Any collection that has at least one `date` (or `datetime`) field gains a
466
- **table ↔ calendar** toggle in its header — **zero config**. The calendar is a
467
- month grid where each record lands on the day cell matching its date. Clicking a
468
- record **chip** opens the same detail/edit panel the table uses; clicking
469
- anywhere else in a day cell opens the **day (time-allocation) view** — a popup
470
- vertical timeline of that day (see below), whose **+** button starts a new record
471
- prefilled to that day.
472
-
473
- ```json
474
- {
475
- "title": "Events",
476
- "icon": "event",
477
- "dataPath": "data/events/items",
478
- "primaryKey": "id",
479
- "fields": {
480
- "id": { "type": "string", "label": "ID", "primary": true, "required": true },
481
- "name": { "type": "string", "label": "Name", "required": true },
482
- "on": { "type": "date", "label": "Date", "required": true },
483
- "until": { "type": "date", "label": "End" }
484
- },
485
- "displayField": "name",
486
- "calendarField": "on",
487
- "calendarEndField": "until"
488
- }
489
- ```
490
-
491
- Notes:
492
-
493
- - **No schema change is needed to get the toggle** — it appears whenever a `date`
494
- field exists. The two keys only *tune* it: `calendarField` pins which date
495
- anchors the grid (otherwise the first `date` field is used, and the user can
496
- switch in-view when there are several); `calendarEndField` makes a record span
497
- multiple days (`calendarField` → `calendarEndField`, inclusive).
498
- - `displayField` sets the chip label (falls back to the primary key).
499
- - Records whose anchor date is missing or unparseable are listed in a small
500
- "No date" tray under the grid — never silently dropped.
501
- - The calendar is purely a **rendering** of the records: it adds no storage and
502
- fires nothing. It composes with `triggerField` / `spawn` (which drive bells and
503
- recurrence) but is independent of them.
504
- - This is the collection-native calendar — the way to give the user a
505
- calendar of dated records. (The old standalone Calendar view +
506
- `manageCalendar` tool were removed; `calendarField` is its replacement.)
507
-
508
- #### Day view (time allocation)
509
-
510
- Clicking a day's number badge opens a popup vertical timeline (a 24-hour grid)
511
- showing how that day's records are allocated across the clock. Records need a
512
- **time of day** to draw as time blocks; supply it one of two ways:
513
-
514
- - A `datetime` `calendarField` (and optionally `calendarEndField`) — the clock
515
- comes from the field value itself (`2026-06-11T14:00`).
516
- - A `date` `calendarField` **plus** `calendarTimeField` naming a string field
517
- with a free-form time or range. Recognised shapes:
518
-
519
- | Time value | Day-view rendering |
520
- |---|---|
521
- | `"14:00-17:00"` (a range) | a proportional **time block** from 14:00 to 17:00 |
522
- | `"17:00-"` or `"16:30"` (start only) | a **single line** at that time (no known end) |
523
- | `"終日"`, blank, or unparseable | a chip in the **all-day strip** at the bottom |
524
-
525
- Separators `-`, `–`, `—`, `~`, `〜`, `~` are all accepted. Overlapping blocks
526
- split into side-by-side lanes; a multi-day `datetime` span is clamped to each
527
- day with ▲/▼ arrows marking where it continues. Example (`date` + `time`
528
- column):
529
-
530
- ```json
531
- {
532
- "title": "Engagements",
533
- "icon": "event",
534
- "dataPath": "data/engagements/items",
535
- "primaryKey": "id",
536
- "fields": {
537
- "id": { "type": "string", "label": "ID", "primary": true, "required": true },
538
- "title": { "type": "string", "label": "Title", "required": true },
539
- "date": { "type": "date", "label": "Date", "required": true },
540
- "time": { "type": "string", "label": "Time" }
541
- },
542
- "displayField": "title",
543
- "calendarField": "date",
544
- "calendarTimeField": "time"
545
- }
546
- ```
547
-
548
- ### Kanban view
549
-
550
- Any collection that has at least one `enum` field gains a **Kanban board** toggle
551
- in its header — **zero config**. The board renders one column per declared enum
552
- value (in `values` order), plus a trailing **Uncategorized** column for
553
- empty/unknown values (omitted when the group enum is `required`). Dragging a card
554
- between columns writes that enum field; clicking a card opens the same
555
- detail/edit panel.
556
-
557
- ```json
558
- {
559
- "title": "Tasks",
560
- "icon": "checklist",
561
- "dataPath": "data/tasks/items",
562
- "primaryKey": "id",
563
- "fields": {
564
- "id": { "type": "string", "label": "ID", "primary": true, "required": true },
565
- "title": { "type": "string", "label": "Title", "required": true },
566
- "status": { "type": "enum", "label": "Status", "values": ["Backlog", "Todo", "In Progress", "Done"] },
567
- "done": { "type": "toggle", "label": "Done", "field": "status", "onValue": "Done", "offValue": "Todo" }
568
- },
569
- "displayField": "title",
570
- "kanbanField": "status"
571
- }
572
- ```
573
-
574
- Notes:
575
-
576
- - **No schema change is needed to get the toggle** — it appears whenever an
577
- `enum` field exists. `kanbanField` only *tunes* which enum groups the board
578
- (otherwise the first `enum` field is used, switchable in-view).
579
- - **The enum is the single source of truth.** For a todo-style "done" checkbox,
580
- use a `toggle` field projecting the status enum (above) — do NOT add a separate
581
- stored boolean. Checking the box sets `status` to the done value (and moves the
582
- card to that column); dragging the card to the Done column checks the box. They
583
- are the same write.
584
- - Columns are not draggable (order comes from the enum's `values`) and there is
585
- no manual ordering within a column — a drop only changes the enum value.
586
- - Like the calendar, the board is purely a **rendering** of the records: it adds
587
- no storage. `completionField` / `completionDoneValues` (bells) are independent
588
- but pair naturally with the Done column.
589
- - **Building a todo / task list?** Read `config/helps/todo-collection.md` — the
590
- complete, copy-pasteable recipe (status enum + `done` toggle + priority bells +
591
- calendar) plus the legacy-`todo-plugin` migration steps.
592
-
593
- ### Worked example: a Todo list
594
-
595
- The full todo recipe — complete `schema.json`, `SKILL.md`, a sample record, and
596
- the legacy-`todo-plugin` migration steps — has its own file:
597
- **`config/helps/todo-collection.md`**. Read it whenever you create or migrate a
598
- todo / task list; it's the canonical template, so copy it rather than assembling
599
- one from the fragments above. The one rule to remember: the `status` enum is the
600
- single source of truth and the "done" checkbox is a `toggle` field projecting it
601
- — **omit the toggle and the list has no checkbox.**
602
-
603
- ## Records — one JSON object per file
604
-
605
- - Write each record to `<dataPath>/<id>.json` via the **Write** tool; the `id`
606
- field's value is the filename (no extension).
607
- - **List the directory first** and pick a fresh id rather than silently
608
- overwriting. Update = Read, merge, Write back (preserve fields you weren't
609
- asked to change). Delete = remove the file.
610
- - **Never write `derived` fields**, and never write an `embed` field — both are
611
- display-only / host-computed.
612
- - Leave optional fields out of the JSON entirely rather than writing empty
613
- strings.
614
- - For a `ref` field, write the raw target slug, and make sure that record
615
- actually exists in the target collection — an invalid slug renders as a broken
616
- link. The host enforces structure and safety; **you own semantic correctness**
617
- (valid refs, sane values).
618
-
619
- ## End-to-end: creating a new collection skill
620
-
621
- 1. Pick a `<slug>` (lowercase-hyphen, no `mc-` prefix) and a `dataPath`
622
- (`data/<name>/items`).
623
- 2. Write `data/skills/<slug>/schema.json` — `title`, `icon`, `dataPath`,
624
- `primaryKey` (with the matching field flagged `primary: true`), and the
625
- `fields` map in the order you want columns. Add `actions` +
626
- `data/skills/<slug>/templates/*.md` only if the collection needs delegated
627
- behaviour. (The bridge mirrors these into `.claude/skills/<slug>/`.)
628
- 3. Write `data/skills/<slug>/SKILL.md` — front-matter `name` + `description`,
629
- then the record-shape bullets and CRUD conventions.
630
- 4. Tell the user it's ready at `/collections/<slug>`. The bridge mirrors the
631
- files and triggers a re-scan, so the host discovers it without a restart and
632
- with no host code. If it doesn't appear: first confirm you wrote under
633
- `data/skills/<slug>/` (NOT `.claude/skills/…`, which is gated and won't
634
- mirror); then check your `schema.json` passed validation — primary key
635
- flagged `primary: true`, `ref`/`embed` have a valid `to`, `enum` has
636
- `values`, `table` has `of`, `derived` has `formula`, action ids unique,
637
- `dataPath` under the workspace, `triggerField` names a real `date` field and
638
- has the completion pair, `spawn` has `triggerField` and a valid `every`,
639
- `calendarField` / `calendarEndField` name real `date`/`datetime` fields (and
640
- `calendarEndField` requires `calendarField`), `calendarTimeField` names a real
641
- field and requires `calendarField`, `kanbanField` names a real
642
- `enum` field, any `toggle` field names a real `enum` `field` with its
643
- `onValue` / `offValue` among that enum's `values`, and `notifyWhen` (if set)
644
- requires `completionField` and names a real field.
645
- (A schema that fails validation is logged server-side and silently skipped
646
- at discovery.)
647
-
648
- ## Worked reference: the billing suite
649
-
650
- The billing collections are the canonical examples. They ship as **recipes**
651
- (copy-paste schemas + SKILL bodies), not as boot-overwritten presets — read the
652
- recipe when the user wants any of them, and copy the schema verbatim:
653
-
654
- - **`config/helps/billing-clients-worklog.md`** (Bundle A):
655
- - **`clients`** — flat table (`string` / `email` / `text` / `markdown`). The
656
- simplest possible collection; everything else `ref`s into it.
657
- - **`worklog`** — adds a `ref` (`clientId → clients`), a `date`, a `number`, a
658
- `boolean`. A companion data source.
659
- - **`config/helps/billing-invoice.md`** (Bundle B):
660
- - **`profile`** — a `singleton` (one record, id `me`): the issuer identity.
661
- - **`invoice`** — the full toolkit in one schema: an `embed` issuer
662
- (`profile/me`), a `ref` client (`clients`), a `table` of line items, three
663
- `derived` money fields, an `enum` status, and four `actions` (PDF always-on;
664
- sale / payment / void gated by `status` via `when`).