@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
@@ -1,5 +1,5 @@
1
1
  const require_rolldown_runtime = require("./rolldown-runtime-D6vf50IK.cjs");
2
- const require_promptSafety = require("./promptSafety-DN5V__Ku.cjs");
2
+ const require_promptSafety = require("./promptSafety-DRd15gQ6.cjs");
3
3
  const require_collection_paths = require("./collection/paths.cjs");
4
4
  const require_skill_bridge_index = require("./skill-bridge/index.cjs");
5
5
  let node_path = require("node:path");
@@ -7,6 +7,9 @@ node_path = require_rolldown_runtime.__toESM(node_path, 1);
7
7
  let node_fs = require("node:fs");
8
8
  let node_fs_promises = require("node:fs/promises");
9
9
  let node_crypto = require("node:crypto");
10
+ let node_os = require("node:os");
11
+ let iconv_lite = require("iconv-lite");
12
+ iconv_lite = require_rolldown_runtime.__toESM(iconv_lite, 1);
10
13
  let zod = require("zod");
11
14
  //#region src/collection/server/host.ts
12
15
  var current = null;
@@ -574,7 +577,7 @@ function promptPathsFor(collection, workspaceRoot) {
574
577
  const skillDir = (rel === "" || rel.startsWith("..") ? collection.skillDir : rel).split(node_path.default.sep).join("/");
575
578
  return {
576
579
  slug: collection.slug,
577
- dataPath: collection.schema.dataPath,
580
+ dataPath: collection.schema.dataPath ?? collection.schema.dataSource?.path ?? "",
578
581
  skillDir
579
582
  };
580
583
  }
