@mulmoclaude/core 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (122) hide show
  1. package/assets/helps/billing-clients-worklog.md +215 -0
  2. package/assets/helps/billing-invoice.md +458 -0
  3. package/assets/helps/business.md +104 -0
  4. package/assets/helps/collection-skills.md +810 -0
  5. package/assets/helps/custom-view.md +433 -0
  6. package/assets/helps/feeds.md +114 -0
  7. package/assets/helps/gemini.md +57 -0
  8. package/assets/helps/github.md +23 -0
  9. package/assets/helps/guide.md +61 -0
  10. package/assets/helps/index.md +89 -0
  11. package/assets/helps/lessons-collection.md +400 -0
  12. package/assets/helps/mulmoscript.md +249 -0
  13. package/assets/helps/portfolio-tracker.md +211 -0
  14. package/assets/helps/presentation-deck.md +828 -0
  15. package/assets/helps/presenthtml.md +89 -0
  16. package/assets/helps/sandbox.md +97 -0
  17. package/assets/helps/spreadsheet.md +43 -0
  18. package/assets/helps/storyteller.md +101 -0
  19. package/assets/helps/telegram.md +136 -0
  20. package/assets/helps/todo-collection.md +140 -0
  21. package/assets/helps/vocabulary.md +109 -0
  22. package/assets/helps/wiki.md +168 -0
  23. package/assets/skills-preset/mc-cooking-coach/SKILL.md +217 -0
  24. package/assets/skills-preset/mc-library/SKILL.md +188 -0
  25. package/assets/skills-preset/mc-manage-automations/SKILL.md +119 -0
  26. package/assets/skills-preset/mc-manage-skills/SKILL.md +141 -0
  27. package/assets/skills-preset/mc-wiki-deep-lint/SKILL.md +108 -0
  28. package/assets/skills-preset/mc-wiki-health-check/SKILL.md +61 -0
  29. package/assets/skills-preset/mc-wiki-ingest/SKILL.md +182 -0
  30. package/assets/skills-preset/mc-wiki-promote/SKILL.md +175 -0
  31. package/assets/skills-preset/mc-zenn/SKILL.md +136 -0
  32. package/dist/chunk-CKQMccvm.cjs +28 -0
  33. package/dist/collection/core/actionVisible.d.ts +34 -0
  34. package/dist/collection/core/calendarGrid.d.ts +120 -0
  35. package/dist/collection/core/deriveAll.d.ts +38 -0
  36. package/dist/collection/core/derivedFormula.d.ts +18 -0
  37. package/dist/collection/core/draft.d.ts +18 -0
  38. package/dist/collection/core/enumColors.d.ts +33 -0
  39. package/dist/collection/core/errorMessage.d.ts +4 -0
  40. package/dist/collection/core/itemLabel.d.ts +12 -0
  41. package/dist/collection/core/presentCollection.d.ts +13 -0
  42. package/dist/collection/core/promptSafety.d.ts +1 -0
  43. package/dist/collection/core/schema.d.ts +355 -0
  44. package/dist/collection/core/shortHexId.d.ts +8 -0
  45. package/dist/collection/core/sortItems.d.ts +29 -0
  46. package/dist/collection/core/uiTypes.d.ts +106 -0
  47. package/dist/collection/index.cjs +793 -0
  48. package/dist/collection/index.cjs.map +1 -0
  49. package/dist/collection/index.d.ts +14 -0
  50. package/dist/collection/index.js +740 -0
  51. package/dist/collection/index.js.map +1 -0
  52. package/dist/collection/paths.cjs +44 -0
  53. package/dist/collection/paths.cjs.map +1 -0
  54. package/dist/collection/paths.js +41 -0
  55. package/dist/collection/paths.js.map +1 -0
  56. package/dist/collection/server/atomic.d.ts +1 -0
  57. package/dist/collection/server/delete.d.ts +38 -0
  58. package/dist/collection/server/derive.d.ts +8 -0
  59. package/dist/collection/server/discoveredCollection.d.ts +18 -0
  60. package/dist/collection/server/discovery.d.ts +227 -0
  61. package/dist/collection/server/host.d.ts +77 -0
  62. package/dist/collection/server/index.cjs +1721 -0
  63. package/dist/collection/server/index.cjs.map +1 -0
  64. package/dist/collection/server/index.d.ts +11 -0
  65. package/dist/collection/server/index.js +1671 -0
  66. package/dist/collection/server/index.js.map +1 -0
  67. package/dist/collection/server/io.d.ts +114 -0
  68. package/dist/collection/server/paths.d.ts +52 -0
  69. package/dist/collection/server/spawn.d.ts +55 -0
  70. package/dist/collection/server/templatePath.d.ts +25 -0
  71. package/dist/collection/server/util.d.ts +3 -0
  72. package/dist/collection/server/validate.d.ts +19 -0
  73. package/dist/collection/server/views.d.ts +20 -0
  74. package/dist/deriveAll-C15OpM3K.cjs +399 -0
  75. package/dist/deriveAll-C15OpM3K.cjs.map +1 -0
  76. package/dist/deriveAll-C6BYnpBL.js +364 -0
  77. package/dist/deriveAll-C6BYnpBL.js.map +1 -0
  78. package/dist/file-change/index.cjs +72 -0
  79. package/dist/file-change/index.cjs.map +1 -0
  80. package/dist/file-change/index.d.ts +43 -0
  81. package/dist/file-change/index.js +66 -0
  82. package/dist/file-change/index.js.map +1 -0
  83. package/dist/notifier/engine.d.ts +72 -0
  84. package/dist/notifier/index.cjs +484 -0
  85. package/dist/notifier/index.cjs.map +1 -0
  86. package/dist/notifier/index.d.ts +3 -0
  87. package/dist/notifier/index.js +464 -0
  88. package/dist/notifier/index.js.map +1 -0
  89. package/dist/notifier/store.d.ts +18 -0
  90. package/dist/notifier/types.d.ts +118 -0
  91. package/dist/notifier/validate.d.ts +17 -0
  92. package/dist/scheduler/adapter.d.ts +48 -0
  93. package/dist/scheduler/index.cjs +352 -0
  94. package/dist/scheduler/index.cjs.map +1 -0
  95. package/dist/scheduler/index.d.ts +2 -0
  96. package/dist/scheduler/index.js +343 -0
  97. package/dist/scheduler/index.js.map +1 -0
  98. package/dist/scheduler/task-manager.d.ts +51 -0
  99. package/dist/whisper/client.cjs +241 -0
  100. package/dist/whisper/client.cjs.map +1 -0
  101. package/dist/whisper/client.d.ts +35 -0
  102. package/dist/whisper/client.js +239 -0
  103. package/dist/whisper/client.js.map +1 -0
  104. package/dist/whisper/ffmpeg.d.ts +6 -0
  105. package/dist/whisper/index.cjs +433 -0
  106. package/dist/whisper/index.cjs.map +1 -0
  107. package/dist/whisper/index.d.ts +5 -0
  108. package/dist/whisper/index.js +425 -0
  109. package/dist/whisper/index.js.map +1 -0
  110. package/dist/whisper/internal.d.ts +11 -0
  111. package/dist/whisper/models.d.ts +49 -0
  112. package/dist/whisper/sidecar.d.ts +8 -0
  113. package/dist/whisper/whisper.d.ts +28 -0
  114. package/dist/workspace-setup/assets.d.ts +10 -0
  115. package/dist/workspace-setup/index.d.ts +3 -0
  116. package/dist/workspace-setup/index.js +556 -0
  117. package/dist/workspace-setup/index.js.map +1 -0
  118. package/dist/workspace-setup/slug.d.ts +6 -0
  119. package/dist/workspace-setup/slug.js +13 -0
  120. package/dist/workspace-setup/slug.js.map +1 -0
  121. package/dist/workspace-setup/sync.d.ts +94 -0
  122. package/package.json +95 -0
