@mulmoclaude/core 0.22.1 → 0.23.1

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 (59) hide show
  1. package/assets/helps/collection-skills.md +110 -1
  2. package/assets/helps/custom-view.md +38 -0
  3. package/assets/helps/error-recovery.md +36 -0
  4. package/assets/skills-preset/mc-zenn/SKILL.md +31 -0
  5. package/dist/collection/core/queryZ.d.ts +90 -0
  6. package/dist/collection/core/schema.d.ts +14 -1
  7. package/dist/collection/core/schemaZ.d.ts +18 -1
  8. package/dist/collection/index.cjs +3 -2
  9. package/dist/collection/index.cjs.map +1 -1
  10. package/dist/collection/index.d.ts +1 -0
  11. package/dist/collection/index.js +3 -3
  12. package/dist/collection/index.js.map +1 -1
  13. package/dist/collection/registry/server/index.cjs +12 -4
  14. package/dist/collection/registry/server/index.cjs.map +1 -1
  15. package/dist/collection/registry/server/index.js +12 -4
  16. package/dist/collection/registry/server/index.js.map +1 -1
  17. package/dist/collection/server/csvQuery.d.ts +27 -0
  18. package/dist/collection/server/csvStore.d.ts +51 -0
  19. package/dist/collection/server/discoveredCollection.d.ts +9 -1
  20. package/dist/collection/server/discovery.d.ts +7 -3
  21. package/dist/collection/server/index.cjs +17 -2
  22. package/dist/collection/server/index.d.ts +6 -0
  23. package/dist/collection/server/index.js +3 -3
  24. package/dist/collection/server/jsonlQuery.d.ts +4 -0
  25. package/dist/collection/server/manageTool.d.ts +4 -0
  26. package/dist/collection/server/store.d.ts +31 -0
  27. package/dist/collection-watchers/index.cjs +104 -3
  28. package/dist/collection-watchers/index.cjs.map +1 -1
  29. package/dist/collection-watchers/index.js +102 -3
  30. package/dist/collection-watchers/index.js.map +1 -1
  31. package/dist/feeds/index.cjs +2 -2
  32. package/dist/feeds/index.js +2 -2
  33. package/dist/feeds/server/index.cjs +3 -3
  34. package/dist/feeds/server/index.js +3 -3
  35. package/dist/google/auth.d.ts +18 -3
  36. package/dist/google/authFlow.d.ts +1 -0
  37. package/dist/google/calendar.d.ts +44 -0
  38. package/dist/google/index.cjs +165 -47
  39. package/dist/google/index.cjs.map +1 -1
  40. package/dist/google/index.d.ts +2 -2
  41. package/dist/google/index.js +159 -48
  42. package/dist/google/index.js.map +1 -1
  43. package/dist/{ingestTypes-DEjpiZGM.js → ingestTypes-B-dXxUF9.js} +2 -2
  44. package/dist/{ingestTypes-DEjpiZGM.js.map → ingestTypes-B-dXxUF9.js.map} +1 -1
  45. package/dist/{ingestTypes-Dh5lniBL.cjs → ingestTypes-Cev9q77C.cjs} +2 -2
  46. package/dist/{ingestTypes-Dh5lniBL.cjs.map → ingestTypes-Cev9q77C.cjs.map} +1 -1
  47. package/dist/{promptSafety-DN5V__Ku.cjs → promptSafety-DRd15gQ6.cjs} +15 -1
  48. package/dist/promptSafety-DRd15gQ6.cjs.map +1 -0
  49. package/dist/{promptSafety-DyG3EuC7.js → promptSafety-rDWA9_JS.js} +10 -2
  50. package/dist/promptSafety-rDWA9_JS.js.map +1 -0
  51. package/dist/{server-Bd8l1bhv.js → server-D2ibbixF.js} +772 -79
  52. package/dist/server-D2ibbixF.js.map +1 -0
  53. package/dist/{server-Bh-DPK-H.cjs → server-Jaogu3Ky.cjs} +860 -76
  54. package/dist/server-Jaogu3Ky.cjs.map +1 -0
  55. package/package.json +3 -1
  56. package/dist/promptSafety-DN5V__Ku.cjs.map +0 -1
  57. package/dist/promptSafety-DyG3EuC7.js.map +0 -1
  58. package/dist/server-Bd8l1bhv.js.map +0 -1
  59. package/dist/server-Bh-DPK-H.cjs.map +0 -1