@@ -637,6 +640,586 @@ ${dataJson}
637
640
  ${templateText}`;
638
641
  }
639
642
  //#endregion
643
+ //#region src/collection/core/queryZ.ts
644
+ /** Result-column aliases double as SQL identifiers and JSON keys — keep
645
+ * them to a conservative identifier charset so neither side needs
646
+ * escaping gymnastics. */
647
+ var SAFE_ALIAS_PATTERN = /^[A-Za-z_]\w{0,63}$/;
648
+ /** Hard ceiling on returned rows; `limit` clamps below it. A group-by on
649
+ * a near-unique column would otherwise return one row per source row —
650
+ * the exact materialization the aggregate path exists to avoid. */
651
+ var MAX_QUERY_ROWS = 1e4;
652
+ /** Default row cap when the query declares no `limit`. */
653
+ var DEFAULT_QUERY_ROWS = 1e3;
654
+ /** One aggregate column: `count` (rows; `column` optional to count
655
+ * non-null cells) or `sum`/`avg`/`min`/`max` over a named CSV column. */
656
+ var QueryAggregateZ = zod.z.object({
657
+ op: zod.z.enum([
658
+ "count",
659
+ "sum",
660
+ "avg",
661
+ "min",
662
+ "max"
663
+ ]),
664
+ column: zod.z.string().min(1).optional()
665
+ }).refine((aggregate) => aggregate.op === "count" || aggregate.column !== void 0, {
666
+ message: "`column` is required for every aggregate op except `count`",
667
+ path: ["column"]
668
+ });
669
+ /** One filter condition. Same op vocabulary as the schema-level `where`
670
+ * (`core/where.ts`) so authors learn one set; values may be typed
671
+ * (number / boolean) since CSV columns are. `in` requires an array
672
+ * value, every other op a scalar. */
673
+ var QueryWhereZ = zod.z.object({
674
+ field: zod.z.string().min(1),
675
+ op: zod.z.enum([
676
+ "eq",
677
+ "ne",
678
+ "in",
679
+ "gt",
680
+ "gte",
681
+ "lt",
682
+ "lte",
683
+ "contains"
684
+ ]),
685
+ value: zod.z.union([
686
+ zod.z.string(),
687
+ zod.z.number(),
688
+ zod.z.boolean(),
689
+ zod.z.array(zod.z.union([
690
+ zod.z.string(),
691
+ zod.z.number(),
692
+ zod.z.boolean()
693
+ ])).min(1).max(100)
694
+ ])
695
+ }).refine((cond) => cond.op === "in" === Array.isArray(cond.value), {
696
+ message: "`in` requires an array value (the allowed set); every other op requires a scalar value",
697
+ path: ["value"]
698
+ });
699
+ var QueryOrderZ = zod.z.object({
700
+ /** A `groupBy` column or an aggregate alias — membership enforced by
701
+ * the whole-query refine below. */
702
+ field: zod.z.string().min(1),
703
+ dir: zod.z.enum(["asc", "desc"]).optional()
704
+ });
705
+ /** The whole query. At least one of `groupBy` / `aggregates` must be
706
+ * present: bare `groupBy` is a DISTINCT listing, bare `aggregates` a
707
+ * whole-file scalar row, together a grouped aggregation. */
708
+ var CollectionQueryZ = zod.z.object({
709
+ groupBy: zod.z.array(zod.z.string().min(1)).max(8).refine((columns) => new Set(columns.map((column) => column.toLowerCase())).size === columns.length, { message: "`groupBy` columns must be unique (case-insensitively — SQL identifiers ignore case)" }).optional(),
710
+ aggregates: zod.z.record(zod.z.string().regex(SAFE_ALIAS_PATTERN, "aggregate aliases must be simple identifiers (letters/digits/underscore)"), QueryAggregateZ).optional(),
711
+ where: zod.z.array(QueryWhereZ).max(16).optional(),
712
+ orderBy: zod.z.array(QueryOrderZ).max(4).optional(),
713
+ limit: zod.z.number().int().min(1).max(MAX_QUERY_ROWS).optional()
714
+ }).refine((query) => (query.groupBy?.length ?? 0) > 0 || Object.keys(query.aggregates ?? {}).length > 0, {
715
+ message: "declare at least one of `groupBy` (columns to bucket by) or `aggregates` (values to compute)",
716
+ path: ["groupBy"]
717
+ }).refine((query) => Object.keys(query.aggregates ?? {}).length <= 32, {
718
+ message: `\`aggregates\` supports at most 32 entries`,
719
+ path: ["aggregates"]
720
+ }).refine((query) => {
721
+ const groupLower = new Set((query.groupBy ?? []).map((column) => column.toLowerCase()));
722
+ const seen = /* @__PURE__ */ new Set();
723
+ return Object.keys(query.aggregates ?? {}).every((alias) => {
724
+ const lower = alias.toLowerCase();
725
+ if (groupLower.has(lower) || seen.has(lower)) return false;
726
+ seen.add(lower);
727
+ return true;
728
+ });
729
+ }, {
730
+ message: "aggregate aliases must be unique and must not collide with `groupBy` column names (case-insensitively — SQL identifiers ignore case)",
731
+ path: ["aggregates"]
732
+ }).refine((query) => {
733
+ const sortable = /* @__PURE__ */ new Set([...query.groupBy ?? [], ...Object.keys(query.aggregates ?? {})]);
734
+ return (query.orderBy ?? []).every((order) => sortable.has(order.field));
735
+ }, {
736
+ message: "every `orderBy.field` must be a `groupBy` column or an aggregate alias",
737
+ path: ["orderBy"]
738
+ });
739
+ //#endregion
740
+ //#region src/collection/server/csvQuery.ts
741
+ /** Double-quote a SQL identifier (CSV column name / result alias). */
742
+ function quoteIdent(name) {
743
+ return `"${name.replaceAll("\"", "\"\"")}"`;
744
+ }
745
+ /** Single-quote a SQL string literal (a `types={...}` struct key). */
746
+ function quoteLiteral(value) {
747
+ return `'${value.replaceAll("'", "''")}'`;
748
+ }
749
+ /** The `read_csv` argument list shared by every CSV query: the (prepared)
750
+ * path plus a `types` pin forcing the key column to VARCHAR — without it
751
+ * DuckDB's sniffer turns `001` into BIGINT 1, so leading zeros vanish
752
+ * and distinct keys collapse. */
753
+ function readCsvArgs(primaryKey) {
754
+ return `?, types={${quoteLiteral(primaryKey)}: 'VARCHAR'}`;
755
+ }
756
+ /** One aggregate's SQL expression. `sum`/`avg` TRY_CAST to DOUBLE so a
757
+ * column the sniffer kept as VARCHAR (mixed values) aggregates over its
758
+ * numeric cells instead of erroring; non-numeric cells become NULL and
759
+ * are skipped — standard BI tolerance. `min`/`max` stay native (they are
760
+ * meaningful on strings and dates too). */
761
+ function aggregateExpr(aggregate) {
762
+ const { op, column } = aggregate;
763
+ if (op === "count") return column === void 0 ? "count(*)" : `count(${quoteIdent(column)})`;
764
+ if (op === "sum" || op === "avg") return `${op}(TRY_CAST(${quoteIdent(column ?? "")} AS DOUBLE))`;
765
+ return `${op}(${quoteIdent(column ?? "")})`;
766
+ }
767
+ /** One where condition → SQL fragment + its bound parameters. String
768
+ * equality compares against `CAST(col AS VARCHAR)` so a sniffer-typed
769
+ * column still matches its textual value; numeric/boolean values compare
770
+ * natively (DuckDB coerces the column side). */
771
+ function whereFragment(cond) {
772
+ const column = quoteIdent(cond.field);
773
+ const asText = `CAST(${column} AS VARCHAR)`;
774
+ if (cond.op === "in") {
775
+ const values = cond.value;
776
+ return {
777
+ sql: `${values.every((value) => typeof value === "string") ? asText : column} IN (${values.map(() => "?").join(", ")})`,
778
+ params: values
779
+ };
780
+ }
781
+ if (cond.op === "contains") return {
782
+ sql: `contains(${asText}, ?)`,
783
+ params: [String(cond.value)]
784
+ };
785
+ const operator = {
786
+ eq: "=",
787
+ ne: "<>",
788
+ gt: ">",
789
+ gte: ">=",
790
+ lt: "<",
791
+ lte: "<="
792
+ }[cond.op];
793
+ return {
794
+ sql: `${typeof cond.value === "string" && (cond.op === "eq" || cond.op === "ne") ? asText : column} ${operator} ?`,
795
+ params: [cond.value]
796
+ };
797
+ }
798
+ /** Compile a validated query against `fromSql` (a table-function call
799
+ * whose FIRST placeholder is the source path — the executor binds it).
800
+ * Returns the SQL and the where-value parameters that follow the path.
801
+ * Callers MUST have run `CollectionQueryZ` first; this function trusts
802
+ * the shape (aliases already charset-checked, orderBy membership already
803
+ * enforced). */
804
+ function compileQuery(query, fromSql) {
805
+ const groupBy = query.groupBy ?? [];
806
+ const aggregates = Object.entries(query.aggregates ?? {});
807
+ const selectList = [...groupBy.map(quoteIdent), ...aggregates.map(([alias, aggregate]) => `${aggregateExpr(aggregate)} AS ${quoteIdent(alias)}`)];
808
+ const where = (query.where ?? []).map(whereFragment);
809
+ const clauses = [`SELECT ${selectList.join(", ")}`, `FROM ${fromSql}`];
810
+ if (where.length > 0) clauses.push(`WHERE ${where.map((fragment) => fragment.sql).join(" AND ")}`);
811
+ if (groupBy.length > 0) clauses.push(`GROUP BY ${groupBy.map(quoteIdent).join(", ")}`);
812
+ const orderBy = (query.orderBy ?? []).map((order) => quoteIdent(order.field) + (order.dir === "desc" ? " DESC" : " ASC"));
813
+ if (orderBy.length > 0) clauses.push(`ORDER BY ${orderBy.join(", ")}`);
814
+ clauses.push(`LIMIT ${query.limit ?? 1e3}`);
815
+ return {
816
+ sql: clauses.join(" "),
817
+ params: where.flatMap((fragment) => fragment.params)
818
+ };
819
+ }
820
+ /** Compile against a CSV file (the dataSource store's engine). */
821
+ function compileCsvQuery(query, primaryKey) {
822
+ return compileQuery(query, `read_csv(${readCsvArgs(primaryKey)})`);
823
+ }
824
+ /** Compile against a JSONL file of ENRICHED records — the file-backed
825
+ * collections' engine (see `jsonlQuery.ts`). No VARCHAR key pin needed:
826
+ * enriched record ids are already strings. `sample_size=-1` makes the
827
+ * schema inference scan EVERY line — with the default sample, a sparse
828
+ * optional/derived field first appearing past the sample would not be
829
+ * inferred as a column and the query would binder-error on it (Codex P2
830
+ * on #2165). The full scan costs nothing extra here: aggregation reads
831
+ * the whole file anyway. */
832
+ function compileJsonlQuery(query) {
833
+ return compileQuery(query, `read_json(?, format='newline_delimited', sample_size=-1)`);
834
+ }
835
+ //#endregion
836
+ //#region src/collection/server/csvStore.ts
837
+ /** `list()` row cap. Over-cap files are truncated with a warn — the v1
838
+ * contract is "browse + per-record views", not full-table analytics. */
839
+ var MAX_CSV_ROWS = 5e3;
840
+ /** Record ids minted from non-safe key values: `id0x` + utf-8 hex. Raw key
841
+ * values that themselves match this pattern are ALSO encoded, so the
842
+ * encoded namespace never collides with a raw value (injective mapping). */
843
+ var ENCODED_ID_PATTERN = /^id0x([0-9a-f]+)$/;
844
+ /** A CSV key value → the record id it's addressed by. Safe values pass
845
+ * through untouched; everything else (and anything shaped like an encoded
846
+ * id) becomes `id0x<hex>`. Pure + exported for unit tests. */
847
+ function encodeCsvRecordId(rawKey) {
848
+ if (safeRecordId(rawKey) === rawKey && !ENCODED_ID_PATTERN.test(rawKey)) return rawKey;
849
+ return `id0x${Buffer.from(rawKey, "utf-8").toString("hex")}`;
850
+ }
851
+ /** A record id → the CSV key value to look up. Inverse of
852
+ * `encodeCsvRecordId` for encoded ids; anything else is already the raw
853
+ * value. Pure + exported for unit tests. */
854
+ function decodeCsvRecordId(itemId) {
855
+ const match = ENCODED_ID_PATTERN.exec(itemId);
856
+ if (!match) return itemId;
857
+ return Buffer.from(match[1], "hex").toString("utf-8");
858
+ }
859
+ /** Normalize one DuckDB JS value into a JSON-safe record value: BigInt →
860
+ * number (string beyond the safe range), DATE/TIMESTAMP → ISO string
861
+ * (date-only when the clock is exactly UTC midnight, matching the `date`
862
+ * field contract), exotic DuckDB values → their string form. Pure +
863
+ * exported for unit tests. */
864
+ function normalizeCsvValue(value) {
865
+ if (typeof value === "bigint") return value <= BigInt(Number.MAX_SAFE_INTEGER) && value >= BigInt(-Number.MAX_SAFE_INTEGER) ? Number(value) : value.toString();
866
+ if (value instanceof Date) {
867
+ const iso = value.toISOString();
868
+ return iso.endsWith("T00:00:00.000Z") ? iso.slice(0, 10) : iso;
869
+ }
870
+ if (value !== null && typeof value === "object") return String(value);
871
+ return value;
872
+ }
873
+ /** One raw DuckDB row → a CollectionItem, or null when the key cell is
874
+ * missing/empty (the row can't be addressed). The primaryKey field is
875
+ * OVERWRITTEN with the (possibly encoded) record id so `item[primaryKey]`
876
+ * and the record's address never drift — same invariant the file store's
877
+ * write path enforces. Pure + exported for unit tests. */
878
+ function csvRowToItem(row, primaryKey) {
879
+ const normalized = Object.fromEntries(Object.entries(row).map(([key, value]) => [key, normalizeCsvValue(value)]));
880
+ const rawKey = normalized[primaryKey];
881
+ if (rawKey === null || rawKey === void 0 || String(rawKey) === "") return null;
882
+ return {
883
+ ...normalized,
884
+ [primaryKey]: encodeCsvRecordId(String(rawKey))
885
+ };
886
+ }
887
+ /** Dedupe by record id, LAST row wins (matches `csvRead`'s last-match
888
+ * pick). Returns the surviving items in first-seen order. Pure +
889
+ * exported for unit tests. */
890
+ function dedupeByRecordId(items, primaryKey) {
891
+ const byId = /* @__PURE__ */ new Map();
892
+ for (const item of items) byId.set(String(item[primaryKey]), item);
893
+ return {
894
+ items: [...byId.values()],
895
+ duplicates: items.length - byId.size
896
+ };
897
+ }
898
+ /** True when a thrown DuckDB error is the `types` pin naming a column the
899
+ * CSV doesn't have — the schema/file-mismatch case the caller downgrades
900
+ * to "empty collection + warn" instead of a 500. */
901
+ function isMissingKeyColumnError(err) {
902
+ return String(err).includes("do not exist in the CSV");
903
+ }
904
+ /** Bytes sniffed for UTF-8 validity. The trailing 3 bytes of the sample
905
+ * are dropped so a multibyte char split at the boundary can't produce a
906
+ * false negative on a valid file. */
907
+ var SNIFF_BYTES = 1024 * 1024;
908
+ function isValidUtf8(buf) {
909
+ try {
910
+ new TextDecoder("utf-8", { fatal: true }).decode(buf);
911
+ return true;
912
+ } catch {
913
+ return false;
914
+ }
915
+ }
916
+ /** Detect the (best-effort) encoding of a non-UTF-8 buffer. BOMs decide
917
+ * UTF-16; otherwise cp932 (the Shift_JIS superset — Excel-exported
918
+ * Japanese CSVs are the primary non-UTF-8 case this feature serves). */
919
+ function fallbackEncoding(buf) {
920
+ if (buf.length >= 2 && buf[0] === 255 && buf[1] === 254) return "utf-16le";
921
+ if (buf.length >= 2 && buf[0] === 254 && buf[1] === 255) return "utf-16be";
922
+ return "cp932";
923
+ }
924
+ function cacheDir() {
925
+ return node_path.default.join((0, node_os.tmpdir)(), "mulmoclaude-csv-utf8");
926
+ }
927
+ /** Read only the first `bytes` of a file — the encoding sniff must not
928
+ * pull a multi-hundred-MB CSV into memory on the (common) UTF-8 path. */
929
+ async function readHead(absPath, bytes) {
930
+ const handle = await (0, node_fs_promises.open)(absPath, "r");
931
+ try {
932
+ const { size } = await handle.stat();
933
+ const buf = Buffer.alloc(Math.min(bytes, size));
934
+ await handle.read(buf, 0, buf.length, 0);
935
+ return buf;
936
+ } finally {
937
+ await handle.close();
938
+ }
939
+ }
940
+ /** Decode the whole file into a UTF-8 cache copy and return its path.
941
+ * Cache key = (path, mtime, size), so a replaced CSV re-decodes and an
942
+ * unchanged one never does. */
943
+ async function pathExists$1(target) {
944
+ try {
945
+ await (0, node_fs_promises.stat)(target);
946
+ return true;
947
+ } catch {
948
+ return false;
949
+ }
950
+ }
951
+ /** Best-effort removal of older decode-cache entries for the same source
952
+ * path — a frequently-replaced large CSV would otherwise accumulate one
953
+ * full copy per (mtime, size) forever. Runs AFTER the current copy is
954
+ * published; a concurrent reader holding an old fd is unaffected
955
+ * (unlink-while-open is safe on POSIX). */
956
+ async function evictSupersededCache(key, keepBasename) {
957
+ try {
958
+ const entries = await (0, node_fs_promises.readdir)(cacheDir());
959
+ await Promise.all(entries.filter((name) => name.startsWith(`${key}-`) && name !== keepBasename).map((name) => (0, node_fs_promises.unlink)(node_path.default.join(cacheDir(), name)).catch(() => void 0)));
960
+ } catch {}
961
+ }
962
+ /** Decode the whole file into a UTF-8 cache copy and return its path.
963
+ * Cache key = (path, mtime, size), so a replaced CSV re-decodes and an
964
+ * unchanged one never does; superseded copies are evicted. The cache
965
+ * lives in the SHARED OS tmpdir, so the dir is 0700 and files 0600 —
966
+ * decoded rows must not be readable by other local users. */
967
+ async function decodeToCache(absPath, info) {
968
+ const key = (0, node_crypto.createHash)("sha256").update(absPath).digest("hex").slice(0, 16);
969
+ const cached = node_path.default.join(cacheDir(), `${key}-${Math.trunc(info.mtimeMs)}-${info.size}.csv`);
970
+ if (!await pathExists$1(cached)) {
971
+ const whole = await (0, node_fs_promises.readFile)(absPath);
972
+ const encoding = fallbackEncoding(whole);
973
+ const text = iconv_lite.default.decode(whole, encoding);
974
+ await (0, node_fs_promises.mkdir)(cacheDir(), {
975
+ recursive: true,
976
+ mode: 448
977
+ });
978
+ const tmp = `${cached}.${(0, node_crypto.randomBytes)(4).toString("hex")}.tmp`;
979
+ await (0, node_fs_promises.writeFile)(tmp, text, {
980
+ encoding: "utf-8",
981
+ mode: 384
982
+ });
983
+ await (0, node_fs_promises.rename)(tmp, cached);
984
+ log.info("collections", "decoded non-UTF-8 dataSource file to cache", {
985
+ path: absPath,
986
+ encoding
987
+ });
988
+ await evictSupersededCache(key, node_path.default.basename(cached));
989
+ }
990
+ return cached;
991
+ }
992
+ /** Re-validate the dataSource file at READ time, mirroring the JSON
993
+ * store's per-read defenses: realpath containment (a symlink swapped in
994
+ * after discovery must not walk out of the workspace) and an lstat
995
+ * regular-file check (a symlink leaf is refused outright, even one
996
+ * pointing inside the workspace — same rule as `isRegularFile` on
997
+ * record files). Returns the stat info, or null for "no readable file"
998
+ * (ENOENT / refused), which callers render as an empty collection. */
999
+ async function safeCsvStat(absPath, workspaceRoot) {
1000
+ if (!isContainedInRoot(absPath, workspaceRoot)) {
1001
+ log.warn("collections", "dataSource read refused: path escapes workspace", { path: absPath });
1002
+ return null;
1003
+ }
1004
+ let info;
1005
+ try {
1006
+ info = await (0, node_fs_promises.lstat)(absPath);
1007
+ } catch (err) {
1008
+ if (err.code === "ENOENT") return null;
1009
+ throw err;
1010
+ }
1011
+ if (!info.isFile()) {
1012
+ log.warn("collections", "dataSource read refused: not a regular file (symlink?)", { path: absPath });
1013
+ return null;
1014
+ }
1015
+ return info;
1016
+ }
1017
+ /** Return a path DuckDB can read as UTF-8: the original file when it
1018
+ * already is UTF-8 (the cheap, common case — only the head is sniffed),
1019
+ * else a decoded cache copy (see `decodeToCache`). Returns null when
1020
+ * there is no readable file (missing, symlink, or containment-refused —
1021
+ * see `safeCsvStat`), which callers render as an empty collection. */
1022
+ async function ensureUtf8CsvPath(absPath, workspaceRoot) {
1023
+ const info = await safeCsvStat(absPath, workspaceRoot);
1024
+ if (info === null) return null;
1025
+ const head = await readHead(absPath, SNIFF_BYTES);
1026
+ const sample = head.length === SNIFF_BYTES ? head.subarray(0, SNIFF_BYTES - 3) : head;
1027
+ if (!(head.length >= 2 && (head[0] === 255 && head[1] === 254 || head[0] === 254 && head[1] === 255)) && isValidUtf8(sample)) return absPath;
1028
+ return decodeToCache(absPath, info);
1029
+ }
1030
+ var instancePromise = null;
1031
+ /** Lazily create one shared in-memory DuckDB instance. The dynamic import
1032
+ * keeps the native module OUT of core's load path — a platform where the
1033
+ * prebuilt binding is missing degrades to a per-query error on dataSource
1034
+ * collections only, never a broken core. A failed init is retried on the
1035
+ * next call (the promise is reset). */
1036
+ async function duckDbInstance() {
1037
+ if (instancePromise === null) instancePromise = import("@duckdb/node-api").then((mod) => mod.DuckDBInstance.create(":memory:"));
1038
+ try {
1039
+ return await instancePromise;
1040
+ } catch (err) {
1041
+ instancePromise = null;
1042
+ throw new Error(`DuckDB is unavailable on this host (@duckdb/node-api failed to load: ${String(err)}) — dataSource collections cannot be read`);
1043
+ }
1044
+ }
1045
+ async function queryCsv(sql, params) {
1046
+ const connection = await (await duckDbInstance()).connect();
1047
+ try {
1048
+ return (await connection.runAndReadAll(sql, params)).getRowObjectsJS();
1049
+ } finally {
1050
+ connection.disconnectSync();
1051
+ }
1052
+ }
1053
+ /** Every row of the CSV as records — capped, deduped, id-encoded. The
1054
+ * key column is pinned to VARCHAR (see `readCsvArgs`). `workspaceRoot`
1055
+ * drives the per-read containment check; omitted, the configured host
1056
+ * root is used. */
1057
+ async function csvList(absPath, primaryKey, workspaceRoot) {
1058
+ const utf8Path = await ensureUtf8CsvPath(absPath, workspaceRoot ?? getWorkspaceRoot());
1059
+ if (utf8Path === null) return [];
1060
+ let rows;
1061
+ try {
1062
+ rows = await queryCsv(`SELECT * FROM read_csv(${readCsvArgs(primaryKey)}) LIMIT 5001`, [utf8Path]);
1063
+ } catch (err) {
1064
+ if (!isMissingKeyColumnError(err)) throw err;
1065
+ log.warn("collections", "dataSource CSV has no primaryKey column — every row is skipped", {
1066
+ path: absPath,
1067
+ primaryKey
1068
+ });
1069
+ return [];
1070
+ }
1071
+ if (rows.length > 5e3) {
1072
+ log.warn("collections", "dataSource CSV truncated to row cap", {
1073
+ path: absPath,
1074
+ cap: MAX_CSV_ROWS
1075
+ });
1076
+ rows.length = MAX_CSV_ROWS;
1077
+ }
1078
+ const items = rows.map((row) => csvRowToItem(row, primaryKey)).filter((item) => item !== null);
1079
+ const skipped = rows.length - items.length;
1080
+ if (skipped > 0) log.warn("collections", "dataSource CSV rows skipped (empty key cell)", {
1081
+ path: absPath,
1082
+ skipped
1083
+ });
1084
+ const deduped = dedupeByRecordId(items, primaryKey);
1085
+ if (deduped.duplicates > 0) log.warn("collections", "dataSource CSV has duplicate key values (last row wins)", {
1086
+ path: absPath,
1087
+ duplicates: deduped.duplicates
1088
+ });
1089
+ return deduped.items;
1090
+ }
1091
+ /** The scan-order ordinal column the last-match read adds. Underscore
1092
+ * prefix keeps it out of any plausible CSV header namespace; it is
1093
+ * stripped from the returned record either way. */
1094
+ var ROW_ORDINAL = "__mc_row";
1095
+ /** One record by id. The comparison value rides as a prepared-statement
1096
+ * parameter, and the LAST matching row is selected IN DuckDB (scan-order
1097
+ * ordinal + LIMIT 1) — a CSV with thousands of duplicate keys must not
1098
+ * materialize them all for one detail read. Consistent with csvList's
1099
+ * last-wins dedupe. */
1100
+ async function csvRead(absPath, primaryKey, itemId, workspaceRoot) {
1101
+ const utf8Path = await ensureUtf8CsvPath(absPath, workspaceRoot ?? getWorkspaceRoot());
1102
+ if (utf8Path === null) return null;
1103
+ const rawKey = decodeCsvRecordId(itemId);
1104
+ const last = (await queryCsv(`SELECT * FROM (SELECT *, row_number() OVER () AS ${quoteIdent(ROW_ORDINAL)} FROM read_csv(${readCsvArgs(primaryKey)})) WHERE CAST(${quoteIdent(primaryKey)} AS VARCHAR) = ? ORDER BY ${quoteIdent(ROW_ORDINAL)} DESC LIMIT 1`, [utf8Path, rawKey])).at(0);
1105
+ if (last === void 0) return null;
1106
+ const { [ROW_ORDINAL]: __ordinal, ...record } = last;
1107
+ return csvRowToItem(record, primaryKey);
1108
+ }
1109
+ /** Run a validated aggregation query (the structured DSL — see
1110
+ * `core/queryZ.ts`) over the WHOLE file: no row cap on the scan (a
1111
+ * capped aggregate would be a wrong number), only the result-row LIMIT
1112
+ * the compiler emits. Values are normalized like list/read rows so a
1113
+ * chart consumer gets plain JSON scalars. */
1114
+ async function csvRunQuery(absPath, primaryKey, query, workspaceRoot) {
1115
+ const utf8Path = await ensureUtf8CsvPath(absPath, workspaceRoot ?? getWorkspaceRoot());
1116
+ if (utf8Path === null) return [];
1117
+ const { sql, params } = compileCsvQuery(query, primaryKey);
1118
+ return (await queryCsv(sql, [utf8Path, ...params])).map((row) => Object.fromEntries(Object.entries(row).map(([key, value]) => [key, normalizeCsvValue(value)])));
1119
+ }
1120
+ //#endregion
1121
+ //#region src/collection/server/store.ts
1122
+ /** True when the collection accepts UI/tool writes. A `dataSource`
1123
+ * collection is read-only: updates happen by editing/replacing the
1124
+ * data file itself. Every write entry point checks this BEFORE calling
1125
+ * `writeItem`/`deleteItem` — server-enforced, not just UI-hidden. */
1126
+ function collectionWritable(collection) {
1127
+ return !require_promptSafety.isReadOnlySchema(collection.schema);
1128
+ }
1129
+ /** The one-line refusal write paths surface (HTTP 405 / MCP error text). */
1130
+ function readOnlyRefusal(slug) {
1131
+ return `collection '${slug}' is read-only (backed by an external dataSource) — update the data file itself instead`;
1132
+ }
1133
+ /** Pick the store implementation for a discovered collection. A
1134
+ * `dataSource` schema whose `dataSourceFile` failed to resolve yields a
1135
+ * read-only EMPTY store rather than falling back to the (writable) file
1136
+ * store — a half-loaded read-only collection must never become writable. */
1137
+ function storeFor(collection, opts = {}) {
1138
+ if (require_promptSafety.isReadOnlySchema(collection.schema)) {
1139
+ const file = collection.dataSourceFile;
1140
+ const key = collection.schema.primaryKey;
1141
+ return {
1142
+ capabilities: { writable: false },
1143
+ list: () => file === void 0 ? Promise.resolve([]) : csvList(file, key, opts.workspaceRoot),
1144
+ read: (itemId) => file === void 0 ? Promise.resolve(null) : csvRead(file, key, itemId, opts.workspaceRoot),
1145
+ query: (query) => file === void 0 ? Promise.resolve([]) : csvRunQuery(file, key, query, opts.workspaceRoot)
1146
+ };
1147
+ }
1148
+ return {
1149
+ capabilities: { writable: true },
1150
+ list: () => listItems(collection.dataDir, opts),
1151
+ read: (itemId) => readItem(collection.dataDir, itemId, opts)
1152
+ };
1153
+ }
1154
+ //#endregion
1155
+ //#region src/collection/server/jsonlQuery.ts
1156
+ /** SQL semantics for an aggregate-only query over ZERO rows: one scalar
1157
+ * row (`count` = 0, everything else NULL) — the same shape the CSV path
1158
+ * produces for a header-only file, so callers reading `rows[0]` never
1159
+ * break on storage kind or emptiness. A grouped query over zero rows is
1160
+ * zero groups (`[]`), also matching SQL. Synthesized here because DuckDB
1161
+ * has no empty file to infer a schema from. */
1162
+ function emptyCollectionResult(query) {
1163
+ if ((query.groupBy?.length ?? 0) > 0) return [];
1164
+ const aggregates = Object.entries(query.aggregates ?? {});
1165
+ return [Object.fromEntries(aggregates.map(([alias, aggregate]) => [alias, aggregate.op === "count" ? 0 : null]))];
1166
+ }
1167
+ /** The SOURCE columns a query reads: groupBy columns, aggregate input
1168
+ * columns, and where fields. (orderBy resolves to groupBy columns or
1169
+ * aggregate ALIASES — never to a new source column — so it adds none.) */
1170
+ function referencedSourceColumns(query) {
1171
+ const columns = new Set(query.groupBy ?? []);
1172
+ for (const aggregate of Object.values(query.aggregates ?? {})) if (aggregate.column !== void 0) columns.add(aggregate.column);
1173
+ for (const cond of query.where ?? []) columns.add(cond.field);
1174
+ return [...columns];
1175
+ }
1176
+ /** Referenced columns that appear in NO row — a freshly-added optional
1177
+ * field, or a derived field whose inputs no record has yet. DuckDB only
1178
+ * infers columns from keys that occur in the JSONL, so without help a
1179
+ * valid query on such a field binder-errors here while the CSV path
1180
+ * (whose header always declares the column) returns NULLs. Padding the
1181
+ * FIRST line with `col: null` makes the column exist (full-scan
1182
+ * inference unions keys across lines) with matching NULL semantics. */
1183
+ function absentReferencedColumns(rows, query) {
1184
+ const present = /* @__PURE__ */ new Set();
1185
+ for (const row of rows) for (const key of Object.keys(row)) present.add(key);
1186
+ return referencedSourceColumns(query).filter((column) => !present.has(column));
1187
+ }
1188
+ /** Run a validated query over enriched collection rows. */
1189
+ async function runQueryOverRows(rows, query) {
1190
+ if (rows.length === 0) return emptyCollectionResult(query);
1191
+ await (0, node_fs_promises.mkdir)(cacheDir(), {
1192
+ recursive: true,
1193
+ mode: 448
1194
+ });
1195
+ const jsonlPath = node_path.default.join(cacheDir(), `q-${(0, node_crypto.randomBytes)(8).toString("hex")}.jsonl`);
1196
+ try {
1197
+ const nullPads = Object.fromEntries(absentReferencedColumns(rows, query).map((column) => [column, null]));
1198
+ const handle = await (0, node_fs_promises.open)(jsonlPath, "wx", 384);
1199
+ try {
1200
+ let first = true;
1201
+ for (const row of rows) {
1202
+ await handle.write(`${JSON.stringify(first ? {
1203
+ ...nullPads,
1204
+ ...row
1205
+ } : row)}\n`);
1206
+ first = false;
1207
+ }
1208
+ } finally {
1209
+ await handle.close();
1210
+ }
1211
+ const { sql, params } = compileJsonlQuery(query);
1212
+ return (await queryCsv(sql, [jsonlPath, ...params])).map((row) => Object.fromEntries(Object.entries(row).map(([key, value]) => [key, normalizeCsvValue(value)])));
1213
+ } finally {
1214
+ await (0, node_fs_promises.unlink)(jsonlPath).catch((err) => {
1215
+ if (err.code !== "ENOENT") log.warn("collections", "temp JSONL cleanup failed", {
1216
+ path: jsonlPath,
1217
+ error: String(err)
1218
+ });
1219
+ });
1220
+ }
1221
+ }
1222
+ //#endregion
640
1223
  //#region src/collection/core/recordZ.ts
