@mulmoclaude/core 0.2.10 → 0.2.12

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.
@@ -3,11 +3,12 @@
3
3
  Read this when the user asks to **set up invoicing / billing** (sample query:
4
4
  *"Set up invoicing for my business"*). It scaffolds **two** collection skills:
5
5
 
6
- - **`profile`** — the user's **own** business identity (the "bill-from" block:
7
- company name, tax ID, payment details). A singleton (one record, id `me`).
8
- - **`invoice`** an invoice ledger; each invoice references a client, embeds the
9
- profile as the issuer, and carries a table of line items with host-computed
10
- subtotal / tax / total.
6
+ - **`profile`** — the user's **own** business identit(ies) (the "bill-from" block:
7
+ company name, tax ID, payment details). One or more records, one per issuer; the
8
+ primary one is id `me`.
9
+ - **`invoice`** an invoice ledger; each invoice references a client, picks an
10
+ issuer profile (`issuerId` `profile`) which it embeds as the bill-from block,
11
+ and carries a table of line items with host-computed subtotal / tax / total.
11
12
 
12
13
  This is **Bundle B** of the billing suite. It **builds on Bundle A**
13
14
  (`clients` + `worklog`, see `config/helps/billing-clients-worklog.md`):
@@ -37,7 +38,10 @@ use the `mc-` prefix.**
37
38
  > other collections in the workspace. The whole point is a reproducible billing
38
39
  > suite. (For a *custom* collection, use `config/helps/collection-skills.md`
39
40
  > instead.) Existing records under `data/invoice/items` / `data/profile/items`
40
- > already match these schemas and will render as-is no data edits needed.
41
+ > render as-is, with one exception: because `invoice.issuerId` is new and
42
+ > required, **backfill `issuerId: "me"` on any pre-existing invoice records** (a
43
+ > `manageCollection` `putItems` with `mode: "merge"`) so their issuer block
44
+ > resolves — otherwise they show a "missing issuer" prompt until set.
41
45
 
42
46
  ## Slug contract (do not change these)
43
47
 
@@ -45,7 +49,7 @@ use the `mc-` prefix.**
45
49
 
46
50
  | Collection | slug | `dataPath` | referenced by |
47
51
  |---|---|---|---|
48
- | Profile | `profile` | `data/profile/items` | `invoice.issuer` (embed `profile/me`) |
52
+ | Profile | `profile` | `data/profile/items` | `invoice.issuerId` (ref) + `invoice.issuer` (embed via `idField`) |
49
53
  | Invoice | `invoice` | `data/invoice/items` | — |
50
54
  | Clients | `clients` | `data/clients/items` | `invoice.clientId` (ref, from Bundle A) |
51
55
 
@@ -56,13 +60,15 @@ use the `mc-` prefix.**
56
60
 
57
61
  ## Order: `profile` before `invoice`
58
62
 
59
- `invoice.issuer` embeds the `profile/me` record, so create `profile` first.
63
+ `invoice.issuerId` references a `profile` record (and `issuer` embeds it), so
64
+ create `profile` first.
60
65
 
61
66
  ---
62
67
 
63
68
  ## 1. `profile`
64
69
 
65
- `data/skills/profile/schema.json` (a **singleton** exactly one record, id `me`):
70
+ `data/skills/profile/schema.json` (one record per issuer; the primary one is id
71
+ `me` — **not** a singleton, so an invoice can pick among several issuers):
66
72
 
67
73
  ```json
68
74
  {
@@ -70,7 +76,6 @@ use the `mc-` prefix.**
70
76
  "icon": "badge",
71
77
  "dataPath": "data/profile/items",
72
78
  "primaryKey": "id",
73
- "singleton": "me",
74
79
  "fields": {
75
80
  "id": { "type": "string", "label": "ID", "primary": true, "required": true },
76
81
  "companyName": { "type": "string", "label": "Company / legal name", "required": true },
@@ -90,25 +95,27 @@ use the `mc-` prefix.**
90
95
  ```markdown
91
96
  ---
92
97
  name: profile
93
- description: The user's own business profile — the issuer ("bill-from") identity
94
- used on invoices. A singleton collection with exactly one record, id `me`. The
95
- record lives at `data/profile/items/me.json`; the user views and edits it at
96
- `/collections/profile`. Record I/O via the `manageCollection` tool (raw
97
- Read / Write / Edit on the JSON file is the escape hatch).
98
+ description: The user's own business profile(s) — the issuer ("bill-from") identity
99
+ used on invoices. One or more records, each an issuer; the primary one is `me`.
100
+ Records live at `data/profile/items/<id>.json`; the user views and edits them at
101
+ `/collections/profile`. Each invoice picks one via its `issuerId` ref. Record I/O
102
+ via the `manageCollection` tool (raw Read / Write / Edit on the JSON is the escape hatch).
98
103
  ---
99
104
 
100
105
  # Business Profile (schema-driven collection)
101
106
 
102
- Holds the user's **own** business identity — the "bill-from" side of an invoice.
103
- Counterpart to `clients`, which holds the "bill-to" parties.
107
+ Holds the user's **own** business identit(ies) — the "bill-from" side of an
108
+ invoice. Counterpart to `clients`, which holds the "bill-to" parties.
104
109
 
105
- ## Singletonexactly one record, id `me`
106
- This collection has **one** record. Its primary key is always the literal string
107
- `me`, stored at `data/profile/items/me.json`. Never create a second record and
108
- never invent another id read, create, and update `me.json` only.
110
+ ## Multiple issuers `me` is the primary one
111
+ This collection can hold **several** records, one per issuer identity (a personal
112
+ name, a company, a second LLC). The primary profile keeps the id `me`, stored at
113
+ `data/profile/items/me.json`; add more with their own kebab-case ids (e.g.
114
+ `pervasive`, `personal`). An invoice points at one via its `issuerId`, so a
115
+ profile must exist before an invoice can reference it.
109
116
 
110
117
  ## Record shape
111
- - `id` — string, **primary key**, always `me`
118
+ - `id` — string, **primary key** (kebab-case slug; the primary profile is `me`)
112
119
  - `companyName` — string, **required** (the legal/company name shown on invoices)
113
120
  - `taxRegistrationId` — string (VAT / EIN / JP T-number — region-dependent)
114
121
  - `email` — email
@@ -123,18 +130,20 @@ never invent another id — read, create, and update `me.json` only.
123
130
  Leave optional fields the user hasn't given you out of the JSON entirely.
124
131
 
125
132
  ## What to do
126
- **Set up / update**: Read `data/profile/items/me.json` (may not exist yet), merge
127
- changes, Write back. Preserve fields you weren't asked to change. If missing and
128
- the user wants to set their profile, create it with `id: "me"` plus the fields
129
- they provided.
133
+ **Set up / update**: Read the relevant `data/profile/items/<id>.json` (the primary
134
+ one is `me.json`, may not exist yet), merge changes, Write back. Preserve fields
135
+ you weren't asked to change. When creating the primary profile use `id: "me"`; for
136
+ an additional issuer, list `data/profile/items/` first and pick a fresh kebab-case
137
+ id.
130
138
 