@@ -126,7 +126,8 @@ skipped, never crashes the host):
126
126
  | ---------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
127
127
  | `title` | Human name shown in the sidebar / header. Required. |
128
128
  | `icon` | A **Material Symbols** name (`receipt_long`, `people`, `schedule`, `menu_book`). Required. |
129
- | `dataPath` | Workspace-relative records folder, e.g. `data/recipes/items`. Must stay under the workspace. Required. |
129
+ | `dataPath` | Workspace-relative records folder, e.g. `data/recipes/items`. Must stay under the workspace. Required — unless `dataSource` is set (declare exactly ONE of the two). |
130
+ | `dataSource` | Optional. `{ "type": "csv", "path": "data/students.csv" }` — the records ARE the rows of an external data file (workspace-relative, containment-checked like `dataPath`). Makes the collection **read-only** in every UI/tool write path; see "External data (CSV) collections" below. Mutually exclusive with `dataPath`, `singleton`, `ingest`, `spawn`, and `mutate` actions. |
130
131
  | `primaryKey` | The field name whose value is the filename. That field MUST set `primary: true`. The value must be a valid record id (see the **Records** section's id-charset rule). Required. |
131
132
  | `singleton` | Optional. When set, at most one record exists, pinned to this exact id (e.g. `me`). Host pre-fills + locks the create form and hides Add once it exists. |
132
133
  | `fields` | Ordered map of field-name → field spec. **Insertion order = column order** in the table. Required. |
@@ -853,6 +854,13 @@ records through **`manageCollection`**, not raw file I/O:
853
854
  / `fields` on large collections to keep the result small — e.g.
854
855
  `fields: ["id"]` to check for an id collision before an add.
855
856
  - **Delete** — remove the record file (`manageCollection` has no delete).
857
+ - **Aggregate — `queryItems`.** Counts, sums, averages, group-bys on ANY
858
+ collection via a structured query (`groupBy` / `aggregates` / `where` /
859
+ `orderBy` / `limit` — full shape in the "External data (CSV) collections"
860
+ section below). On file-backed collections it aggregates the ENRICHED
861
+ records, so computed fields (`derived` / `rollup` / `toggle`) are
862
+ queryable columns — "sum of invoice totals" works even when `total` is a
863
+ formula. Prefer it over doing arithmetic on `getItems` output.
856
864
  - **Cross-collection questions — `getOntology`.** Returns every collection in
857
865
  the workspace with its `primaryKey`, effective `displayField`, record count,
858
866
  and its `ref` / `embed` / `backlinks` / `rollup` relations (field → related slug, including refs
@@ -905,6 +913,107 @@ directly:
905
913
  (This safety net applies after `putItems` batches too, but direct writes are
906
914
  where it earns its keep.)
907
915
 
916
+ ## External data (CSV) collections — `dataSource`
917
+
918
+ When the user has a data file they want to "manage" / "visualize" / "見たい"
919
+ (a student roster, an HR export, a product list — the BI use case), do NOT
920
+ import the rows into record files. Define a collection **on top of** the file
921
+ with `dataSource` — the file stays the single source of truth and the whole
922
+ collection UI (table, kanban, calendar, custom views, remote views) works over
923
+ its rows via the host's DuckDB-backed CSV store.
924
+
925
+ **The schema-inference recipe** (user: "この CSV を管理したい" / "make this CSV a
926
+ collection"):
927
+
928
+ 1. **Inspect the file** — Read the first ~30 lines. Note the header row
929
+ (column names), each column's apparent type, and which column uniquely
930
+ identifies a row.
931
+ 2. **Pick the key column** — set `primaryKey` to that column's name and flag
932
+ its field `primary: true`. Prefer an ID-ish column (student number, SKU,
933
+ email) over a name. Check for duplicates if unsure — duplicated key values
934
+ don't error, but the LAST row silently wins.
935
+ **Declare the key field as `type: "string"` even when the column is
936
+ numeric** (a row number, an integer ID): record ids are strings, and the
937
+ store overwrites the key field's value with the id — a `number`-typed key
938
+ would just hold a string anyway. Consequence to keep in mind: sorting by
939
+ the key column is lexicographic ("10" before "2"); if numeric ordering
940
+ matters to the user, sort by another column.
941
+ 3. **Declare `fields` matching the column names** — field name = CSV column
942
+ name, verbatim (Japanese column names are fine). Only declared fields
943
+ render as table columns; extra CSV columns still ride along in the record
944
+ detail. Use `number` / `date` / `enum` (when a column has a small closed
945
+ value set) / `string` for the rest — DuckDB sniffs the raw types, the
946
+ field spec controls rendering.
947
+ 4. **Set `displayField`** to the most human-readable column (a name). This
948
+ matters extra here: a key value that isn't a safe record id (Japanese
949
+ text, spaces) is hex-encoded into the record's address, and
950
+ `displayField` is what keeps lists and notifications readable.
951
+ 5. **Write the schema** with `dataSource` instead of `dataPath`, plus a
952
+ normal `SKILL.md`, under `data/skills/<slug>/` — same create flow as any
953
+ collection. In the SKILL body, point aggregation questions at
954
+ `manageCollection` `queryItems` (see below) — NOT at python/pandas and
955
+ NOT at `getItems`; SKILL.md text outlives help updates, so a wrong
956
+ steer here misroutes every future session on this collection.
957
+
958
+ Semantics to remember (and to tell the user):
959
+
960
+ - **Read-only** — no Add/Edit/Delete in the UI, `putItems` refuses, HTTP
961
+ writes answer 405. To change the data, **edit or replace the file itself**
962
+ (you can do that with the normal file tools when asked); open views refresh
963
+ automatically via a file watcher.
964
+ - **Encoding** — Shift_JIS / CP932 and UTF-16 files work as-is; the host
965
+ decodes to a cache copy and never rewrites the user's file. Don't convert
966
+ the file to UTF-8 "to be safe" — an Excel re-export would just undo it.
967
+ - **Row cap** — `getItems` / the UI list stops at 5,000 rows (a warn is
968
+ logged). Fine for browsing — but NEVER compute an aggregate from
969
+ `getItems` output on a large file (a capped scan gives a silently wrong
970
+ number). Use `queryItems` instead.
971
+ - **Aggregation — `manageCollection` `queryItems`**: a structured query over
972
+ the WHOLE file (uncapped scan, DuckDB underneath). Answer counts / sums /
973
+ averages / group-bys with it — don't shell out to python/pandas for
974
+ questions it covers. (It works on EVERY collection — see the Records
975
+ section; this bullet is about the dataSource specifics.) Shape:
976
+
977
+ ```json
978
+ {
979
+ "groupBy": ["Category"],
980
+ "aggregates": { "total": { "op": "sum", "column": "Price" }, "n": { "op": "count" } },
981
+ "where": [{ "field": "Availability", "op": "eq", "value": "in_stock" }],
982
+ "orderBy": [{ "field": "total", "dir": "desc" }],
983
+ "limit": 100
984
+ }
985
+ ```
986
+
987
+ Ops: `count` (column optional) / `sum` / `avg` / `min` / `max`; `where`
988
+ ops are the familiar `eq/ne/in/gt/gte/lt/lte/contains`; `orderBy` sorts
989
+ by a groupBy column or an aggregate alias; result rows are clamped
990
+ (default 1,000). At least one of `groupBy`/`aggregates` is required.
991
+ `sum`/`avg` skip non-numeric cells. A custom view can run the same
992
+ query shape via `POST <dataUrl>/query` with its read token — see
993
+ `config/helps/custom-view.md` — which is how dataSource dashboards
994
+ chart live data.
995
+ - **Not registry material** — dataSource collections can't be imported from
996
+ or contributed to a registry (the data file is machine-local).
997
+
998
+ Minimal example (Japanese roster, Shift_JIS file dropped at
999
+ `data/students.csv`):
1000
+
1001
+ ```json
1002
+ {
1003
+ "title": "生徒名簿",
1004
+ "icon": "school",
1005
+ "dataSource": { "type": "csv", "path": "data/students.csv" },
1006
+ "primaryKey": "学籍番号",
1007
+ "displayField": "氏名",
1008
+ "fields": {
1009
+ "学籍番号": { "type": "string", "label": "学籍番号", "primary": true },
1010
+ "氏名": { "type": "string", "label": "氏名" },
1011
+ "学年": { "type": "enum", "label": "学年", "values": ["1", "2", "3"] },
1012
+ "入学日": { "type": "date", "label": "入学日" }
1013
+ }
1014
+ }
1015
+ ```
1016
+
908
1017
  ## End-to-end: creating a new collection skill
909
1018
 
910
1019
  1. Pick a `<slug>` (lowercase-hyphen, no `mc-` prefix) and a `dataPath`
@@ -101,6 +101,44 @@ toggles, embeds) — the same numbers the user sees elsewhere.
101
101
  / shows one record at a time. Combinable with `fields`.
102
102
  - The primary key is always returned regardless of `fields`.
103
103
 
104
+ ### Aggregation queries (`read` capability)
105
+
106
+ A view can run structured aggregations over the collection — on a
107
+ `dataSource` (CSV) collection this scans the WHOLE file (the record read
108
+ above is row-capped at 5,000 there, so an aggregate computed from it would
109
+ be silently wrong); on a file-backed collection it aggregates the ENRICHED
110
+ records, so computed fields (`derived` / `rollup` / `toggle`) are queryable
111
+ columns:
112
+
113
+ ```js
114
+ const res = await fetch(dataUrl + "/query", {
115
+ method: "POST",
116
+ headers: { Authorization: "Bearer " + token, "Content-Type": "application/json" },
117
+ body: JSON.stringify({
118
+ query: {
119
+ groupBy: ["Category"],
120
+ aggregates: { total: { op: "sum", column: "Price" }, n: { op: "count" } },
121
+ where: [{ field: "Availability", op: "eq", value: "in_stock" }],
122
+ orderBy: [{ field: "total", dir: "desc" }],
123
+ limit: 100,
124
+ },
125
+ }),
126
+ });
127
+ const { rows } = await res.json(); // [{ Category, total, n }, ...] — chart these
128
+ ```
129
+
130
+ - Ops: `count` (column optional) / `sum` / `avg` / `min` / `max`; `where`
131
+ ops: `eq/ne/in/gt/gte/lt/lte/contains`; at least one of
132
+ `groupBy`/`aggregates`; `orderBy` sorts by a groupBy column or an
133
+ aggregate alias; result rows clamp at 1,000 by default.
134
+ - Structured JSON only — there is **no SQL surface**, by design.
135
+ - Combine with `onChange` (below) to stay live: in the callback, re-run
136
+ **this POST query** (wrap it in your own `refresh()` and register that)
137
+ — NOT the `?fields=` record read from the section above, which would
138
+ silently repaint an aggregation view with non-aggregated data. A
139
+ replaced CSV or edited records then redraw the chart — that's a live
140
+ dashboard.
141
+
104
142
  ### Writing records (only with the `write` capability)
105
143
 
106
144
  ```js
@@ -183,6 +183,36 @@ plugin via the `/skills` UI to refresh both the tgz and the ledger.
183
183
  A version skew on a peer dep means the plugin was built against an
184
184
  older host — bump the plugin via the Discover tab's update flow.
185
185
 
186
+ ## dataSource (CSV) collection reads fail — "DuckDB is unavailable on this host"
187
+
188
+ A collection whose schema declares `dataSource` (external CSV) reads its rows
189
+ through `@duckdb/node-api`, a NATIVE module with per-platform prebuilt
190
+ bindings. When the binding is missing or fails to load, ONLY dataSource
191
+ collections break (every file-backed collection keeps working) and reads
192
+ fail with `DuckDB is unavailable on this host (@duckdb/node-api failed to
193
+ load: …)`.
194
+
195
+ Diagnosis + fixes, in order:
196
+
197
+ 1. **Reinstall dependencies** — `yarn install` at the app root. The usual
198
+ cause is an install that skipped the platform package
199
+ (`@duckdb/node-bindings-<platform>-<arch>`), e.g. after copying
200
+ `node_modules` between machines or architectures (an arm64 → x64 Docker
201
+ volume mount does exactly this).
202
+ 2. **Check the platform is supported** — `ls node_modules/@duckdb/ | cat`.
203
+ You should see a `node-bindings-<your platform>` package. If DuckDB ships
204
+ no prebuilt binding for the host (rare: musl/alpine, exotic arch), there
205
+ is no local fix — tell the user dataSource collections need a supported
206
+ platform (glibc Linux x64/arm64, macOS, Windows) and that their other
207
+ collections are unaffected.
208
+ 3. **Docker**: make sure the image installs dependencies INSIDE the
209
+ container (matching libc/arch) rather than bind-mounting a host
210
+ `node_modules`.
211
+
212
+ Related, not an error: a dataSource CSV in Shift_JIS / UTF-16 is decoded
213
+ automatically to a cache copy under the OS temp dir — never "fix" the
214
+ user's file by re-encoding it.
215
+
186
216
  ## Fallback
187
217
 
188
218
  If none of the above matches the failing tool output:
@@ -308,6 +338,7 @@ shipped builders produce, and asserts `handlePermission` comes back over the MCP
308
338
  - **"multiple client_secret_*.json files found"**.
309
339
  - **"Google Calendar API: HTTP 403"** with a hint about enabling the API.
310
340
  - **"could not obtain a Google access token"** (grant revoked).
341
+ - `calendarListCalendars` / non-primary calendar lookups fail with **HTTP 401/403 / insufficient scope**, or the list comes back empty even though the user has other calendars.
311
342
 
312
343
  ### Cause
313
344
 
@@ -328,6 +359,11 @@ Two ways the link can be minted, and the tool picks automatically:
328
359
  - **Not linked / grant revoked** — ask the user to link (or re-link) the account from this app's
329
360
  settings, then retry. Do NOT tell them to create a Google Cloud project, and do NOT try to
330
361
  create or edit the token file yourself.
362
+ - **Calendar-list / non-primary lookup fails with insufficient scope (or lists nothing)** — the
363
+ account was linked before the calendar-list read scope (`calendar.calendarlist.readonly`) was
364
+ added, so the stored grant predates it. Ask the user to re-link from settings to add the scope,
365
+ then retry. Reading events on a non-primary calendar by id needs no re-link once the calendar
366
+ list is visible.
331
367
  - **Sign-in service unreachable / HTTP error** — the broker is down or the network is blocked.
332
368
  It is only needed to link and to renew an expired access token; ask the user to retry shortly.
333
369
  (A user with their own `~/.secrets/client_secret_*.json` never depends on it.)
@@ -43,9 +43,11 @@ unless the user already told you:
43
43
 
44
44
  - **Clone an existing Zenn repo** (they already write Zenn on GitHub). Get the
45
45
  repo URL, then:
46
+
46
47
  ```bash
47
48
  git clone <url> github/zenn
48
49
  ```
50
+
49
51
  `npx zenn` fetches zenn-cli on demand, so a missing local dependency is fine.
50
52
 
51
53
  - **Create a fresh project** (standard zenn-cli flow):
@@ -90,6 +92,7 @@ fill it in. **A published slug becomes the article URL and can't change** — pi
90
92
  it deliberately.
91
93
 
92
94
  **Step 3 — write the frontmatter** (Zenn house style):
95
+
93
96
  ```yaml
94
97
  ---
95
98
  title: "<a clear title, in the user's language>"
@@ -99,6 +102,7 @@ topics: ["MulmoClaude", "<related>"]
99
102
  published: true
100
103
  ---
101
104
  ```
105
+
102
106
  - **Always include `MulmoClaude` in `topics`.** At most 5 topics, no spaces
103
107
  inside a single topic.
104
108
  - `type` defaults to `tech`; `published` defaults to `true` (use `false` when
@@ -109,6 +113,33 @@ implementation (fenced ` ```lang ` blocks, runnable commands) → result → a s
109
113
  wrap-up. Informative but casual; describe the work, not yourself. Put images in