@@ -0,0 +1,249 @@
1
+ # MulmoScript
2
+
3
+ MulmoScript is a JSON/YAML format for authoring multimedia stories — narrated slideshows that can be rendered as video. Each script describes a sequence of **beats** (slides), each with a speaker, narration text, and a visual element.
4
+
5
+ MulmoScript files are rendered in the canvas. The underlying engine is [mulmocast](https://github.com/receptron/mulmocast).
6
+
7
+ ## Provider Note
8
+
9
+ **Always use Google providers** for all generation in this app:
10
+
11
+ | Purpose | Provider | Example config |
12
+ |---|---|---|
13
+ | TTS (speech) | `gemini` | `"provider": "gemini", "voiceId": "Kore"` |
14
+ | Image generation | `google` | `"provider": "google", "model": "gemini-3.1-flash-image-preview"` |
15
+ | Video generation | `google` | `"provider": "google", "model": "veo-3.1-generate"` |
16
+
17
+ Do not use `openai`, `elevenlabs`, or other providers — they are not configured in this app.
18
+
19
+ ## Top-Level Structure
20
+
21
+ ```json
22
+ {
23
+ "$mulmocast": { "version": "1.1" },
24
+ "title": "My Story",
25
+ "lang": "en",
26
+ "canvasSize": { "width": 1280, "height": 720 },
27
+ "speechParams": { ... },
28
+ "imageParams": { ... },
29
+ "audioParams": { ... },
30
+ "beats": [ ... ]
31
+ }
32
+ ```
33
+
34
+ | Field | Required | Description |
35
+ |---|---|---|
36
+ | `$mulmocast` | Yes | Header — `version` must be `"1.1"` |
37
+ | `beats` | Yes | Array of beats (slides) |
38
+ | `title` | No | Script title |
39
+ | `description` | No | Short description |
40
+ | `lang` | No | Default language code (e.g. `"en"`, `"ja"`) |
41
+ | `canvasSize` | No | Pixel dimensions, default 1280×720 |
42
+ | `speechParams` | No | TTS speaker configuration |
43
+ | `imageParams` | No | Image generation configuration |
44
+ | `movieParams` | No | Video rendering and filter options |
45
+ | `audioParams` | No | BGM, volume, padding, ducking |
46
+ | `captionParams` | No | Caption language and style |
47
+ | `slideParams` | No | Global slide theme (colors, fonts, branding) |
48
+ | `references` | No | Source citations `[{ url, title?, description?, type? }]` |
49
+
50
+ ## Beats
51
+
52
+ Each beat is one slide in the presentation.
53
+
54
+ ```json
55
+ {
56
+ "speaker": "Presenter",
57
+ "text": "Welcome to this story.",
58
+ "image": { "type": "markdown", "markdown": "# Hello World" }
59
+ }
60
+ ```
61
+
62
+ | Field | Description |
63
+ |---|---|
64
+ | `speaker` | Speaker name (must match a key in `speechParams.speakers`) |
65
+ | `text` | Narration text read aloud by TTS |
66
+ | `texts` | Alternative: array of text segments |
67
+ | `id` | Optional identifier for cross-referencing |
68
+ | `description` | Internal note, not rendered |
69
+ | `image` | Visual content — one of the media types below |
70
+
71
+ ## Image (Visual) Types
72
+
73
+ ### `markdown`
74
+ Render Markdown as a slide image.
75
+
76
+ ```json
77
+ { "type": "markdown", "markdown": "## My Slide\n- Point one\n- Point two" }
78
+ ```
79
+
80
+ Supports layout variants:
81
+ - **Simple**: `"markdown": "# Title\nContent..."` (string or string array)
82
+ - **Content**: `"markdown": { "content": "..." }`
83
+ - **Two columns**: `"markdown": { "row-2": ["Left content", "Right content"] }`
84
+ - **2×2 grid**: `"markdown": { "2x2": ["A", "B", "C", "D"] }`
85
+ - **With header/sidebar**: add `"header"` and/or `"sidebar-left"` keys
86
+
87
+ Optional: `style` (CSS string), `backgroundImage`.
88
+
89
+ ### `slide`
90
+ Structured slide with a typed layout system and theming.
91
+
92
+ ```json
93
+ {
94
+ "type": "slide",
95
+ "slide": {
96
+ "layout": "title",
97
+ "title": "My Presentation",
98
+ "subtitle": "A subtitle"
99
+ }
100
+ }
101
+ ```
102
+
103
+ **Layouts**: `"title"`, `"columns"`
104
+
105
+ Column content item types: `text`, `bullets`, `code`, `callout`, `metric`, `divider`, `image`, `imageRef`, `chart`, `mermaid`, `table`
106
+
107
+ Optional: per-slide `theme` override.
108
+
109
+ ### `textSlide`
110
+ Simple title + bullets slide.
111
+
112
+ ```json
113
+ {
114
+ "type": "textSlide",
115
+ "slide": { "title": "Key Points", "subtitle": "Optional", "bullets": ["One", "Two"] }
116
+ }
117
+ ```
118
+
119
+ ### `image`
120
+ Embed an existing image.
121
+
122
+ ```json
123
+ { "type": "image", "source": { "kind": "url", "url": "https://..." } }
124
+ { "type": "image", "source": { "kind": "path", "path": "assets/photo.png" } }
125
+ ```
126
+
127
+ Source kinds: `url`, `base64`, `path`
128
+
129
+ ### `chart`
130
+ Render a chart from data.
131
+
132
+ ```json
133
+ { "type": "chart", "title": "Sales", "chartData": { "type": "bar", "data": { ... } } }
134
+ ```
135
+
136
+ ### `mermaid`
137
+ Render a Mermaid diagram.
138
+
139
+ ```json
140
+ {
141
+ "type": "mermaid",
142
+ "title": "System Flow",
143
+ "code": { "kind": "text", "text": "graph TD\n A --> B" }
144
+ }
145
+ ```
146
+
147
+ Code source kinds: `url`, `base64`, `text`, `path`
148
+
149
+ ### `html_tailwind`
150
+ Custom HTML + Tailwind CSS slide.
151
+
152
+ ```json
153
+ {
154
+ "type": "html_tailwind",
155
+ "html": "<div class='text-4xl font-bold'>Hello</div>",
156
+ "animation": true
157
+ }
158
+ ```
159
+
160
+ Optional: `script` (JS), `elements` (swipe elements), `animation` (`true` or `{ fps, movie? }`).
161
+
162
+ ### `web`
163
+ Embed a web page.
164
+
165
+ ```json
166
+ { "type": "web", "url": "https://example.com" }
167
+ ```
168
+
169
+ ### `pdf` / `svg` / `movie`
170
+ Embed a file by source (url/base64/path).
171
+
172
+ ### `moviePrompt`
173
+ Generate a video clip from a text prompt.
174
+
175
+ ```json
176
+ { "type": "moviePrompt", "prompt": "A sunset over the ocean" }
177
+ ```
178
+
179
+ ## speechParams
180
+
181
+ Configure TTS voices per speaker.
182
+
183
+ ```json
184
+ {
185
+ "speechParams": {
186
+ "speakers": {
187
+ "Presenter": {
188
+ "provider": "gemini",
189
+ "voiceId": "Kore",
190
+ "isDefault": true,
191
+ "displayName": { "en": "Presenter" }
192
+ }
193
+ }
194
+ }
195
+ }
196
+ ```
197
+
198
+ Each speaker supports per-language voice overrides via a `lang` map.
199
+
200
+ ## audioParams
201
+
202
+ Control BGM and volume mixing.
203
+
204
+ ```json
205
+ {
206
+ "audioParams": {
207
+ "bgm": { "kind": "path", "path": "music/theme.mp3" },
208
+ "bgmVolume": 0.3,
209
+ "audioVolume": 1.0,
210
+ "padding": 0.5,
211
+ "suppressSpeech": false
212
+ }
213
+ }
214
+ ```
215
+
216
+ ## Minimal Example
217
+
218
+ ```json
219
+ {
220
+ "$mulmocast": { "version": "1.1" },
221
+ "title": "Hello World",
222
+ "lang": "en",
223
+ "speechParams": {
224
+ "speakers": {
225
+ "Presenter": { "provider": "gemini", "voiceId": "Kore", "displayName": { "en": "Presenter" } }
226
+ }
227
+ },
228
+ "imageParams": { "provider": "google", "model": "gemini-3.1-flash-image-preview" },
229
+ "beats": [
230
+ {
231
+ "speaker": "Presenter",
232
+ "text": "Welcome to my story.",
233
+ "image": {
234
+ "type": "textSlide",
235
+ "slide": { "title": "Hello World", "bullets": ["Simple", "Clear", "Visual"] }
236
+ }
237
+ },
238
+ {
239
+ "speaker": "Presenter",
240
+ "text": "Here is a diagram of the process.",
241
+ "image": {
242
+ "type": "mermaid",
243
+ "title": "Process Flow",
244
+ "code": { "kind": "text", "text": "graph LR\n A[Start] --> B[Process] --> C[End]" }
245
+ }
246
+ }
247
+ ]
248
+ }
249
+ ```
@@ -0,0 +1,211 @@
1
+ # Portfolio tracker — the stock-quotes + portfolio recipe
2
+
3
+ Read this when the user asks to **set up a stock portfolio tracker / track their
4
+ investments / "value my holdings"** (sample query: *"Set up a stock portfolio
5
+ tracker"*). It scaffolds **two** collection skills that work as a pair:
6
+
7
+ - **`stock-quotes`** — a watchlist of public-equity quotes (price, P/E, yield).
8
+ Prices come from Yahoo Finance (15-minute delayed); the agent fetches them.
9
+ - **`portfolio`** — the user's holdings (ticker + share count). Each holding's
10
+ **price and value are computed live** from the matching `stock-quotes` row via
11
+ a cross-collection `ref` + `derived` formula, so one quote refresh updates every
12
+ holding that references it.
13
+
14
+ This is the canonical **`<refField>.<col>` cross-collection lookup** pattern (see
15
+ `config/helps/collection-skills.md`): `portfolio.value = shares * ticker.price`
16
+ reads `price` off the `stock-quotes` record the holding's `ticker` points at —
17
+ the price lives **only** in `stock-quotes`; the portfolio never stores a copy.
18
+
19
+ Read `config/helps/collection-skills.md` first for the general schema DSL. Author
20
+ everything under `data/skills/<slug>/` (the bridge mirrors it to
21
+ `.claude/skills/<slug>/`; the user opens it at `/collections/<slug>`). **Do not
22
+ use the `mc-` prefix.**
23
+
24
+ > **Follow this recipe verbatim — do NOT redesign.** The schemas below are fixed
25
+ > and known-good. Write them exactly as given (you may only adjust `title`/`icon`
26
+ > if the user explicitly asks). Do **not** add fields, do **not** call
27
+ > `presentForm` to ask design questions, and do **not** mimic other collections
28
+ > in the workspace. The whole point is a reproducible, working pair. (For a
29
+ > *custom* collection, use `config/helps/collection-skills.md` instead.) If the
30
+ > user already has `data/stock-quotes/items` / `data/portfolio/items` records,
31
+ > they match these schemas and will render as-is — no data edits needed.
32
+
33
+ ## Slug contract (do not change these)
34
+
35
+ `portfolio.ticker` references `stock-quotes` by this exact slug, and
36
+ `portfolio.value` dereferences its `price` column. Author with **exactly** these
37
+ slugs so the cross-collection lookup resolves:
38
+
39
+ | Collection | slug | `dataPath` | referenced by |
40
+ |---|---|---|---|
41
+ | Stock Quotes | `stock-quotes` | `data/stock-quotes/items` | `portfolio.ticker` (ref), `portfolio.{price,value}` (derived) |
42
+ | Portfolio | `portfolio` | `data/portfolio/items` | — |
43
+
44
+ ## Order: `stock-quotes` before `portfolio`
45
+
46
+ `portfolio.ticker` is a `ref` to `stock-quotes` and `portfolio.price`/`value`
47
+ dereference its `price` column, so create `stock-quotes` first (and add the rows
48
+ for the tickers the user holds). A holding whose `ticker` has no `stock-quotes`
49
+ row renders `price`/`value` as `—` (fail-soft) until the quote exists.
50
+
51
+ ---
52
+
53
+ ## 1. `stock-quotes`
54
+
55
+ `data/skills/stock-quotes/schema.json`:
56
+
57
+ ```json
58
+ {
59
+ "title": "Stock Quotes",
60
+ "icon": "trending_up",
61
+ "dataPath": "data/stock-quotes/items",
62
+ "primaryKey": "id",
63
+ "fields": {
64
+ "id": { "type": "string", "label": "ID", "primary": true, "required": true },
65
+ "ticker": { "type": "string", "label": "Ticker", "required": true },
66
+ "companyName": { "type": "string", "label": "Company", "required": true },
67
+ "price": { "type": "number", "label": "Latest Price" },
68
+ "currency": { "type": "string", "label": "Currency" },
69
+ "peRatio": { "type": "number", "label": "P/E Ratio" },
70
+ "dividendYield": { "type": "number", "label": "Dividend Yield (%)" },
71
+ "asOf": { "type": "string", "label": "As Of" },
72
+ "notes": { "type": "markdown", "label": "Notes" }
73
+ }
74
+ }
75
+ ```
76
+
77
+ `data/skills/stock-quotes/SKILL.md`:
78
+
79
+ ```markdown
80
+ ---
81
+ name: stock-quotes
82
+ description: A simple stock-quote watchlist. Use whenever the user asks to add, list, update, or remove a stock quote, or to look up the latest price / PE ratio / dividend yield for a ticker. Records live at `data/stock-quotes/items/<ticker>.json` (one JSON per ticker, lowercase id); the user views them at `/collections/stock-quotes`, rendered from `schema.json` by the host. Record I/O via the `manageCollection` tool (getItems / putItems; raw Read / Write / Edit on the JSON files is the escape hatch). Price data comes from Yahoo Finance (15-minute delayed).
83
+ ---
84
+
85
+ # Stock Quotes (schema-driven collection)
86
+
87
+ A lightweight watchlist of public-equity quotes.
88
+
89
+ ## Record shape
90
+
91
+ - `id` — string, **primary key**, lowercase ticker symbol (e.g. `aapl`, `msft`). Filename without extension.
92
+ - `ticker` — ticker symbol in upstream casing (e.g. `AAPL`) — **required**
93
+ - `companyName` — company long name (e.g. `Apple Inc.`) — **required**
94
+ - `price` — latest stock price (number, in the listed currency)
95
+ - `currency` — ISO currency code of the price (e.g. `USD`)
96
+ - `peRatio` — trailing P/E ratio (number). Omit if the company has no/negative earnings.
97
+ - `dividendYield` — trailing dividend yield as a percent (number, e.g. `0.42` means 0.42%). Omit if no dividend.
98
+ - `asOf` — ISO-8601 timestamp of when the quote was fetched
99
+ - `notes` — markdown notes (optional)
100
+
101
+ Omit fields the user didn't supply or that Yahoo Finance didn't return — don't write empty strings.
102
+
103
+ ## What to do
104
+
105
+ **Add / refresh**: Fetch from Yahoo Finance (`https://query1.finance.yahoo.com/v7/finance/quote?symbols=<SYM>` or `quoteSummary` with `summaryDetail,price,defaultKeyStatistics`). Build the records and store them in one call — `manageCollection` `putItems` with `slug: "stock-quotes"` validates each row against the schema before writing; fix any `rejected` row using its `problem` text and retry just those. Upserting an existing id is the intended way to refresh a quote.
106
+
107
+ **List**: `manageCollection` `getItems` with `slug: "stock-quotes"` (use `fields` to keep it small). Don't dump every record into chat — link to `/collections/stock-quotes`.
108
+
109
+ **Update**: `putItems` with `mode: "merge"` and a partial row (`{ id, <changed fields> }`) — it keeps every field the row omits. Always update `asOf`.
110
+
111
+ **Delete**: Confirm once if ambiguous, then remove the file (`data/stock-quotes/items/<id>.json`).
112
+
113
+ ## Linking from chat
114
+
115
+ When referring to a specific ticker, link to the collection view, not the raw JSON:
116
+
117
+ - ✅ `[AAPL](/collections/stock-quotes?selected=aapl)`
118
+ - ❌ `[AAPL](data/stock-quotes/items/aapl.json)`
119
+
120
+ `?selected=<id>` opens the detail view directly.
121
+
122
+ ## Citation discipline
123
+
124
+ Always tell the user when the price was fetched and that it's typically 15-minute delayed. Don't paraphrase fundamentals (PE, yield) without anchoring to the data source.
125
+ ```
126
+
127
+ ---
128
+
129
+ ## 2. `portfolio`
130
+
131
+ `data/skills/portfolio/schema.json` (`ticker` is a `ref` **to `stock-quotes`**;
132
+ `price` / `value` are `derived` cross-collection lookups):
133
+
134
+ ```json
135
+ {
136
+ "title": "My Portfolio",
137
+ "icon": "account_balance_wallet",
138
+ "dataPath": "data/portfolio/items",
139
+ "primaryKey": "id",
140
+ "fields": {
141
+ "id": { "type": "string", "label": "ID", "primary": true, "required": true },
142
+ "ticker": { "type": "ref", "to": "stock-quotes", "label": "Stock", "required": true },
143
+ "shares": { "type": "number", "label": "Shares", "required": true },
144
+ "price": { "type": "derived", "label": "Latest Price", "formula": "ticker.price", "display": "money", "currency": "USD" },
145
+ "value": { "type": "derived", "label": "Value", "formula": "shares * ticker.price", "display": "money", "currency": "USD" },
146
+ "notes": { "type": "markdown", "label": "Notes" }
147
+ }
148
+ }
149
+ ```
150
+
151
+ `data/skills/portfolio/SKILL.md`:
152
+
153
+ ```markdown
154
+ ---
155
+ name: portfolio
156
+ description: A personal stock portfolio. Use whenever the user asks to add, list, edit, or remove a holding, or to see the value of their portfolio. Each record is one holding (ticker + share count); the latest price is pulled live from the `stock-quotes` collection via a derived ref, so the holding's value updates whenever the quote is refreshed. Records live at `data/portfolio/items/<id>.json`; the user views them at `/collections/portfolio`, rendered from `schema.json` by the host. Record I/O via the `manageCollection` tool — its getItems is the ONLY way to read the computed `price` / `value` columns (the stored JSON never contains them).
157
+ ---
158
+
159
+ # My Portfolio (schema-driven collection)
160
+
161
+ A lightweight book of equity holdings. Each row records *how many shares* of a ticker the user owns; the *price* and *value* columns are computed on the fly from the matching `stock-quotes` record, so a single quote refresh updates every holding that references it.
162
+
163
+ ## Record shape
164
+
165
+ - `id` — string, **primary key**, lowercase ticker symbol (e.g. `aapl`, `tsla`). Filename without extension.
166
+ - `ticker` — **ref → stock-quotes**, stores the lowercase target slug (e.g. `aapl`). The matching row MUST already exist in `stock-quotes`, otherwise `price` and `value` render as `—`.
167
+ - `shares` — number, share count (required).
168
+ - `price` — **derived**, `ticker.price`. Host-computed; **do NOT write this field**.
169
+ - `value` — **derived**, `shares * ticker.price`. Host-computed; **do NOT write this field**.
170
+ - `notes` — markdown notes (optional).
171
+
172
+ Omit fields the user didn't supply — don't write empty strings. Never write `price` or `value` into the JSON; the host recomputes them on every render and the form refuses to persist them.
173
+
174
+ ## What to do
175
+
176
+ **Add a holding**: confirm the ticker has a row in `/collections/stock-quotes`; if not, add it first via the `stock-quotes` skill (so `ticker.price` resolves). Then `manageCollection` `putItems` with `slug: "portfolio"` and a row carrying `id`, `ticker`, `shares` (and optional `notes`) — never `price` / `value`; putItems rejects computed keys.
177
+
178
+ **List / value the portfolio**: `manageCollection` `getItems` with `slug: "portfolio"` — the returned records INCLUDE the host-computed `price` and `value`, so total the `value` column from there (reading the raw JSON files would show neither). Don't dump every record into chat — link to `/collections/portfolio`.
179
+
180
+ **Update shares**: `putItems` with `mode: "merge"` and `{ "id": "<id>", "shares": <n> }` — merge keeps the fields the row omits.
181
+
182
+ **Refresh prices**: don't touch the portfolio records — go refresh the matching row in `stock-quotes` instead. The portfolio's `price` and `value` columns update on the next render.
183
+
184
+ **Delete**: Confirm once if ambiguous, then remove the file (`data/portfolio/items/<id>.json`).
185
+
186
+ ## Linking from chat
187
+
188
+ When referring to a specific holding, link to the collection view, not the raw JSON:
189
+
190
+ - ✅ `[AAPL holding](/collections/portfolio?selected=aapl)`
191
+ - ❌ `[AAPL holding](data/portfolio/items/aapl.json)`
192
+
193
+ `?selected=<id>` opens the detail view directly.
194
+ ```
195
+
196
+ ---
197
+
198
+ ## How prices stay current
199
+
200
+ Prices are **not** auto-fetched. The agent populates / refreshes `stock-quotes`
201
+ by fetching Yahoo Finance (see the `stock-quotes` SKILL above) — e.g. when adding
202
+ a holding or when the user asks to refresh. `portfolio.price` / `value` are pure
203
+ projections of `stock-quotes.price`, recomputed on every render, so refreshing a
204
+ quote updates every holding that references it with zero edits to the portfolio.
205
+
206
+ ## Done
207
+
208
+ Tell the user both collections are ready at `/collections/stock-quotes` and
209
+ `/collections/portfolio`. The bridge mirrors the files and re-scans, so they
210
+ appear without a restart. Offer to fetch quotes for the tickers they hold so the
211
+ `price` / `value` columns populate immediately.