131
- **Look up**: Read `me.json` and answer from it. If missing, tell the user their
132
- profile isn't set up yet and offer to collect it (`presentForm` only if several
133
- fields are needed at once).
139
+ **Look up**: Read the relevant profile JSON and answer from it. If the primary
140
+ `me.json` is missing, tell the user their profile isn't set up yet and offer to
141
+ collect it (`presentForm` only if several fields are needed at once).
134
142
 
135
- **Never delete** the profile unless the user explicitly asks to reset it.
143
+ **Never delete** a profile unless the user explicitly asks to an invoice's
144
+ `issuerId` ref breaks if you remove a profile it points at.
136
145
 
137
- ## Linking to the profile in chat
146
+ ## Linking to a profile in chat
138
147
  - Do: `[your business profile](/collections/profile?selected=me)`
139
148
  - Don't: link the raw JSON path.
140
149
 
@@ -147,8 +156,9 @@ only when you genuinely need several fields they haven't provided.
147
156
 
148
157
  ## 2. `invoice`
149
158
 
150
- `data/skills/invoice/schema.json` (`issuer` embeds `profile/me`; `clientId` is a
151
- `ref` **to `clients`**; subtotal / tax / total are `derived`):
159
+ `data/skills/invoice/schema.json` (`issuerId` is a `ref` **to `profile`** and
160
+ `issuer` embeds whichever profile it names via `idField`; `clientId` is a `ref`
161
+ **to `clients`**; subtotal / tax / total are `derived`):
152
162
 
153
163
  ```json
154
164
  {
@@ -158,7 +168,8 @@ only when you genuinely need several fields they haven't provided.
158
168
  "primaryKey": "id",
159
169
  "fields": {
160
170
  "id": { "type": "string", "label": "ID", "primary": true, "required": true },
161
- "issuer": { "type": "embed", "to": "profile", "id": "me", "label": "From (issuer)" },
171
+ "issuerId": { "type": "ref", "to": "profile", "label": "From (issuer)", "required": true },
172
+ "issuer": { "type": "embed", "to": "profile", "idField": "issuerId", "label": "From (issuer)" },
162
173
  "clientId": { "type": "ref", "to": "clients", "label": "Client", "required": true },
163
174
  "issueDate": { "type": "date", "label": "Issued", "required": true },
164
175
  "dueDate": { "type": "date", "label": "Due" },
@@ -196,7 +207,8 @@ name: invoice
196
207
  description: A simple invoice ledger — create, list, edit, and remove invoices.
197
208
  Records live at `data/invoice/items/<id>.json`; the user views them at
198
209
  `/collections/invoice`. Each invoice references a client (`clientId` → the
199
- `clients` collection) and embeds the user's `profile/me` as the issuer.
210
+ `clients` collection) and an issuer profile (`issuerId` the `profile`
211
+ collection), which it embeds as the bill-from block.
200
212
  Subtotal / tax / total are host-computed — you don't write them.
201
213
  ---
202
214
 
@@ -207,9 +219,13 @@ description: A simple invoice ledger — create, list, edit, and remove invoices
207
219
  counter): `INV-2026-0001`. List `data/invoice/items/` first to find the highest
208
220
  number for the year, then increment; pick the next free number rather than
209
221
  overwriting.
210
- - `issuer` — **display-only** embed of `profile/me` (the bill-from block). You do
211
- **not** write this it carries no stored value. If the profile isn't set up,
212
- the view shows a "set it up" prompt; point the user at `/collections/profile`.
222
+ - `issuerId` — ref `profile`, **required**. The bill-from identity. Write the
223
+ raw profile slug (the primary profile is `me`); the host renders it as a dropdown
224
+ picker. List `data/profile/items/` to find the right slug; no profile set up yet
225
+ → point the user at `/collections/profile`.
226
+ - `issuer` — **display-only** embed of the profile named by `issuerId` (the
227
+ bill-from block). You do **not** write this — it carries no stored value; it
228
+ follows whatever `issuerId` points at.
213
229
  - `clientId` — ref → `clients`, **required**
214
230
  - `issueDate` — ISO date `YYYY-MM-DD`, **required** (default today)
215
231
  - `dueDate` — ISO date (optional)
@@ -232,8 +248,10 @@ or supply a literal slug. Never invent a clientId — it renders as a broken lin
232
248
 
233
249
  ## What to do
234
250
  **Create**: derive an `id`, build the record, Write `data/invoice/items/<id>.json`.
235
- Defaults: `status: "draft"`, `issueDate: <today>`. Set `currency` from context —
236
- don't silently default to USD. Don't write `subtotal` / `tax` / `total`.
251
+ Defaults: `status: "draft"`, `issueDate: <today>`, `issuerId: "me"` (the primary
252
+ profile) unless the user names a different issuer then resolve it to a real
253
+ `profile` slug. Set `currency` from context — don't silently default to USD. Don't
254
+ write `subtotal` / `tax` / `total`.
237
255
 
238
256
  **Create from worklog hours** (common): when the user says "invoice Acme for the
239
257
  work I did this month":
@@ -304,11 +322,12 @@ and present it in the canvas with the `presentDocument` tool. Steps:
304
322
 
305
323
  1. **Resolve the recipient (Bill To).** Read `data/clients/items/<clientId>.json`
306
324
  for the client's `name`, `address`, `email`.
307
- 2. **Resolve the issuer (From).** Read `data/profile/items/me.json` for
308
- `companyName`, `taxRegistrationId`, `address`, `email`, `phone`,
309
- `paymentDetails`. **If that file does not exist**, stop and tell the user their
310
- business profile isn't set up point them at `/collections/profile` and do
311
- not write a half-blank invoice.
325
+ 2. **Resolve the issuer (From).** Read `data/profile/items/<issuerId>.json` — the
326
+ invoice's own `issuerId` (fall back to `me` for a legacy invoice without one) —
327
+ for `companyName`, `taxRegistrationId`, `address`, `email`, `phone`,
328
+ `paymentDetails`. **If that file does not exist**, stop and tell the user that
329
+ issuer's business profile isn't set up — point them at `/collections/profile` —
330
+ and do not write a half-blank invoice.
312
331
  3. **Compute totals** from line items: `subtotal` = Σ(`quantity` × `rate`), `tax`
313
332
  = `subtotal` × `taxRate` (missing `taxRate` = 0), `total` = `subtotal` + `tax`.
314
333
  Format money with the invoice's `currency`.
@@ -332,8 +351,11 @@ The invoice record is in the `<record_data_json>` block above. You are the
332
351
  `accounting` role and own `manageAccounting`. Post the double-entry sale journal.
333
352
 
334
353
  ### 1. Resolve the book
335
- 1. Read `data/profile/items/me.json`. If it has a non-empty `defaultBookId`, use
336
- it as `bookId` and skip the rest of this step.
354
+ 1. Read `data/profile/items/<issuerId>.json` the invoice's own `issuerId` (fall
355
+ back to `me` for a legacy invoice without one). If it has a non-empty
356
+ `defaultBookId`, use it as `bookId` and skip the rest of this step. If that
357
+ profile file doesn't exist, tell the user that issuer's profile isn't set up
358
+ (point them at `/collections/profile`) and stop.
337
359
  2. Otherwise call `manageAccounting` `getBooks`. One book → use it. Several →
338
360
  keep books whose currency/country fit the invoice; if one remains use it; else
339
361
  prefer the book whose name matches the issuer's `companyName`; only if still
@@ -378,11 +400,14 @@ The invoice record is in the `<record_data_json>` block above. You are the
378
400
  `accounting` role and own `manageAccounting`. The invoice is paid; post the
379
401
  cash-receipt journal that clears the receivable. Payment is always direct deposit
380
402
  to the issuer's bank account (the issuer's `paymentDetails` on
381
- `data/profile/items/me.json`), so the debit is the book's Cash / Checking account.
403
+ `data/profile/items/<issuerId>.json`), so the debit is the book's Cash / Checking
404
+ account.
382
405
 
383
406
  ### 1. Resolve the book
384
- Read `data/profile/items/me.json` for a non-empty `defaultBookId`; else `getBooks`
385
- and pick as in the sale flow. No book tell the user to set one up and stop.
407
+ Read `data/profile/items/<issuerId>.json` the invoice's own `issuerId` (fall
408
+ back to `me` for a legacy invoice) for a non-empty `defaultBookId`; else
409
+ `getBooks` and pick as in the sale flow. No book → tell the user to set one up and
410
+ stop.
386
411
 
387
412
  ### 2. Resolve real account codes
388
413
  `getAccounts` — never invent: **Cash / Checking / Bank** (asset, the debit; if
@@ -418,7 +443,8 @@ The invoice record is in the `<record_data_json>` block above. You are the
418
443
  journals posted for it.
419
444
 
420
445
  ### 1. Resolve the book
421
- Read `data/profile/items/me.json` for a non-empty `defaultBookId`; else resolve
446
+ Read `data/profile/items/<issuerId>.json` the invoice's own `issuerId` (fall
447
+ back to `me` for a legacy invoice) — for a non-empty `defaultBookId`; else resolve
422
448
  via `getBooks` (as in the sale flow). No book → nothing to void; say so and stop.
423
449
 
424
450
  ### 2. Resolve the Accounts Receivable code
@@ -168,10 +168,17 @@ Every field spec needs a `type` and a `label`. Extra keys by type:
168
168
  A `derived` field on the same record can also **dereference** a `ref` to read
169
169
  a numeric column off the record it points at — see the cross-collection
170
170
  formula syntax below.
171
- - **`embed`** — `to: "<target-slug>"`, `id: "<record-id>"`. Pulls a _fixed_
171
+ - **`embed`** — `to: "<target-slug>"`, plus **exactly one** of `id` (a _fixed_
172
+ record id, same for every row) or `idField` (a _per-record_ target). Pulls a
172
173
  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" }`.