641
1224
  /** The emptiness rule shared by `required` and the "only check present
642
1225
  * values" gate. NOT a truthiness check — `0` and `false` are filled. */
@@ -753,6 +1336,7 @@ async function listRecordFilenames(dataDir, workspaceRoot) {
753
1336
  }
754
1337
  }
755
1338
  async function validateCollectionRecords(collection, opts = {}) {
1339
+ if (collection.schema.dataSource !== void 0) return [];
756
1340
  const workspaceRoot = opts.workspaceRoot ?? getWorkspaceRoot();
757
1341
  const entries = await listRecordFilenames(collection.dataDir, workspaceRoot);
758
1342
  const issues = [];
@@ -1402,10 +1986,24 @@ function fieldDrivenFromFieldCarried(schema) {
1402
1986
  }
1403
1987
  return (carry ?? []).includes(driven.fromField);
1404
1988
  }
1989
+ /** External-data collection: the records ARE the rows of a user-supplied
1990
+ * data file (v1: CSV), queried through DuckDB — never copied into
1991
+ * `<dataDir>/<id>.json` files. Declaring `dataSource` makes the collection
1992
+ * **read-only** in every UI/tool write path; updates happen by replacing /
1993
+ * editing the file itself (file-watch republishes the views). `path` is
1994
+ * workspace-relative and containment-checked exactly like `dataPath`. The
1995
+ * row-id column is the schema's existing `primaryKey` — there is
1996
+ * deliberately no second key concept here.
1997
+ * See plans/feat-collection-csv-duckdb-source.md. */
1998
+ var DataSourceZ = zod.z.object({
1999
+ type: zod.z.literal("csv"),
2000
+ path: zod.z.string().min(1)
2001
+ });
1405
2002
  var CollectionSchemaZ = zod.z.object({
1406
2003
  title: zod.z.string().min(1),
1407
2004
  icon: zod.z.string().min(1),
1408
- dataPath: zod.z.string().min(1),
2005
+ dataPath: zod.z.string().min(1).optional(),
2006
+ dataSource: DataSourceZ.optional(),
1409
2007
  primaryKey: zod.z.string().min(1),
1410
2008
  singleton: zod.z.string().trim().min(1).optional(),
1411
2009
  fields: zod.z.record(zod.z.string(), FieldSpecZ),
@@ -1425,6 +2023,15 @@ var CollectionSchemaZ = zod.z.object({
1425
2023
  notifyWhen: WhenZ.optional(),
1426
2024
  ingest: IngestZ.optional(),
1427
2025
  dynamicIcon: DynamicIconSpecZ.optional()
2026
+ }).refine((schema) => schema.dataPath !== void 0 !== (schema.dataSource !== void 0), {
2027
+ message: "declare exactly one of `dataPath` (native JSON records) or `dataSource` (external read-only data file), never both or neither",
2028
+ path: ["dataPath"]
2029
+ }).refine((schema) => schema.dataSource === void 0 || schema.singleton === void 0 && schema.ingest === void 0 && schema.spawn === void 0, {
2030
+ message: "a `dataSource` collection is read-only — it cannot declare `singleton`, `ingest`, or `spawn` (all of them write records)",
2031
+ path: ["dataSource"]
2032
+ }).refine((schema) => schema.dataSource === void 0 || [...schema.actions ?? [], ...schema.collectionActions ?? []].every((action) => action.kind !== "mutate"), {
2033
+ message: "a `dataSource` collection is read-only — its actions cannot use `kind: \"mutate\"` (a host write); use `chat`/`agent` actions instead",
2034
+ path: ["dataSource"]
1428
2035
  }).refine((schema) => schema.singleton === void 0 || require_promptSafety.isSafeRecordId(schema.singleton), {
1429
2036
  message: "schema `singleton` must be a valid item id (alphanumeric / hyphen / underscore / interior dot, no `..` or path separators)",
1430
2037
  path: ["singleton"]
@@ -1550,6 +2157,12 @@ function applyFeedSchemaDefaults(parsed, slug) {
1550
2157
  dataPath: `data/feeds/${slug}`
1551
2158
  };
1552
2159
  }
2160
+ /** The conventional per-slug records dir a `dataSource` collection gets as
2161
+ * its `dataDir` (records never live there, but archive/delete paths stay
2162
+ * well-defined — same shape the registry's R3 normalization uses). */
2163
+ function conventionalDataPath(slug) {
2164
+ return `data/collections/${slug}/items`;
2165
+ }
1553
2166
  /** The acceptance gates discovery applies AFTER `CollectionSchemaZ` parses,
1554
2167
  * before a schema becomes a live collection:
1555
2168
  *
@@ -1559,7 +2172,8 @@ function applyFeedSchemaDefaults(parsed, slug) {
1559
2172
  * edit is dropped with no error;
1560
2173
  * - a `feed` schema must declare an `ingest` block (else it's a dead,
1561
2174
  * non-refreshable card);
1562
- * - `dataPath` must resolve INSIDE the workspace.
2175
+ * - `dataPath` — or a `dataSource`'s `path` — must resolve INSIDE the
2176
+ * workspace (same realpath containment for both).
1563
2177
  *
1564
2178
  * Exported so `manageCollection`'s `putSchema` can run the SAME gates before
1565
2179
  * it reports success — a schema that passes `CollectionSchemaZ` but fails one
@@ -1579,7 +2193,24 @@ function acceptParsedSchema(schema, opts) {
1579
2193
  ok: false,
1580
2194
  reason: "a feed schema must declare an `ingest` block"
1581
2195
  };
1582
- const dataDir = resolveDataDir(schema.dataPath, opts.workspaceRoot);
2196
+ if (schema.dataSource !== void 0) {
2197
+ const dataSourceFile = resolveDataDir(schema.dataSource.path, opts.workspaceRoot);
2198
+ if (dataSourceFile === null) return {
2199
+ ok: false,
2200
+ reason: `dataSource.path '${schema.dataSource.path}' escapes the workspace`
2201
+ };
2202
+ const dataDir = resolveDataDir(conventionalDataPath(opts.slug), opts.workspaceRoot);
2203
+ if (dataDir === null) return {
2204
+ ok: false,
2205
+ reason: `slug '${opts.slug}' yields no workspace-contained data dir`
2206
+ };
2207
+ return {
2208
+ ok: true,
2209
+ dataDir,
2210
+ dataSourceFile
2211
+ };
2212
+ }
2213
+ const dataDir = resolveDataDir(schema.dataPath ?? "", opts.workspaceRoot);
1583
2214
  if (dataDir === null) return {
1584
2215
  ok: false,
1585
2216
  reason: `dataPath '${schema.dataPath}' escapes the workspace`
@@ -1627,7 +2258,8 @@ async function loadOneCollection(skillsRoot, slug, source, workspaceRoot) {
1627
2258
  const schema = parsed.data;
1628
2259
  const acceptance = acceptParsedSchema(schema, {
1629
2260
  source,
1630
- workspaceRoot
2261
+ workspaceRoot,
2262
+ slug: safeName
1631
2263
  });
1632
2264
  if (!acceptance.ok) {
1633
2265
  log.warn("collections", "schema.json rejected after validation, skipping", {
@@ -1641,6 +2273,7 @@ async function loadOneCollection(skillsRoot, slug, source, workspaceRoot) {
1641
2273
  source,
1642
2274
  schema,
1643
2275
  dataDir: acceptance.dataDir,
2276
+ ...acceptance.dataSourceFile !== void 0 ? { dataSourceFile: acceptance.dataSourceFile } : {},
1644
2277
  skillDir: node_path.default.join(skillsRoot, safeName)
1645
2278
  };
1646
2279
  }
@@ -1712,7 +2345,8 @@ function toSummary(collection) {
1712
2345
  slug: collection.slug,
1713
2346
  title: collection.schema.title,
1714
2347
  icon: collection.schema.icon,
1715
- source: collection.source
2348
+ source: collection.source,
2349
+ ...collection.schema.dataSource !== void 0 ? { readonly: true } : {}
1716
2350
  };
1717
2351
  }
1718
2352
  function toDetail(collection) {
@@ -1767,6 +2401,20 @@ async function countRecordFiles(dataDir, workspaceRoot) {
1767
2401
  return 0;
1768
2402
  }
1769
2403
  }
2404
+ /** A collection's record count for the ontology. File-backed: the cheap
2405
+ * readdir count. `dataSource`-backed: the row count via the CSV store
2406
+ * (subject to its row cap) — the dataDir is a phantom there, so the
2407
+ * readdir count would always misreport 0. Fail-soft to 0 like
2408
+ * `countRecordFiles` (an unreadable file / missing DuckDB must not
2409
+ * break the whole ontology). */
2410
+ async function countRecords(collection, workspaceRoot) {
2411
+ if (!collectionWritable(collection)) try {
2412
+ return (await storeFor(collection, { workspaceRoot }).list()).length;
2413
+ } catch {
2414
+ return 0;
2415
+ }
2416
+ return countRecordFiles(collection.dataDir, workspaceRoot);
2417
+ }
1770
2418
  async function toOntologyEntry(collection, workspaceRoot) {
1771
2419
  const { schema } = collection;
1772
2420
  return {
@@ -1775,7 +2423,7 @@ async function toOntologyEntry(collection, workspaceRoot) {
1775
2423
  icon: schema.icon,
1776
2424
  primaryKey: schema.primaryKey,
1777
2425
  displayField: schema.displayField ?? schema.primaryKey,
1778
- recordCount: await countRecordFiles(collection.dataDir, workspaceRoot),
2426
+ recordCount: await countRecords(collection, workspaceRoot),
1779
2427
  relations: schemaRelations(schema)
1780
2428
  };
1781
2429
  }
@@ -1821,7 +2469,7 @@ function uniqueBacklinkSources(schema) {
1821
2469
  async function loadTarget(slug, opts) {
1822
2470
  const target = await loadCollection(slug, opts);
1823
2471
  if (!target) return null;
1824
- const items = await listItems(target.dataDir, { workspaceRoot: opts.workspaceRoot });
2472
+ const items = await storeFor(target, { workspaceRoot: opts.workspaceRoot }).list();
1825
2473
  const byId = {};
1826
2474
  for (const item of items) {
1827
2475
  const itemId = item[target.schema.primaryKey];
@@ -1939,7 +2587,7 @@ async function computeCollectionIcon(collection, opts = {}) {
1939
2587
  try {
1940
2588
  const source = await loadCollection(spec.source.collection, opts);
1941
2589
  if (!source) return spec.fallback ?? schema.icon;
1942
- const ordered = sortByPrimaryKey(await listItems(source.dataDir, { workspaceRoot: opts.workspaceRoot }), source.schema.primaryKey);
2590
+ const ordered = sortByPrimaryKey(await storeFor(source, { workspaceRoot: opts.workspaceRoot }).list(), source.schema.primaryKey);
1943
2591
  const orderBy = spec.source.orderBy ?? require_promptSafety.firstDateField(source.schema);
1944
2592
  const recordsById = buildRecordsById(ordered, source.schema.primaryKey);
1945
2593
  return require_promptSafety.resolveIcon(require_promptSafety.selectDynamicRecord(ordered, spec.source, orderBy, recordsById), spec, schema.icon, recordsById);
@@ -2237,6 +2885,20 @@ function isDataDirSafe(dataDir, slug, workspaceRoot) {
2237
2885
  const resolved = node_path.default.resolve(dataDir);
2238
2886
  return acceptableRoots.some((root) => resolved === root || resolved.startsWith(root + node_path.default.sep));
2239
2887
  }
2888
+ /** Step 2 of the restore doc — how to bring the records back. A
2889
+ * `dataSource` collection has no record files to copy (its rows live in
2890
+ * the external data file, which the delete never touches). */
2891
+ function restoreRecordsStep(schema) {
2892
+ if (schema.dataPath === void 0) return `2. Records: nothing to copy. This is a \`dataSource\` collection —
2893
+ its records are the rows of \`${schema.dataSource?.path}\`, which the
2894
+ delete never touched.`;
2895
+ return `2. Copy the item data: \`cp\` every file under \`records/\` into
2896
+ \`${schema.dataPath}/\`. The records are part of the collection and
2897
+ must be restored. They are plain data files (NOT bridged), so use
2898
+ \`cp\` — the Write-tool rule in step 1 applies ONLY to the skill
2899
+ files, not to these records (there may be many; copy them, do not
2900
+ Write them one by one).`;
2901
+ }
2240
2902
  function buildRestoreDoc(collection) {
2241
2903
  const { slug, schema } = collection;
2242
2904
  return `# Restore "${schema.title}" (collection \`${slug}\`)
@@ -2257,18 +2919,13 @@ Follow these steps to restore it.
2257
2919
  \`.claude/skills/\` directly is not an option either: that path is
2258
2920
  permission-gated.)
2259
2921
 
2260
- 2. Copy the item data: \`cp\` every file under \`records/\` into
2261
- \`${schema.dataPath}/\`. The records are part of the collection and
2262
- must be restored. They are plain data files (NOT bridged), so use
2263
- \`cp\` — the Write-tool rule in step 1 applies ONLY to the skill
2264
- files, not to these records (there may be many; copy them, do not
2265
- Write them one by one).
2922
+ ${restoreRecordsStep(schema)}
2266
2923
 
2267
2924
  3. Confirm the collection reappears at \`/collections/${slug}\`.
2268
2925
 
2269
2926
  - slug: \`${slug}\`
2270
2927
  - title: ${schema.title}
2271
- - dataPath: \`${schema.dataPath}\`
2928
+ - dataPath: \`${schema.dataPath ?? `(dataSource) ${schema.dataSource?.path}`}\`
2272
2929
  `;
2273
2930
  }
2274
2931
  /** Copy one skill copy + the records + RESTORE.md into `archiveDir`. */
@@ -2491,14 +3148,15 @@ async function recordIssuesWarning(collection, deps) {
2491
3148
  return `${issues.length} record file(s) have data problems and are missing from this result. Fix each (Read → correct → Write):\n${lines}`;
2492
3149
  }
2493
3150
  async function loadRequestedItems(collection, ids, deps) {
3151
+ const store = storeFor(collection, { workspaceRoot: deps.workspaceRoot });
2494
3152
  if (!ids) return {
2495
- items: await listItems(collection.dataDir, { workspaceRoot: deps.workspaceRoot }),
3153
+ items: await store.list(),
2496
3154
  missing: []
2497
3155
  };
2498
3156
  const items = [];
2499
3157
  const missing = [];
2500
3158
  for (const recordId of ids) {
2501
- const item = await readItem(collection.dataDir, recordId, { workspaceRoot: deps.workspaceRoot }).catch(() => null);
3159
+ const item = await store.read(recordId).catch(() => null);
2502
3160
  if (item) items.push(item);
2503
3161
  else missing.push(recordId);
2504
3162
  }
@@ -2602,7 +3260,35 @@ async function putOneItem(collection, record, mode, deps) {
2602
3260
  if (result.kind === "conflict") return reject(itemId, `'${itemId}' already exists — mode "create" refuses overwrite; use "upsert" to update it`);
2603
3261
  return reject(itemId, "write refused: the collection's data dir escapes the workspace");
2604
3262
  }
3263
+ /** Aggregation over a collection via the structured query DSL
3264
+ * (`core/queryZ.ts`) — the paved road for counts / sums / group-bys
3265
+ * that a row listing can't answer honestly. Two engines behind one
3266
+ * shape: a dataSource collection queries its CSV natively through the
3267
+ * store (`store.query`, uncapped whole-file scan); a file-backed
3268
+ * collection aggregates its ENRICHED records (computed fields —
3269
+ * `derived` / `rollup` / `toggle` — are real columns) through the same
3270
+ * compiled SQL over a temp JSONL (`runQueryOverRows`). */
3271
+ async function handleQueryItems(collection, queryArg, deps) {
3272
+ const parsed = CollectionQueryZ.safeParse(queryArg);
3273
+ if (!parsed.success) return `manageCollection: \`query\` rejected — fix and retry:\n${parsed.error.issues.slice(0, 20).map((issue) => `- ${issue.path.map(String).join(".") || "(root)"}: ${require_promptSafety.defangForPrompt(issue.message)}`).join("\n")}`;
3274
+ const store = storeFor(collection, { workspaceRoot: deps.workspaceRoot });
3275
+ if (store.query) {
3276
+ const rows = await store.query(parsed.data);
3277
+ return JSON.stringify({
3278
+ collection: collection.slug,
3279
+ count: rows.length,
3280
+ rows
3281
+ });
3282
+ }
3283
+ const rows = await runQueryOverRows(await enrichItems(collection, await store.list(), deps), parsed.data);
3284
+ return JSON.stringify({
3285
+ collection: collection.slug,
3286
+ count: rows.length,
3287
+ rows
3288
+ });
3289
+ }
2605
3290
  async function handlePutItems(collection, args, deps) {
3291
+ if (!collectionWritable(collection)) return `manageCollection: ${readOnlyRefusal(collection.slug)} (its records are the rows of '${collection.schema.dataSource?.path}'; edit that file to change the data).`;
2606
3292
  const written = [];
2607
3293
  const rejected = [];
2608
3294
  for (const record of args.items) {
@@ -2701,7 +3387,8 @@ function schemaDiscoveryGate(schema, base) {
2701
3387
  const primaryField = schema.fields[schema.primaryKey];
2702
3388
  if (!primaryField) return `primaryKey '${schema.primaryKey}' is not one of the declared fields`;
2703
3389
  if (primaryField.primary !== true) return `the primaryKey field '${schema.primaryKey}' must be flagged \`primary: true\``;
2704
- if (resolveDataDir(schema.dataPath, base) === null) return `dataPath '${schema.dataPath}' escapes the workspace`;
3390
+ if (schema.dataPath !== void 0 && resolveDataDir(schema.dataPath, base) === null) return `dataPath '${schema.dataPath}' escapes the workspace`;
3391
+ if (schema.dataSource !== void 0 && resolveDataDir(schema.dataSource.path, base) === null) return `dataSource.path '${schema.dataSource.path}' escapes the workspace`;
2705
3392
  return null;
2706
3393
  }
2707
3394
  /** Validate a schema against CollectionSchemaZ and, on success, persist it.
@@ -2723,7 +3410,7 @@ async function handlePutSchema(slug, schemaArg, deps) {
2723
3410
  written: true
2724
3411
  });
2725
3412
  }
2726
- var MANAGE_COLLECTION_PROMPT = "Use `manageCollection` instead of raw Read/Write/Edit when working with a collection's records OR its schema (raw file I/O stays available as the escape hatch). Before authoring or changing a collection's `schema.json`, call `schemaDocs` to load the field/DSL reference, then read with `getSchema` and write with `putSchema` — `putSchema` validates the whole schema before writing and returns actionable errors instead of silently failing discovery's validation. `getItems` is the only way to see computed values — `derived` fields (e.g. a portfolio's value), `toggle` projections, and `embed` records are host-computed and never present in the stored JSON files. On large collections pass `ids` and/or `fields` to keep the result small. For a question that spans collections (\"which clients have unpaid invoices?\"), start with `getOntology`: it lists every collection with its primaryKey, record count, and outbound `ref`/`embed` relations, so you know which collections to join before reading any records. `putItems` validates every row against the schema before writing (required fields, enum values, primaryKey = record id) and returns `{ written, rejected }`; fix each rejected row using its `problem` text and retry just those rows. Never include computed fields in a row you write. To update a few fields of an existing record, use `mode: \"merge\"` with a partial row ({ id, <changed fields> }) — the default upsert replaces the WHOLE record, so a partial upsert would silently erase every optional field it omits.";
3413
+ var MANAGE_COLLECTION_PROMPT = "Use `manageCollection` instead of raw Read/Write/Edit when working with a collection's records OR its schema (raw file I/O stays available as the escape hatch). Before authoring or changing a collection's `schema.json`, call `schemaDocs` to load the field/DSL reference, then read with `getSchema` and write with `putSchema` — `putSchema` validates the whole schema before writing and returns actionable errors instead of silently failing discovery's validation. `getItems` is the only way to see computed values — `derived` fields (e.g. a portfolio's value), `toggle` projections, and `embed` records are host-computed and never present in the stored JSON files. On large collections pass `ids` and/or `fields` to keep the result small. For a question that spans collections (\"which clients have unpaid invoices?\"), start with `getOntology`: it lists every collection with its primaryKey, record count, and outbound `ref`/`embed` relations, so you know which collections to join before reading any records. `putItems` validates every row against the schema before writing (required fields, enum values, primaryKey = record id) and returns `{ written, rejected }`; fix each rejected row using its `problem` text and retry just those rows. Never include computed fields in a row you write. To update a few fields of an existing record, use `mode: \"merge\"` with a partial row ({ id, <changed fields> }) — the default upsert replaces the WHOLE record, so a partial upsert would silently erase every optional field it omits. Answer aggregation questions (counts, sums, averages, group-bys) with `queryItems` on ANY collection — on a dataSource (CSV) collection it scans the whole file (getItems is row-capped, so aggregates computed from its output can be silently wrong on large files); on a file-backed collection it aggregates the enriched records, so computed fields (derived/rollup/toggle) are queryable columns.";
2727
3414
  /** Validate getItems' optional `ids`/`fields` args, then delegate. */
2728
3415
  async function dispatchGetItems(collection, args, deps) {
2729
3416
  const ids = optionalStringArray(args.ids, "ids");
@@ -2744,82 +3431,113 @@ async function manageCollectionHandler(deps, args) {
2744
3431
  if (!slug) return "manageCollection: `slug` is required (the collection's slug).";
2745
3432
  if (action === "getSchema") return handleGetSchema(slug, deps);
2746
3433
  if (action === "putSchema") return handlePutSchema(slug, args.schema, deps);
2747
- if (action !== "getItems" && action !== "putItems") return "manageCollection: `action` must be \"getItems\", \"putItems\", \"getOntology\", \"schemaDocs\", \"getSchema\", or \"putSchema\".";
3434
+ if (action !== "getItems" && action !== "putItems" && action !== "queryItems") return "manageCollection: `action` must be \"getItems\", \"putItems\", \"queryItems\", \"getOntology\", \"schemaDocs\", \"getSchema\", or \"putSchema\".";
2748
3435
  const collection = await loadCollection(slug, deps);
2749
3436
  if (!collection) return unknownCollection(slug);
2750
3437
  if (action === "getItems") return dispatchGetItems(collection, args, deps);
3438
+ if (action === "queryItems") return handleQueryItems(collection, args.query, deps);
2751
3439
  const parsed = parsePutItems(args, slug);
2752
3440
  if (typeof parsed === "string") return parsed;
2753
3441
  return handlePutItems(collection, parsed, deps);
2754
3442
  }
2755
- function makeManageCollectionTool(deps = {}) {
2756
- return {
2757
- definition: {
2758
- name: "manageCollection",
2759
- description: "Read and write a schema-driven collection through the host — both its records and its structure. getItems returns records WITH computed values (derived formulas, toggles, embeds) the stored JSON files don't contain; putItems validates each row against the schema before writing. getOntology maps the whole workspace: every collection with its record count and outbound ref/embed relations — call it first for cross-collection questions. schemaDocs returns the collection-authoring reference; getSchema/putSchema read and validate-then-write the collection's schema.json. Prefer it over raw file I/O on collections.",
2760
- inputSchema: {
3443
+ var MANAGE_COLLECTION_DEFINITION = {
3444
+ name: "manageCollection",
3445
+ description: "Read and write a schema-driven collection through the host — both its records and its structure. getItems returns records WITH computed values (derived formulas, toggles, embeds) the stored JSON files don't contain; putItems validates each row against the schema before writing. getOntology maps the whole workspace: every collection with its record count and outbound ref/embed relations — call it first for cross-collection questions. schemaDocs returns the collection-authoring reference; getSchema/putSchema read and validate-then-write the collection's schema.json. Prefer it over raw file I/O on collections.",
3446
+ inputSchema: {
3447
+ type: "object",
3448
+ properties: {
3449
+ action: {
3450
+ type: "string",
3451
+ enum: [
3452
+ "getItems",
3453
+ "putItems",
3454
+ "queryItems",
3455
+ "getOntology",
3456
+ "schemaDocs",
3457
+ "getSchema",
3458
+ "putSchema"
3459
+ ],
3460
+ description: "What to do."
3461
+ },
3462
+ slug: {
3463
+ type: "string",
3464
+ description: "The collection's slug (its directory name, e.g. `stock-quotes`). Required for everything except schemaDocs and getOntology."
3465
+ },
3466
+ ids: {
3467
+ type: "array",
3468
+ items: { type: "string" },
3469
+ description: "getItems: only these record ids (primary-key values). Omit for all records."
3470
+ },
3471
+ fields: {
3472
+ type: "array",
3473
+ items: { type: "string" },
3474
+ description: "getItems: only these fields per record (the primary key is always included). Omit for all fields. Use on large collections."
3475
+ },
3476
+ items: {
3477
+ type: "array",
3478
+ items: { type: "object" },
3479
+ description: "putItems: the record objects to store. Each must carry the schema's primaryKey value (it doubles as the filename)."
3480
+ },
3481
+ mode: {
3482
+ type: "string",
3483
+ enum: [
3484
+ "upsert",
3485
+ "create",
3486
+ "merge"
3487
+ ],
3488
+ description: "putItems: \"upsert\" (default) replaces existing records WHOLE; \"create\" rejects rows whose id already exists; \"merge\" updates only the fields a row carries, keeping the rest of the existing record (rejects unknown ids). Use \"merge\" when changing a few fields."
3489
+ },
3490
+ schema: {
2761
3491
  type: "object",
2762
- properties: {
2763
- action: {
2764
- type: "string",
2765
- enum: [
2766
- "getItems",
2767
- "putItems",
2768
- "getOntology",
2769
- "schemaDocs",
2770
- "getSchema",
2771
- "putSchema"
2772
- ],
2773
- description: "What to do."
2774
- },
2775
- slug: {
2776
- type: "string",
2777
- description: "The collection's slug (its directory name, e.g. `stock-quotes`). Required for everything except schemaDocs and getOntology."
2778
- },
2779
- ids: {
2780
- type: "array",
2781
- items: { type: "string" },
2782
- description: "getItems: only these record ids (primary-key values). Omit for all records."
2783
- },
2784
- fields: {
2785
- type: "array",
2786
- items: { type: "string" },
2787
- description: "getItems: only these fields per record (the primary key is always included). Omit for all fields. Use on large collections."
2788
- },
2789
- items: {
2790
- type: "array",
2791
- items: { type: "object" },
2792
- description: "putItems: the record objects to store. Each must carry the schema's primaryKey value (it doubles as the filename)."
2793
- },
2794
- mode: {
2795
- type: "string",
2796
- enum: [
2797
- "upsert",
2798
- "create",
2799
- "merge"
2800
- ],
2801
- description: "putItems: \"upsert\" (default) replaces existing records WHOLE; \"create\" rejects rows whose id already exists; \"merge\" updates only the fields a row carries, keeping the rest of the existing record (rejects unknown ids). Use \"merge\" when changing a few fields."
2802
- },
2803
- schema: {
2804
- type: "object",
2805
- description: "putSchema: the full collection schema object (same shape as schema.json — title, icon, dataPath, primaryKey, fields, …). Call getSchema first for the current one, and schemaDocs for the field DSL."
2806
- }
2807
- },
2808
- required: ["action"]
3492
+ description: "putSchema: the full collection schema object (same shape as schema.json — title, icon, dataPath, primaryKey, fields, …). Call getSchema first for the current one, and schemaDocs for the field DSL."
3493
+ },
3494
+ query: {
3495
+ type: "object",
3496
+ description: "queryItems: a structured aggregation query — `{ groupBy?: [\"col\"], aggregates?: { alias: { op: \"count\"|\"sum\"|\"avg\"|\"min\"|\"max\", column? } }, where?: [{ field, op, value }], orderBy?: [{ field, dir? }], limit? }`. At least one of groupBy/aggregates. On a dataSource (CSV) collection it scans the WHOLE file uncapped (unlike getItems); on a file-backed collection it aggregates the ENRICHED records, so computed fields (derived/rollup/toggle) are queryable columns. Use it for counts / sums / averages / group-bys instead of arithmetic over getItems output."
2809
3497
  }
2810
3498
  },
3499
+ required: ["action"]
3500
+ }
3501
+ };
3502
+ function makeManageCollectionTool(deps = {}) {
3503
+ return {
3504
+ definition: MANAGE_COLLECTION_DEFINITION,
2811
3505
  alwaysActive: true,
2812
3506
  prompt: MANAGE_COLLECTION_PROMPT,
2813
3507
  handler: (args) => manageCollectionHandler(deps, args)
2814
3508
  };
2815
3509
  }
2816
3510
  //#endregion
3511
+ Object.defineProperty(exports, "CollectionQueryZ", {
3512
+ enumerable: true,
3513
+ get: function() {
3514
+ return CollectionQueryZ;
3515
+ }
3516
+ });
2817
3517
  Object.defineProperty(exports, "CollectionSchemaZ", {
2818
3518
  enumerable: true,
2819
3519
  get: function() {
2820
3520
  return CollectionSchemaZ;
2821
3521
  }
2822
3522
  });
3523
+ Object.defineProperty(exports, "DEFAULT_QUERY_ROWS", {
3524
+ enumerable: true,
3525
+ get: function() {
3526
+ return DEFAULT_QUERY_ROWS;
3527
+ }
3528
+ });
3529
+ Object.defineProperty(exports, "MAX_CSV_ROWS", {
3530
+ enumerable: true,
3531
+ get: function() {
3532
+ return MAX_CSV_ROWS;
3533
+ }
3534
+ });
3535
+ Object.defineProperty(exports, "MAX_QUERY_ROWS", {
3536
+ enumerable: true,
3537
+ get: function() {
3538
+ return MAX_QUERY_ROWS;
3539
+ }
3540
+ });
2823
3541
  Object.defineProperty(exports, "MAX_SCHEMA_ISSUES", {
2824
3542
  enumerable: true,
2825
3543
  get: function() {
@@ -2880,12 +3598,30 @@ Object.defineProperty(exports, "buildWorkspaceOntology", {
2880
3598
  return buildWorkspaceOntology;
2881
3599
  }
2882
3600
  });
3601
+ Object.defineProperty(exports, "collectionWritable", {
3602
+ enumerable: true,
3603
+ get: function() {
3604
+ return collectionWritable;
3605
+ }
3606
+ });
2883
3607
  Object.defineProperty(exports, "collectionsRegistriesConfigPath", {
2884
3608
  enumerable: true,
2885
3609
  get: function() {
2886
3610
  return collectionsRegistriesConfigPath;
2887
3611
  }
2888
3612
  });
3613
+ Object.defineProperty(exports, "compileCsvQuery", {
3614
+ enumerable: true,
3615
+ get: function() {
3616
+ return compileCsvQuery;
3617
+ }
3618
+ });
3619
+ Object.defineProperty(exports, "compileJsonlQuery", {
3620
+ enumerable: true,
3621
+ get: function() {
3622
+ return compileJsonlQuery;
3623
+ }
3624
+ });
2889
3625
  Object.defineProperty(exports, "compileRecordZ", {
2890
3626
  enumerable: true,
2891
3627
  get: function() {
@@ -2910,12 +3646,30 @@ Object.defineProperty(exports, "configureCollectionHost", {
2910
3646
  return configureCollectionHost;
2911
3647
  }
2912
3648
  });
3649
+ Object.defineProperty(exports, "csvRowToItem", {
3650
+ enumerable: true,
3651
+ get: function() {
3652
+ return csvRowToItem;
3653
+ }
3654
+ });
2913
3655
  Object.defineProperty(exports, "daysInMonth", {
2914
3656
  enumerable: true,
2915
3657
  get: function() {
2916
3658
  return daysInMonth;
2917
3659
  }
2918
3660
  });
3661
+ Object.defineProperty(exports, "decodeCsvRecordId", {
3662
+ enumerable: true,
3663
+ get: function() {
3664
+ return decodeCsvRecordId;
3665
+ }
3666
+ });
3667
+ Object.defineProperty(exports, "dedupeByRecordId", {
3668
+ enumerable: true,
3669
+ get: function() {
3670
+ return dedupeByRecordId;
3671
+ }
3672
+ });
2919
3673
  Object.defineProperty(exports, "deleteCollection", {
2920
3674
  enumerable: true,
2921
3675
  get: function() {
@@ -2946,6 +3700,12 @@ Object.defineProperty(exports, "discoverCollections", {
2946
3700
  return discoverCollections;
2947
3701
  }
2948
3702
  });
3703
+ Object.defineProperty(exports, "encodeCsvRecordId", {
3704
+ enumerable: true,
3705
+ get: function() {
3706
+ return encodeCsvRecordId;
3707
+ }
3708
+ });
2949
3709
  Object.defineProperty(exports, "enrichItems", {
2950
3710
  enumerable: true,
2951
3711
  get: function() {
@@ -3042,6 +3802,12 @@ Object.defineProperty(exports, "maybeSpawnSuccessor", {
3042
3802
  return maybeSpawnSuccessor;
3043
3803
  }
3044
3804
  });
3805
+ Object.defineProperty(exports, "normalizeCsvValue", {
3806
+ enumerable: true,
3807
+ get: function() {
3808
+ return normalizeCsvValue;
3809
+ }
3810
+ });
3045
3811
  Object.defineProperty(exports, "parseCivil", {
3046
3812
  enumerable: true,
3047
3813
  get: function() {
@@ -3078,6 +3844,12 @@ Object.defineProperty(exports, "readItem", {
3078
3844
  return readItem;
3079
3845
  }
3080
3846
  });
3847
+ Object.defineProperty(exports, "readOnlyRefusal", {
3848
+ enumerable: true,
3849
+ get: function() {
3850
+ return readOnlyRefusal;
3851
+ }
3852
+ });
3081
3853
  Object.defineProperty(exports, "readSkillTemplate", {
3082
3854
  enumerable: true,
3083
3855
  get: function() {
@@ -3114,6 +3886,12 @@ Object.defineProperty(exports, "resolveTemplatePath", {
3114
3886
  return resolveTemplatePath;
3115
3887
  }
3116
3888
  });
3889
+ Object.defineProperty(exports, "runQueryOverRows", {
3890
+ enumerable: true,
3891
+ get: function() {
3892
+ return runQueryOverRows;
3893
+ }
3894
+ });
3117
3895
  Object.defineProperty(exports, "safeRecordId", {
3118
3896
  enumerable: true,
3119
3897
  get: function() {
@@ -3138,6 +3916,12 @@ Object.defineProperty(exports, "setCollectionChangePublisher", {
3138
3916
  return setCollectionChangePublisher;
3139
3917
  }
3140
3918
  });
3919
+ Object.defineProperty(exports, "storeFor", {
3920
+ enumerable: true,
3921
+ get: function() {
3922
+ return storeFor;
3923
+ }
3924
+ });
3141
3925
  Object.defineProperty(exports, "successorId", {
3142
3926
  enumerable: true,
3143
3927
  get: function() {
@@ -3181,4 +3965,4 @@ Object.defineProperty(exports, "writeItem", {
3181
3965
  }
3182
3966
  });
3183
3967
 
3184
- //# sourceMappingURL=server-Bh-DPK-H.cjs.map
3968
+ //# sourceMappingURL=server-Jaogu3Ky.cjs.map