110
114
  `github/zenn/images/` and reference them as `![alt](/images/<file>)`.
111
115
 
116
+ Write it the way a person would, not the way a model defaults to. Each rule
117
+ below comes from real reader feedback — ignore them and the draft reads as
118
+ machine output:
119
+
120
+ - **Sound human, not like a generated listicle.** Prefer flowing prose over
121
+ walls of bullets, use bold sparingly, and let paragraphs connect into an
122
+ argument. If every section is just a heading plus a list, vary the rhythm.
123
+ - **No uncommon loanwords or jargon.** Use only terms a typical reader already
124
+ knows. Niche English acronyms, trendy katakana, and insider slang either get
125
+ dropped or replaced with plain words. A term the field itself hasn't settled
126
+ on ("code smell" and the like) is a red flag — say the plain thing instead
127
+ ("code that works now but bites you later").
128
+ - **Define every term and acronym on first use**, in plain language, and put the
129
+ foundational ones up front — define "DRY" before the article leans on it. A
130
+ reader should never hit a word they can't parse.
131
+ - **Carry a source's essence, don't just cite it.** When an idea comes from a
132
+ book or article, explain the idea itself in your own words; "see <book>"
133
+ teaches nothing. Search for the actual content when you're unsure what it says,
134
+ then write the substance.
135
+ - **Make the article self-contained.** Anything project-specific — a repo's
136
+ internals, a config choice, a domain constraint — needs enough general
137
+ background that a reader with zero context can follow. Running longer is fine
138
+ when the extra length buys understanding.
139
+ - **Match the stated audience.** "Explain to intermediate developers" means
140
+ patient explanations, concrete examples, real code from the work, and links —
141
+ not a terse summary.
142
+
112
143
  **Step 5 — save + preview.** Write `github/zenn/articles/<slug>.md`. Tell the