174
+ **nothing is stored** on this record). Fixed every row embeds the same
175
+ singleton record (e.g. a one-record `settings` collection):
176
+ `{ "type": "embed", "to": "settings", "id": "app" }`.
177
+ Per-record — `idField` names a sibling field (typically a `ref`) holding the
178
+ target id, so each row embeds a different record; the host renders that
179
+ sibling as a dropdown picker in the editor and hides its own cell (the embed
180
+ owns it). E.g. a multi-issuer invoice: a `ref` field `issuerId → profile`
181
+ plus `{ "type": "embed", "to": "profile", "idField": "issuerId" }`.
175
182
  - **`table`** — `of: { <col>: <sub-field-spec>, ... }`. An array of rows. Each
176
183
  sub-field is a flat spec; sub-fields **cannot** be `table` or `derived`
177
184
  (no nested tables, no computed columns).
@@ -851,8 +858,9 @@ recipe when the user wants any of them, and copy the schema verbatim:
851
858
  - **`worklog`** — adds a `ref` (`clientId → clients`), a `date`, a `number`, a
852
859
  `boolean`. A companion data source.
853
860
  - **`config/helps/billing-invoice.md`** (Bundle B):
854
- - **`profile`** — a `singleton` (one record, id `me`): the issuer identity.
855
- - **`invoice`** — the full toolkit in one schema: an `embed` issuer
856
- (`profile/me`), a `ref` client (`clients`), a `table` of line items, three
857
- `derived` money fields, an `enum` status, and four `actions` (PDF always-on;
858
- sale / payment / void gated by `status` via `when`).
861
+ - **`profile`** — one record per issuer identity (primary id `me`); each invoice
862
+ picks one.
863
+ - **`invoice`** the full toolkit in one schema: a `ref` issuer (`issuerId
864
+ profile`) embedded via an `idField` `embed`, a `ref` client (`clients`), a
865
+ `table` of line items, three `derived` money fields, an `enum` status, and four
866
+ `actions` (PDF always-on; sale / payment / void gated by `status` via `when`).
@@ -151,6 +151,27 @@ export interface CollectionCustomView {
151
151
  /** Skill-relative path to the HTML file under `views/` (e.g.
152
152
  * `views/year.html`). Path-safe, must end in `.html`. */
153
153
  file: string;
154
+ /** Optional skill-relative path to a JSON translation dictionary co-located
155
+ * with the view (e.g. `views/year.i18n.json`). Shape mirrors **vue-i18n
156
+ * locale messages** so an author can lift their app's locale JSON
157
+ * verbatim:
158
+ *
159
+ * ```json
160
+ * { "en": { "next": "Next", "hello": "Hello, {name}" },
161
+ * "ja": { "next": "次へ", "hello": "{name} さん、こんにちは" } }
162
+ * ```
163
+ *
164
+ * The host picks the block matching the active app locale (fallback
165
+ * `"en"`, else `{}`) and injects ONLY that flat string map into
166
+ * `window.__MC_VIEW.dict`. The iframe-side helper
167
+ * `__MC_VIEW.t(key, named?)` mirrors vue-i18n's `t('msg', { name: 'x' })`
168
+ * signature — named-interpolation only (no pluralization / linked
169
+ * messages in v1; shipping vue-i18n's full runtime into every sandboxed
170
+ * iframe would dominate page weight). The view never sees other locales'
171
+ * strings. Constrained to `views/*.i18n.json` so authors keep the
172
+ * translation file next to the HTML it translates. Absent ⇒ host-side
173
+ * no-op (an i18n-less view keeps working; `t(key)` echoes the key). */
174
+ i18n?: string;
154
175
  /** What the view may do with the data endpoint. Defaults to `["read"]`
155
176
  * (least privilege); declare `["read","write"]` only for views that
156
177
  * edit records. The mint endpoint clamps any requested caps to this. */
@@ -199,9 +220,20 @@ export interface CollectionFieldSpec {
199
220
  * record to pull from the `to` collection (e.g. `me` for the
200
221
  * singleton mc-profile). Nothing is stored on this record — the
201
222
  * embed is a display-only directive resolved at render time, so
202
- * it never appears in the list table or the edit form. Required
203
- * when type is `embed`; ignored on every other type. */
223
+ * it never appears in the list table or the edit form. Supply
224
+ * either this (a fixed target, same for every record) or
225
+ * `idField` (a per-record target) — exactly one. Ignored on every
226
+ * other type. */
204
227
  id?: string;
228
+ /** When `type === "embed"`: the name of a sibling top-level field
229
+ * whose value names the target record's primary key — letting the
230
+ * embed point at a *different* record per row (e.g. an invoice's
231
+ * `issuerId` ref selects which `profile` to embed as the
232
+ * bill-from block). The renderer reads `record[idField]` at render
233
+ * time; an absent/empty value resolves to "no record" (the same
234
+ * fail-soft as a missing fixed `id`). Mutually exclusive with `id`
235
+ * — an embed must declare exactly one. Ignored on every other type. */
236
+ idField?: string;
205
237
  /** When `type === "money"` (or `type === "derived"` with
206
238
  * `display: "money"`): a literal ISO 4217 currency code passed to
207
239
  * `Intl.NumberFormat` for display — fixed for every record. The
@@ -36,8 +36,20 @@ var VIEWS_PREFIX = "views/";
36
36
  function isSafeCustomViewPath(value) {
37
37
  return value.startsWith(VIEWS_PREFIX) && /\.html$/.test(value) && isSafeTemplatePath(value);
38
38
  }
39
+ /**
40
+ * A custom-view `i18n` value: the JSON dictionary file authors ship next to
41
+ * their `views/<id>.html`. Constrained to `views/<name>.i18n.json` so the
42
+ * field can only point at a translation file co-located with the view it
43
+ * translates — the same `views/` containment the HTML reader uses. The
44
+ * `.i18n.json` suffix (vs plain `.json`) makes the file's purpose grep-able
45
+ * for both authors and tooling.
46
+ */
47
+ function isSafeCustomViewI18nPath(value) {
48
+ return value.startsWith(VIEWS_PREFIX) && /\.i18n\.json$/.test(value) && isSafeTemplatePath(value);
49
+ }
39
50
  //#endregion
40
51
  exports.isSafeActionTemplatePath = isSafeActionTemplatePath;
52
+ exports.isSafeCustomViewI18nPath = isSafeCustomViewI18nPath;
41
53
  exports.isSafeCustomViewPath = isSafeCustomViewPath;
42
54
  exports.isSafeTemplatePath = isSafeTemplatePath;
43
55
 
@@ -1 +1 @@
1
- {"version":3,"file":"paths.cjs","names":[],"sources":["../../src/collection/server/templatePath.ts"],"sourcesContent":["// Shared template-path safety, used by BOTH the collection schema\n// validator (`discovery.ts` `ActionSpecSchema`) and the skill-bridge\n// hook (`hooks/handlers/skillBridge.ts`). Centralised so the set of\n// action `template` paths a schema may declare is *exactly* the set\n// the bridge mirrors into `.claude/skills/<slug>/` — if the two\n// diverge, a schema can validate yet reference a template the bridge\n// silently drops (runtime \"template could not be read\").\n//\n// Keep this module dependency-free: the bridge is bundled into a lean\n// esbuild dispatcher, so it must not drag in workspace/path machinery.\n\n/** `templates/` — the subdir an action template must live under. */\nconst TEMPLATES_PREFIX = \"templates/\";\n\n/**\n * A safe skill-relative path: non-empty, no backslash, not absolute,\n * and every `/`-separated segment is a plain `[A-Za-z0-9._-]+` token\n * that isn't `.` / `..` (no traversal). Multi-segment (nested) paths\n * are allowed. The reader's realpath containment is the hard\n * guarantee; this fails a bad path fast.\n */\nexport function isSafeTemplatePath(value: string): boolean {\n if (value.length === 0 || value.includes(\"\\\\\") || value.startsWith(\"/\")) return false;\n return value.split(\"/\").every((seg) => seg.length > 0 && seg !== \".\" && seg !== \"..\" && /^[A-Za-z0-9._-]+$/.test(seg));\n}\n\n/**\n * An action `template` value: a safe path that lives under the skill's\n * `templates/` subdir. This is the canonical contract — the schema\n * validator rejects anything else up front, and the bridge mirrors\n * exactly these. Nested paths (`templates/mail/welcome.md`) and any\n * extension are allowed as long as the path is otherwise safe.\n */\nexport function isSafeActionTemplatePath(value: string): boolean {\n return value.startsWith(TEMPLATES_PREFIX) && isSafeTemplatePath(value);\n}\n\n/** `views/` — the subdir a custom-view HTML file must live under. */\nconst VIEWS_PREFIX = \"views/\";\n\n/**\n * A custom-view `file` value: a safe path under the skill's `views/`\n * subdir that ends in `.html`. Custom views are LLM-authored HTML the\n * host renders in a sandboxed iframe; constraining them to `views/*.html`\n * keeps the data folder and the schema/template files off-limits to the\n * view-file reader. Same per-segment safety as templates; the reader's\n * realpath containment is the hard guarantee.\n */\nexport function isSafeCustomViewPath(value: string): boolean {\n return value.startsWith(VIEWS_PREFIX) && /\\.html$/.test(value) && isSafeTemplatePath(value);\n}\n"],"mappings":";;;AAYA,IAAM,mBAAmB;;;;;;;;AASzB,SAAgB,mBAAmB,OAAwB;CACzD,IAAI,MAAM,WAAW,KAAK,MAAM,SAAS,IAAI,KAAK,MAAM,WAAW,GAAG,GAAG,OAAO;CAChF,OAAO,MAAM,MAAM,GAAG,CAAC,CAAC,OAAO,QAAQ,IAAI,SAAS,KAAK,QAAQ,OAAO,QAAQ,QAAQ,oBAAoB,KAAK,GAAG,CAAC;AACvH;;;;;;;;AASA,SAAgB,yBAAyB,OAAwB;CAC/D,OAAO,MAAM,WAAW,gBAAgB,KAAK,mBAAmB,KAAK;AACvE;;AAGA,IAAM,eAAe;;;;;;;;;AAUrB,SAAgB,qBAAqB,OAAwB;CAC3D,OAAO,MAAM,WAAW,YAAY,KAAK,UAAU,KAAK,KAAK,KAAK,mBAAmB,KAAK;AAC5F"}
1
+ {"version":3,"file":"paths.cjs","names":[],"sources":["../../src/collection/server/templatePath.ts"],"sourcesContent":["// Shared template-path safety, used by BOTH the collection schema\n// validator (`discovery.ts` `ActionSpecSchema`) and the skill-bridge\n// hook (`hooks/handlers/skillBridge.ts`). Centralised so the set of\n// action `template` paths a schema may declare is *exactly* the set\n// the bridge mirrors into `.claude/skills/<slug>/` — if the two\n// diverge, a schema can validate yet reference a template the bridge\n// silently drops (runtime \"template could not be read\").\n//\n// Keep this module dependency-free: the bridge is bundled into a lean\n// esbuild dispatcher, so it must not drag in workspace/path machinery.\n\n/** `templates/` — the subdir an action template must live under. */\nconst TEMPLATES_PREFIX = \"templates/\";\n\n/**\n * A safe skill-relative path: non-empty, no backslash, not absolute,\n * and every `/`-separated segment is a plain `[A-Za-z0-9._-]+` token\n * that isn't `.` / `..` (no traversal). Multi-segment (nested) paths\n * are allowed. The reader's realpath containment is the hard\n * guarantee; this fails a bad path fast.\n */\nexport function isSafeTemplatePath(value: string): boolean {\n if (value.length === 0 || value.includes(\"\\\\\") || value.startsWith(\"/\")) return false;\n return value.split(\"/\").every((seg) => seg.length > 0 && seg !== \".\" && seg !== \"..\" && /^[A-Za-z0-9._-]+$/.test(seg));\n}\n\n/**\n * An action `template` value: a safe path that lives under the skill's\n * `templates/` subdir. This is the canonical contract — the schema\n * validator rejects anything else up front, and the bridge mirrors\n * exactly these. Nested paths (`templates/mail/welcome.md`) and any\n * extension are allowed as long as the path is otherwise safe.\n */\nexport function isSafeActionTemplatePath(value: string): boolean {\n return value.startsWith(TEMPLATES_PREFIX) && isSafeTemplatePath(value);\n}\n\n/** `views/` — the subdir a custom-view HTML file must live under. */\nconst VIEWS_PREFIX = \"views/\";\n\n/**\n * A custom-view `file` value: a safe path under the skill's `views/`\n * subdir that ends in `.html`. Custom views are LLM-authored HTML the\n * host renders in a sandboxed iframe; constraining them to `views/*.html`\n * keeps the data folder and the schema/template files off-limits to the\n * view-file reader. Same per-segment safety as templates; the reader's\n * realpath containment is the hard guarantee.\n */\nexport function isSafeCustomViewPath(value: string): boolean {\n return value.startsWith(VIEWS_PREFIX) && /\\.html$/.test(value) && isSafeTemplatePath(value);\n}\n\n/**\n * A custom-view `i18n` value: the JSON dictionary file authors ship next to\n * their `views/<id>.html`. Constrained to `views/<name>.i18n.json` so the\n * field can only point at a translation file co-located with the view it\n * translates — the same `views/` containment the HTML reader uses. The\n * `.i18n.json` suffix (vs plain `.json`) makes the file's purpose grep-able\n * for both authors and tooling.\n */\nexport function isSafeCustomViewI18nPath(value: string): boolean {\n return value.startsWith(VIEWS_PREFIX) && /\\.i18n\\.json$/.test(value) && isSafeTemplatePath(value);\n}\n"],"mappings":";;;AAYA,IAAM,mBAAmB;;;;;;;;AASzB,SAAgB,mBAAmB,OAAwB;CACzD,IAAI,MAAM,WAAW,KAAK,MAAM,SAAS,IAAI,KAAK,MAAM,WAAW,GAAG,GAAG,OAAO;CAChF,OAAO,MAAM,MAAM,GAAG,CAAC,CAAC,OAAO,QAAQ,IAAI,SAAS,KAAK,QAAQ,OAAO,QAAQ,QAAQ,oBAAoB,KAAK,GAAG,CAAC;AACvH;;;;;;;;AASA,SAAgB,yBAAyB,OAAwB;CAC/D,OAAO,MAAM,WAAW,gBAAgB,KAAK,mBAAmB,KAAK;AACvE;;AAGA,IAAM,eAAe;;;;;;;;;AAUrB,SAAgB,qBAAqB,OAAwB;CAC3D,OAAO,MAAM,WAAW,YAAY,KAAK,UAAU,KAAK,KAAK,KAAK,mBAAmB,KAAK;AAC5F;;;;;;;;;AAUA,SAAgB,yBAAyB,OAAwB;CAC/D,OAAO,MAAM,WAAW,YAAY,KAAK,gBAAgB,KAAK,KAAK,KAAK,mBAAmB,KAAK;AAClG"}
@@ -35,7 +35,18 @@ var VIEWS_PREFIX = "views/";
35
35
  function isSafeCustomViewPath(value) {
36
36
  return value.startsWith(VIEWS_PREFIX) && /\.html$/.test(value) && isSafeTemplatePath(value);
37
37
  }
38
+ /**
39
+ * A custom-view `i18n` value: the JSON dictionary file authors ship next to
40
+ * their `views/<id>.html`. Constrained to `views/<name>.i18n.json` so the
41
+ * field can only point at a translation file co-located with the view it
42
+ * translates — the same `views/` containment the HTML reader uses. The
43
+ * `.i18n.json` suffix (vs plain `.json`) makes the file's purpose grep-able
44
+ * for both authors and tooling.
45
+ */
46
+ function isSafeCustomViewI18nPath(value) {
47
+ return value.startsWith(VIEWS_PREFIX) && /\.i18n\.json$/.test(value) && isSafeTemplatePath(value);
48
+ }
38
49
  //#endregion
39
- export { isSafeActionTemplatePath, isSafeCustomViewPath, isSafeTemplatePath };
50
+ export { isSafeActionTemplatePath, isSafeCustomViewI18nPath, isSafeCustomViewPath, isSafeTemplatePath };
40
51
 
41
52
  //# sourceMappingURL=paths.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"paths.js","names":[],"sources":["../../src/collection/server/templatePath.ts"],"sourcesContent":["// Shared template-path safety, used by BOTH the collection schema\n// validator (`discovery.ts` `ActionSpecSchema`) and the skill-bridge\n// hook (`hooks/handlers/skillBridge.ts`). Centralised so the set of\n// action `template` paths a schema may declare is *exactly* the set\n// the bridge mirrors into `.claude/skills/<slug>/` — if the two\n// diverge, a schema can validate yet reference a template the bridge\n// silently drops (runtime \"template could not be read\").\n//\n// Keep this module dependency-free: the bridge is bundled into a lean\n// esbuild dispatcher, so it must not drag in workspace/path machinery.\n\n/** `templates/` — the subdir an action template must live under. */\nconst TEMPLATES_PREFIX = \"templates/\";\n\n/**\n * A safe skill-relative path: non-empty, no backslash, not absolute,\n * and every `/`-separated segment is a plain `[A-Za-z0-9._-]+` token\n * that isn't `.` / `..` (no traversal). Multi-segment (nested) paths\n * are allowed. The reader's realpath containment is the hard\n * guarantee; this fails a bad path fast.\n */\nexport function isSafeTemplatePath(value: string): boolean {\n if (value.length === 0 || value.includes(\"\\\\\") || value.startsWith(\"/\")) return false;\n return value.split(\"/\").every((seg) => seg.length > 0 && seg !== \".\" && seg !== \"..\" && /^[A-Za-z0-9._-]+$/.test(seg));\n}\n\n/**\n * An action `template` value: a safe path that lives under the skill's\n * `templates/` subdir. This is the canonical contract — the schema\n * validator rejects anything else up front, and the bridge mirrors\n * exactly these. Nested paths (`templates/mail/welcome.md`) and any\n * extension are allowed as long as the path is otherwise safe.\n */\nexport function isSafeActionTemplatePath(value: string): boolean {\n return value.startsWith(TEMPLATES_PREFIX) && isSafeTemplatePath(value);\n}\n\n/** `views/` — the subdir a custom-view HTML file must live under. */\nconst VIEWS_PREFIX = \"views/\";\n\n/**\n * A custom-view `file` value: a safe path under the skill's `views/`\n * subdir that ends in `.html`. Custom views are LLM-authored HTML the\n * host renders in a sandboxed iframe; constraining them to `views/*.html`\n * keeps the data folder and the schema/template files off-limits to the\n * view-file reader. Same per-segment safety as templates; the reader's\n * realpath containment is the hard guarantee.\n */\nexport function isSafeCustomViewPath(value: string): boolean {\n return value.startsWith(VIEWS_PREFIX) && /\\.html$/.test(value) && isSafeTemplatePath(value);\n}\n"],"mappings":";;AAYA,IAAM,mBAAmB;;;;;;;;AASzB,SAAgB,mBAAmB,OAAwB;CACzD,IAAI,MAAM,WAAW,KAAK,MAAM,SAAS,IAAI,KAAK,MAAM,WAAW,GAAG,GAAG,OAAO;CAChF,OAAO,MAAM,MAAM,GAAG,CAAC,CAAC,OAAO,QAAQ,IAAI,SAAS,KAAK,QAAQ,OAAO,QAAQ,QAAQ,oBAAoB,KAAK,GAAG,CAAC;AACvH;;;;;;;;AASA,SAAgB,yBAAyB,OAAwB;CAC/D,OAAO,MAAM,WAAW,gBAAgB,KAAK,mBAAmB,KAAK;AACvE;;AAGA,IAAM,eAAe;;;;;;;;;AAUrB,SAAgB,qBAAqB,OAAwB;CAC3D,OAAO,MAAM,WAAW,YAAY,KAAK,UAAU,KAAK,KAAK,KAAK,mBAAmB,KAAK;AAC5F"}
1
+ {"version":3,"file":"paths.js","names":[],"sources":["../../src/collection/server/templatePath.ts"],"sourcesContent":["// Shared template-path safety, used by BOTH the collection schema\n// validator (`discovery.ts` `ActionSpecSchema`) and the skill-bridge\n// hook (`hooks/handlers/skillBridge.ts`). Centralised so the set of\n// action `template` paths a schema may declare is *exactly* the set\n// the bridge mirrors into `.claude/skills/<slug>/` — if the two\n// diverge, a schema can validate yet reference a template the bridge\n// silently drops (runtime \"template could not be read\").\n//\n// Keep this module dependency-free: the bridge is bundled into a lean\n// esbuild dispatcher, so it must not drag in workspace/path machinery.\n\n/** `templates/` — the subdir an action template must live under. */\nconst TEMPLATES_PREFIX = \"templates/\";\n\n/**\n * A safe skill-relative path: non-empty, no backslash, not absolute,\n * and every `/`-separated segment is a plain `[A-Za-z0-9._-]+` token\n * that isn't `.` / `..` (no traversal). Multi-segment (nested) paths\n * are allowed. The reader's realpath containment is the hard\n * guarantee; this fails a bad path fast.\n */\nexport function isSafeTemplatePath(value: string): boolean {\n if (value.length === 0 || value.includes(\"\\\\\") || value.startsWith(\"/\")) return false;\n return value.split(\"/\").every((seg) => seg.length > 0 && seg !== \".\" && seg !== \"..\" && /^[A-Za-z0-9._-]+$/.test(seg));\n}\n\n/**\n * An action `template` value: a safe path that lives under the skill's\n * `templates/` subdir. This is the canonical contract — the schema\n * validator rejects anything else up front, and the bridge mirrors\n * exactly these. Nested paths (`templates/mail/welcome.md`) and any\n * extension are allowed as long as the path is otherwise safe.\n */\nexport function isSafeActionTemplatePath(value: string): boolean {\n return value.startsWith(TEMPLATES_PREFIX) && isSafeTemplatePath(value);\n}\n\n/** `views/` — the subdir a custom-view HTML file must live under. */\nconst VIEWS_PREFIX = \"views/\";\n\n/**\n * A custom-view `file` value: a safe path under the skill's `views/`\n * subdir that ends in `.html`. Custom views are LLM-authored HTML the\n * host renders in a sandboxed iframe; constraining them to `views/*.html`\n * keeps the data folder and the schema/template files off-limits to the\n * view-file reader. Same per-segment safety as templates; the reader's\n * realpath containment is the hard guarantee.\n */\nexport function isSafeCustomViewPath(value: string): boolean {\n return value.startsWith(VIEWS_PREFIX) && /\\.html$/.test(value) && isSafeTemplatePath(value);\n}\n\n/**\n * A custom-view `i18n` value: the JSON dictionary file authors ship next to\n * their `views/<id>.html`. Constrained to `views/<name>.i18n.json` so the\n * field can only point at a translation file co-located with the view it\n * translates — the same `views/` containment the HTML reader uses. The\n * `.i18n.json` suffix (vs plain `.json`) makes the file's purpose grep-able\n * for both authors and tooling.\n */\nexport function isSafeCustomViewI18nPath(value: string): boolean {\n return value.startsWith(VIEWS_PREFIX) && /\\.i18n\\.json$/.test(value) && isSafeTemplatePath(value);\n}\n"],"mappings":";;AAYA,IAAM,mBAAmB;;;;;;;;AASzB,SAAgB,mBAAmB,OAAwB;CACzD,IAAI,MAAM,WAAW,KAAK,MAAM,SAAS,IAAI,KAAK,MAAM,WAAW,GAAG,GAAG,OAAO;CAChF,OAAO,MAAM,MAAM,GAAG,CAAC,CAAC,OAAO,QAAQ,IAAI,SAAS,KAAK,QAAQ,OAAO,QAAQ,QAAQ,oBAAoB,KAAK,GAAG,CAAC;AACvH;;;;;;;;AASA,SAAgB,yBAAyB,OAAwB;CAC/D,OAAO,MAAM,WAAW,gBAAgB,KAAK,mBAAmB,KAAK;AACvE;;AAGA,IAAM,eAAe;;;;;;;;;AAUrB,SAAgB,qBAAqB,OAAwB;CAC3D,OAAO,MAAM,WAAW,YAAY,KAAK,UAAU,KAAK,KAAK,KAAK,mBAAmB,KAAK;AAC5F;;;;;;;;;AAUA,SAAgB,yBAAyB,OAAwB;CAC/D,OAAO,MAAM,WAAW,YAAY,KAAK,gBAAgB,KAAK,KAAK,KAAK,mBAAmB,KAAK;AAClG"}
@@ -32,6 +32,7 @@ export declare const CollectionSchemaZ: z.ZodObject<{
32
32
  required: z.ZodOptional<z.ZodBoolean>;
33
33
  to: z.ZodOptional<z.ZodString>;
34
34
  id: z.ZodOptional<z.ZodString>;
35
+ idField: z.ZodOptional<z.ZodString>;
35
36
  currency: z.ZodOptional<z.ZodString>;
36
37
  currencyField: z.ZodOptional<z.ZodString>;
37
38
  values: z.ZodOptional<z.ZodArray<z.ZodString>>;
@@ -143,6 +144,7 @@ export declare const CollectionSchemaZ: z.ZodObject<{
143
144
  label: z.ZodString;
144
145
  icon: z.ZodOptional<z.ZodString>;
145
146
  file: z.ZodString;
147
+ i18n: z.ZodOptional<z.ZodString>;
146
148
  capabilities: z.ZodOptional<z.ZodArray<z.ZodEnum<{
147
149
  read: "read";
148
150
  write: "write";
@@ -1,5 +1,5 @@
1
1
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
- const require_server = require("../../server-Db-x7OXz.cjs");
2
+ const require_server = require("../../server-Ceb6OQ8v.cjs");
3
3
  const require_collection_paths = require("../paths.cjs");
4
4
  exports.COMPUTED_TYPES = require_server.COMPUTED_TYPES;
5
5
  exports.CollectionSchemaZ = require_server.CollectionSchemaZ;
@@ -23,6 +23,7 @@ exports.getWorkspaceRoot = require_server.getWorkspaceRoot;
23
23
  exports.isContainedInRoot = require_server.isContainedInRoot;
24
24
  exports.isContainedInWorkspace = require_server.isContainedInWorkspace;
25
25
  exports.isSafeActionTemplatePath = require_collection_paths.isSafeActionTemplatePath;
26
+ exports.isSafeCustomViewI18nPath = require_collection_paths.isSafeCustomViewI18nPath;
26
27
  exports.isSafeCustomViewPath = require_collection_paths.isSafeCustomViewPath;
27
28
  exports.isSafeTemplatePath = require_collection_paths.isSafeTemplatePath;
28
29
  exports.isTriggerDue = require_server.isTriggerDue;
@@ -34,6 +35,7 @@ exports.maybeSpawnSuccessor = require_server.maybeSpawnSuccessor;
34
35
  exports.parseCivil = require_server.parseCivil;
35
36
  exports.publishCollectionChange = require_server.publishCollectionChange;
36
37
  exports.readCustomViewHtml = require_server.readCustomViewHtml;
38
+ exports.readCustomViewI18n = require_server.readCustomViewI18n;
37
39
  exports.readItem = require_server.readItem;
38
40
  exports.readSkillTemplate = require_server.readSkillTemplate;
39
41
  exports.resolveCreateItemId = require_server.resolveCreateItemId;
@@ -1,3 +1,3 @@
1
- import { A as readSkillTemplate, B as safeSlugName, C as buildActionSeedPrompt, D as listItems, E as generateItemId, F as isContainedInWorkspace, G as setCollectionChangePublisher, H as getWorkspaceRoot, I as itemFilePath, L as resolveDataDir, M as writeItem, N as SCHEMA_FILE, O as readCustomViewHtml, P as isContainedInRoot, R as resolveTemplatePath, S as validateRecordObject, T as deleteItem, U as log, V as configureCollectionHost, W as publishCollectionChange, _ as loadCollection, a as computeSuccessor, b as COMPUTED_TYPES, c as isTriggerDue, d as resolveEvery, f as successorId, g as discoverCollections, h as acceptParsedSchema, i as advanceTriggerDate, j as resolveCreateItemId, k as readItem, l as maybeSpawnSuccessor, m as CollectionSchemaZ, n as deleteCollection, o as daysInMonth, p as enrichItems, r as deleteCollectionRefusalMessage, s as formatCivil, t as deleteCustomView, u as parseCivil, v as toDetail, w as buildCollectionActionSeedPrompt, x as validateCollectionRecords, y as toSummary, z as safeRecordId } from "../../server-q4WPXtbU.js";
2
- import { isSafeActionTemplatePath, isSafeCustomViewPath, isSafeTemplatePath } from "../paths.js";
3
- export { COMPUTED_TYPES, CollectionSchemaZ, SCHEMA_FILE, acceptParsedSchema, advanceTriggerDate, buildActionSeedPrompt, buildCollectionActionSeedPrompt, computeSuccessor, configureCollectionHost, daysInMonth, deleteCollection, deleteCollectionRefusalMessage, deleteCustomView, deleteItem, discoverCollections, enrichItems, formatCivil, generateItemId, getWorkspaceRoot, isContainedInRoot, isContainedInWorkspace, isSafeActionTemplatePath, isSafeCustomViewPath, isSafeTemplatePath, isTriggerDue, itemFilePath, listItems, loadCollection, log, maybeSpawnSuccessor, parseCivil, publishCollectionChange, readCustomViewHtml, readItem, readSkillTemplate, resolveCreateItemId, resolveDataDir, resolveEvery, resolveTemplatePath, safeRecordId, safeSlugName, setCollectionChangePublisher, successorId, toDetail, toSummary, validateCollectionRecords, validateRecordObject, writeItem };
1
+ import { A as readItem, B as safeRecordId, C as buildActionSeedPrompt, D as listItems, E as generateItemId, F as isContainedInRoot, G as publishCollectionChange, H as configureCollectionHost, I as isContainedInWorkspace, K as setCollectionChangePublisher, L as itemFilePath, M as resolveCreateItemId, N as writeItem, O as readCustomViewHtml, P as SCHEMA_FILE, R as resolveDataDir, S as validateRecordObject, T as deleteItem, U as getWorkspaceRoot, V as safeSlugName, W as log, _ as loadCollection, a as computeSuccessor, b as COMPUTED_TYPES, c as isTriggerDue, d as resolveEvery, f as successorId, g as discoverCollections, h as acceptParsedSchema, i as advanceTriggerDate, j as readSkillTemplate, k as readCustomViewI18n, l as maybeSpawnSuccessor, m as CollectionSchemaZ, n as deleteCollection, o as daysInMonth, p as enrichItems, r as deleteCollectionRefusalMessage, s as formatCivil, t as deleteCustomView, u as parseCivil, v as toDetail, w as buildCollectionActionSeedPrompt, x as validateCollectionRecords, y as toSummary, z as resolveTemplatePath } from "../../server-C7inPZNG.js";
2
+ import { isSafeActionTemplatePath, isSafeCustomViewI18nPath, isSafeCustomViewPath, isSafeTemplatePath } from "../paths.js";
3
+ export { COMPUTED_TYPES, CollectionSchemaZ, SCHEMA_FILE, acceptParsedSchema, advanceTriggerDate, buildActionSeedPrompt, buildCollectionActionSeedPrompt, computeSuccessor, configureCollectionHost, daysInMonth, deleteCollection, deleteCollectionRefusalMessage, deleteCustomView, deleteItem, discoverCollections, enrichItems, formatCivil, generateItemId, getWorkspaceRoot, isContainedInRoot, isContainedInWorkspace, isSafeActionTemplatePath, isSafeCustomViewI18nPath, isSafeCustomViewPath, isSafeTemplatePath, isTriggerDue, itemFilePath, listItems, loadCollection, log, maybeSpawnSuccessor, parseCivil, publishCollectionChange, readCustomViewHtml, readCustomViewI18n, readItem, readSkillTemplate, resolveCreateItemId, resolveDataDir, resolveEvery, resolveTemplatePath, safeRecordId, safeSlugName, setCollectionChangePublisher, successorId, toDetail, toSummary, validateCollectionRecords, validateRecordObject, writeItem };
@@ -78,6 +78,13 @@ export declare function deleteItem(dataDir: string, itemId: string, opts?: IoOpt
78
78
  * carry a primary-key value (UI shortcut — Claude normally derives a
79
79
  * semantic id from the record's name). */
80
80
  export declare function generateItemId(): string;
81
+ /** The shape `readSourceAwareFile` (and its public callers
82
+ * `readCustomViewHtml` / `readCustomViewI18n`) need from a loaded collection:
83
+ * the slug for the safe-name check, the source to pick the base set, and the
84
+ * discovered skill dir for the imported-layout fallback. Kept as a named
85
+ * alias so the three signatures stay in lockstep (and so sonarjs stops
86
+ * flagging the inline `Pick` union as duplication). */
87
+ type SourceAwareReadTarget = Pick<LoadedCollection, "slug" | "source" | "skillDir">;
81
88
  /** Read a collection's custom-view HTML, path-safely. `viewFile` is a
82
89
  * schema-validated `views/*.html` path, resolved with realpath containment.
83
90
  * Returns the HTML, or null when the path is unsafe or the file is missing.
@@ -94,7 +101,26 @@ export declare function generateItemId(): string;
94
101
  * so it only needs the single lookup. `resolveTemplatePath` does the
95
102
  * containment / `..` defense per base, so the fallback never broadens the
96
103
  * attack surface. */
97
- export declare function readCustomViewHtml(collection: Pick<LoadedCollection, "slug" | "source" | "skillDir">, viewFile: string, opts?: IoOptions): Promise<string | null>;
104
+ export declare function readCustomViewHtml(collection: SourceAwareReadTarget, viewFile: string, opts?: IoOptions): Promise<string | null>;
105
+ export interface CustomViewI18nResult {
106
+ /** The locale the returned `dict` is keyed to — equals the requested locale
107
+ * when available, else the `"en"` fallback, else `""` when neither block is
108
+ * present (empty `dict`). The host echoes this back to the iframe so
109
+ * `__MC_VIEW.locale` reflects what the view actually got. */
110
+ locale: string;
111
+ /** Flat key → string map for the picked locale. Empty when the file is
112
+ * absent, malformed, or has no usable locale block. Non-string values in a
113
+ * locale block are dropped — `__MC_VIEW.dict` is contract-flat. */
114
+ dict: Record<string, string>;
115
+ }
116
+ /** Read a custom view's translation dictionary and return only the strings
117
+ * for the requested locale (or the `"en"` fallback, or empty). Same
118
+ * source-aware fallback as `readCustomViewHtml` so imported and authored
119
+ * project collections both work. The on-disk file is `{ <locale>: { <key>:
120
+ * <string> } }`; the host never streams other locales' strings to the view.
121
+ * Malformed JSON / unknown shape yields an empty dict — an i18n-less view
122
+ * keeps working unchanged (`__MC_VIEW.t(key)` falls back to the key). */
123
+ export declare function readCustomViewI18n(collection: SourceAwareReadTarget, i18nFile: string, locale: string, opts?: IoOptions): Promise<CustomViewI18nResult>;
98
124
  /** The item id a CREATE should use for `schema`, or null when the
99
125
  * caller should generate one. A singleton collection pins every
100
126
  * create to its fixed `schema.singleton` id, so the "at most one
@@ -118,3 +144,4 @@ export declare function buildActionSeedPrompt(record: CollectionItem, templateTe
118
144
  * record (see `progressSummary`) + the template verbatim. Pure +
119
145
  * exported for tests. Domain-free — the template carries the specifics. */
120
146
  export declare function buildCollectionActionSeedPrompt(items: CollectionItem[], schema: CollectionSchema, templateText: string): string;
147
+ export {};
@@ -23,3 +23,12 @@ export declare function isSafeActionTemplatePath(value: string): boolean;
23
23
  * realpath containment is the hard guarantee.
24
24
  */
25
25
  export declare function isSafeCustomViewPath(value: string): boolean;
26
+ /**
27
+ * A custom-view `i18n` value: the JSON dictionary file authors ship next to
28
+ * their `views/<id>.html`. Constrained to `views/<name>.i18n.json` so the
29
+ * field can only point at a translation file co-located with the view it
30
+ * translates — the same `views/` containment the HTML reader uses. The
31
+ * `.i18n.json` suffix (vs plain `.json`) makes the file's purpose grep-able
32
+ * for both authors and tooling.
33
+ */
34
+ export declare function isSafeCustomViewI18nPath(value: string): boolean;
@@ -1,6 +1,6 @@
1
1
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
2
  const require_collection_index = require("../collection/index.cjs");
3
- const require_server = require("../server-Db-x7OXz.cjs");
3
+ const require_server = require("../server-Ceb6OQ8v.cjs");
4
4
  const require_notifier = require("../notifier-bS8IEeLA.cjs");
5
5
  let node_fs = require("node:fs");
6
6
  let node_fs_promises = require("node:fs/promises");
@@ -1,5 +1,5 @@
1
1
  import { whenMatches } from "../collection/index.js";
2
- import { D as listItems, _ as loadCollection, c as isTriggerDue, g as discoverCollections, k as readItem, l as maybeSpawnSuccessor } from "../server-q4WPXtbU.js";
2
+ import { A as readItem, D as listItems, _ as loadCollection, c as isTriggerDue, g as discoverCollections, l as maybeSpawnSuccessor } from "../server-C7inPZNG.js";
3
3
  import { d as publish, m as updateForPlugin, n as clear, s as listAll } from "../notifier-ChpY0XrY.js";
4
4
  import { watch } from "node:fs";
5
5
  import { mkdir } from "node:fs/promises";