113
144
  user the path and how to preview: `cd github/zenn && npx zenn preview`
114
145
  (http://localhost:8000). Surface the title, slug, and topics.
@@ -0,0 +1,90 @@
1
+ import { z } from 'zod';
2
+ /** Hard ceiling on returned rows; `limit` clamps below it. A group-by on
3
+ * a near-unique column would otherwise return one row per source row —
4
+ * the exact materialization the aggregate path exists to avoid. */
5
+ export declare const MAX_QUERY_ROWS = 10000;
6
+ /** Default row cap when the query declares no `limit`. */
7
+ export declare const DEFAULT_QUERY_ROWS = 1000;
8
+ /** Cap on aggregate expressions per query — a record shape has no
9
+ * intrinsic size limit, and thousands of expressions would make one
10
+ * full-file scan arbitrarily wide. */
11
+ export declare const MAX_QUERY_AGGREGATES = 32;
12
+ /** One aggregate column: `count` (rows; `column` optional to count
13
+ * non-null cells) or `sum`/`avg`/`min`/`max` over a named CSV column. */
14
+ export declare const QueryAggregateZ: z.ZodObject<{
15
+ op: z.ZodEnum<{
16
+ sum: "sum";
17
+ count: "count";
18
+ avg: "avg";
19
+ min: "min";
20
+ max: "max";
21
+ }>;
22
+ column: z.ZodOptional<z.ZodString>;
23
+ }, z.core.$strip>;
24
+ /** One filter condition. Same op vocabulary as the schema-level `where`
25
+ * (`core/where.ts`) so authors learn one set; values may be typed
26
+ * (number / boolean) since CSV columns are. `in` requires an array
27
+ * value, every other op a scalar. */
28
+ export declare const QueryWhereZ: z.ZodObject<{
29
+ field: z.ZodString;
30
+ op: z.ZodEnum<{
31
+ in: "in";
32
+ eq: "eq";
33
+ ne: "ne";
34
+ gt: "gt";
35
+ gte: "gte";
36
+ lt: "lt";
37
+ lte: "lte";
38
+ contains: "contains";
39
+ }>;
40
+ value: z.ZodUnion<readonly [z.ZodString, z.ZodNumber, z.ZodBoolean, z.ZodArray<z.ZodUnion<readonly [z.ZodString, z.ZodNumber, z.ZodBoolean]>>]>;
41
+ }, z.core.$strip>;
42
+ export declare const QueryOrderZ: z.ZodObject<{
43
+ field: z.ZodString;
44
+ dir: z.ZodOptional<z.ZodEnum<{
45
+ asc: "asc";
46
+ desc: "desc";
47
+ }>>;
48
+ }, z.core.$strip>;
49
+ /** The whole query. At least one of `groupBy` / `aggregates` must be
50
+ * present: bare `groupBy` is a DISTINCT listing, bare `aggregates` a
51
+ * whole-file scalar row, together a grouped aggregation. */
52
+ export declare const CollectionQueryZ: z.ZodObject<{
53
+ groupBy: z.ZodOptional<z.ZodArray<z.ZodString>>;
54
+ aggregates: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodObject<{
55
+ op: z.ZodEnum<{
56
+ sum: "sum";
57
+ count: "count";
58
+ avg: "avg";
59
+ min: "min";
60
+ max: "max";
61
+ }>;
62
+ column: z.ZodOptional<z.ZodString>;
63
+ }, z.core.$strip>>>;
64
+ where: z.ZodOptional<z.ZodArray<z.ZodObject<{
65
+ field: z.ZodString;
66
+ op: z.ZodEnum<{
67
+ in: "in";
68
+ eq: "eq";
69
+ ne: "ne";
70
+ gt: "gt";
71
+ gte: "gte";
72
+ lt: "lt";
73
+ lte: "lte";
74
+ contains: "contains";
75
+ }>;
76
+ value: z.ZodUnion<readonly [z.ZodString, z.ZodNumber, z.ZodBoolean, z.ZodArray<z.ZodUnion<readonly [z.ZodString, z.ZodNumber, z.ZodBoolean]>>]>;
77
+ }, z.core.$strip>>>;
78
+ orderBy: z.ZodOptional<z.ZodArray<z.ZodObject<{
79
+ field: z.ZodString;
80
+ dir: z.ZodOptional<z.ZodEnum<{
81
+ asc: "asc";
82
+ desc: "desc";
83
+ }>>;
84
+ }, z.core.$strip>>>;
85
+ limit: z.ZodOptional<z.ZodNumber>;
86
+ }, z.core.$strip>;
87
+ export type CollectionQueryAggregate = z.infer<typeof QueryAggregateZ>;
88
+ export type CollectionQueryWhere = z.infer<typeof QueryWhereZ>;
89
+ export type CollectionQueryOrder = z.infer<typeof QueryOrderZ>;
90
+ export type CollectionQuery = z.infer<typeof CollectionQueryZ>;
@@ -1,5 +1,5 @@
1
1
  import { z } from 'zod';
2
- import { ActionSpecZ, CollectionSchemaZ, CustomViewZ, DynamicIconRuleZ, DynamicIconSourceZ, DynamicIconSpecZ, EveryFieldDrivenZ, EveryLiteralZ, EveryZ, FieldSpecZ, IngestZ, SpawnZ, SubFieldSpecZ, WhenZ } from './schemaZ';
2
+ import { ActionSpecZ, CollectionSchemaZ, CustomViewZ, DataSourceZ, DynamicIconRuleZ, DynamicIconSourceZ, DynamicIconSpecZ, EveryFieldDrivenZ, EveryLiteralZ, EveryZ, FieldSpecZ, IngestZ, SpawnZ, SubFieldSpecZ, WhenZ } from './schemaZ';
3
3
  /** Minimal "this collection is a feed" descriptor carried on the schema.
4
4
  * Deliberately narrow — the canonical collection contract stays
5
5
  * independent of the host's feeds subsystem. The host's richer retrieval
@@ -138,14 +138,27 @@ export type DynamicIconSpec = z.infer<typeof DynamicIconSpecZ>;
138
138
  * union on `kind` (declarative retrievers | agent worker). The feeds
139
139
  * subsystem's `IngestSpec` is the same union under its historical name. */
140
140
  export type CollectionIngestSpec = z.infer<typeof IngestZ>;
141
+ /** The `dataSource` block: this collection's records are the rows of an
142
+ * external read-only data file (v1: CSV). See `DataSourceZ`. */
143
+ export type CollectionDataSource = z.infer<typeof DataSourceZ>;
141
144
  /** The whole `schema.json` contract. Key-level docs live on
142
145
  * `CollectionSchemaZ` in `./schemaZ`. */
143
146
  export type CollectionSchema = z.infer<typeof CollectionSchemaZ>;
147
+ /** True when `schema` declares an external `dataSource` — i.e. the
148
+ * collection is READ-ONLY through every UI/tool write path (updates
149
+ * happen by editing/replacing the data file itself). Isomorphic: both
150
+ * the server write guards and the client's control hiding key off this
151
+ * one predicate. */
152
+ export declare function isReadOnlySchema(schema: Pick<CollectionSchema, "dataSource">): boolean;
144
153
  export interface CollectionSummary {
145
154
  slug: string;
146
155
  title: string;
147
156
  icon: string;
148
157
  source: CollectionSource;
158
+ /** Present (true) when the collection is backed by an external
159
+ * `dataSource` and therefore read-only in every UI/tool write path.
160
+ * Absent-when-writable, matching the other optional summary flags. */
161
+ readonly?: true;
149
162
  /** Slugs of the source collection(s) a `dynamicIcon` icon was computed
150
163
  * from — present only when `schema.dynamicIcon` is set. Lets a client
151
164
  * know which collection change-channel(s) to watch for a live icon
@@ -608,10 +608,27 @@ export declare const DynamicIconSpecZ: z.ZodObject<{
608
608
  }, z.core.$strip>>;
609
609
  fallback: z.ZodOptional<z.ZodString>;
610
610
  }, z.core.$strip>;
611
+ /** External-data collection: the records ARE the rows of a user-supplied
612
+ * data file (v1: CSV), queried through DuckDB — never copied into
613
+ * `<dataDir>/<id>.json` files. Declaring `dataSource` makes the collection
614
+ * **read-only** in every UI/tool write path; updates happen by replacing /
615
+ * editing the file itself (file-watch republishes the views). `path` is
616
+ * workspace-relative and containment-checked exactly like `dataPath`. The
617
+ * row-id column is the schema's existing `primaryKey` — there is
618
+ * deliberately no second key concept here.
619
+ * See plans/feat-collection-csv-duckdb-source.md. */
620
+ export declare const DataSourceZ: z.ZodObject<{
621
+ type: z.ZodLiteral<"csv">;
622
+ path: z.ZodString;
623
+ }, z.core.$strip>;
611
624
  export declare const CollectionSchemaZ: z.ZodObject<{
612
625
  title: z.ZodString;
613
626
  icon: z.ZodString;
614
- dataPath: z.ZodString;
627
+ dataPath: z.ZodOptional<z.ZodString>;
628
+ dataSource: z.ZodOptional<z.ZodObject<{
629
+ type: z.ZodLiteral<"csv">;
630
+ path: z.ZodString;
631
+ }, z.core.$strip>>;
615
632
  primaryKey: z.ZodString;
616
633
  singleton: z.ZodOptional<z.ZodString>;
617
634
  fields: z.ZodRecord<z.ZodString, z.ZodDiscriminatedUnion<[z.ZodObject<{
@@ -1,5 +1,5 @@
1
1
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
- const require_promptSafety = require("../promptSafety-DN5V__Ku.cjs");
2
+ const require_promptSafety = require("../promptSafety-DRd15gQ6.cjs");
3
3
  const require_errorMessage = require("../errorMessage--LvXe_Jx.cjs");
4
4
  //#region src/collection/core/presentCollection.ts
5
5
  var TOOL_NAME = "presentCollection";
@@ -38,7 +38,7 @@ var executePresentCollection = async (_context, args) => {
38
38
  message: `Presented collection ${itemId ? `${collectionSlug} / ${itemId}` : collectionSlug}`,
39
39
  data,
40
40
  jsonData: data,
41
- instructions: "The collection has been presented to the user as an interactive card. They can browse, open, edit, create, and delete records directly. No further action is needed unless they ask."
41
+ instructions: "The collection has been presented to the user as an interactive card. They can browse and open records directly; on a writable collection they can also edit, create, and delete (a read-only dataSource collection shows no edit controls — its records change by editing the backing data file). No further action is needed unless they ask."
42
42
  };
43
43
  };
44
44
  //#endregion
@@ -478,6 +478,7 @@ exports.fieldVisible = require_promptSafety.fieldVisible;
478
478
  exports.firstDateField = require_promptSafety.firstDateField;
479
479
  exports.firstMissingRequiredField = firstMissingRequiredField;
480
480
  exports.isFieldDrivenEvery = require_promptSafety.isFieldDrivenEvery;
481
+ exports.isReadOnlySchema = require_promptSafety.isReadOnlySchema;
481
482
  exports.isSafeRecordId = require_promptSafety.isSafeRecordId;
482
483
  exports.isSafeSlug = require_promptSafety.isSafeSlug;
483
484
  exports.isSortableField = isSortableField;