@mulmoclaude/core 0.27.0 → 0.29.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (54) hide show
  1. package/assets/helps/collection-skills.md +4 -2
  2. package/assets/helps/wiki.md +47 -2
  3. package/dist/collection/core/ontologyGraph.d.ts +6 -2
  4. package/dist/collection/index.cjs +4 -4
  5. package/dist/collection/index.cjs.map +1 -1
  6. package/dist/collection/index.js +4 -4
  7. package/dist/collection/index.js.map +1 -1
  8. package/dist/collection/registry/server/index.cjs +2 -2
  9. package/dist/collection/registry/server/index.js +2 -2
  10. package/dist/collection/server/backendAvailability.d.ts +5 -0
  11. package/dist/collection/server/csvStore.d.ts +0 -5
  12. package/dist/collection/server/index.cjs +4 -2
  13. package/dist/collection/server/index.d.ts +1 -0
  14. package/dist/collection/server/index.js +3 -3
  15. package/dist/collection/server/manageTool.d.ts +4 -0
  16. package/dist/collection/server/schemaDocs.d.ts +21 -0
  17. package/dist/collection/server/store.d.ts +37 -0
  18. package/dist/collection/server/watchFs.d.ts +17 -0
  19. package/dist/collection-watchers/index.cjs +114 -203
  20. package/dist/collection-watchers/index.cjs.map +1 -1
  21. package/dist/collection-watchers/index.d.ts +1 -1
  22. package/dist/collection-watchers/index.js +114 -202
  23. package/dist/collection-watchers/index.js.map +1 -1
  24. package/dist/collection-watchers/watcher.d.ts +8 -13
  25. package/dist/{discovery-B9Vfojyy.js → discovery-BbsJwVEq.js} +128 -15
  26. package/dist/discovery-BbsJwVEq.js.map +1 -0
  27. package/dist/{discovery-BxbJy2VN.cjs → discovery-Bklck7Ck.cjs} +137 -12
  28. package/dist/discovery-Bklck7Ck.cjs.map +1 -0
  29. package/dist/feeds/server/index.cjs +2 -2
  30. package/dist/feeds/server/index.js +2 -2
  31. package/dist/google/index.cjs +1 -1
  32. package/dist/google/index.js +1 -1
  33. package/dist/{promptSafety-0ZKHX-6J.cjs → promptSafety-BFt2g_wn.cjs} +2 -2
  34. package/dist/promptSafety-BFt2g_wn.cjs.map +1 -0
  35. package/dist/{promptSafety-RE1SPxBN.js → promptSafety-cZIeiZtB.js} +3 -3
  36. package/dist/promptSafety-cZIeiZtB.js.map +1 -0
  37. package/dist/remote-view/index.cjs +8 -0
  38. package/dist/remote-view/index.cjs.map +1 -1
  39. package/dist/remote-view/index.d.ts +7 -0
  40. package/dist/remote-view/index.js +8 -1
  41. package/dist/remote-view/index.js.map +1 -1
  42. package/dist/{server-DOGKfUuD.cjs → server-CghbYZFY.cjs} +199 -22
  43. package/dist/server-CghbYZFY.cjs.map +1 -0
  44. package/dist/{server-sGNe-msA.js → server-DlG6ydHO.js} +200 -23
  45. package/dist/server-DlG6ydHO.js.map +1 -0
  46. package/dist/whisper/client.cjs.map +1 -1
  47. package/dist/whisper/client.js.map +1 -1
  48. package/package.json +1 -1
  49. package/dist/discovery-B9Vfojyy.js.map +0 -1
  50. package/dist/discovery-BxbJy2VN.cjs.map +0 -1
  51. package/dist/promptSafety-0ZKHX-6J.cjs.map +0 -1
  52. package/dist/promptSafety-RE1SPxBN.js.map +0 -1
  53. package/dist/server-DOGKfUuD.cjs.map +0 -1
  54. package/dist/server-sGNe-msA.js.map +0 -1
@@ -640,6 +640,18 @@ ${dataJson}
640
640
  ${templateText}`;
641
641
  }
642
642
  //#endregion
643
+ //#region src/collection/server/backendAvailability.ts
644
+ /** Thrown by a store when its engine or session cannot serve the request. */
645
+ var BackendUnavailableError = class extends Error {
646
+ constructor(message) {
647
+ super(message);
648
+ this.name = "BackendUnavailableError";
649
+ }
650
+ };
651
+ function isBackendUnavailable(err) {
652
+ return err instanceof BackendUnavailableError;
653
+ }
654
+ //#endregion
643
655
  //#region src/collection/core/queryZ.ts
644
656
  /** Result-column aliases double as SQL identifiers and JSON keys — keep
645
657
  * them to a conservative identifier charset so neither side needs
@@ -861,13 +873,23 @@ function decodeCsvRecordId(itemId) {
861
873
  * (date-only when the clock is exactly UTC midnight, matching the `date`
862
874
  * field contract), exotic DuckDB values → their string form. Pure +
863
875
  * exported for unit tests. */
876
+ /** `JSON.stringify` restricted to what a CSV cell can survive. Returns the
877
+ * serialised value, or `String(value)` when serialisation is impossible —
878
+ * losing the content of one cell is bad, failing the entire query is worse. */
879
+ function safeJsonCell(value) {
880
+ try {
881
+ return JSON.stringify(value, (_key, entry) => typeof entry === "bigint" ? entry.toString() : entry) ?? String(value);
882
+ } catch {
883
+ return String(value);
884
+ }
885
+ }
864
886
  function normalizeCsvValue(value) {
865
887
  if (typeof value === "bigint") return value <= BigInt(Number.MAX_SAFE_INTEGER) && value >= BigInt(-Number.MAX_SAFE_INTEGER) ? Number(value) : value.toString();
866
888
  if (value instanceof Date) {
867
889
  const iso = value.toISOString();
868
890
  return iso.endsWith("T00:00:00.000Z") ? iso.slice(0, 10) : iso;
869
891
  }
870
- if (value !== null && typeof value === "object") return String(value);
892
+ if (value !== null && typeof value === "object") return safeJsonCell(value);
871
893
  return value;
872
894
  }
873
895
  /** One raw DuckDB row → a CollectionItem, or null when the key cell is
@@ -878,10 +900,11 @@ function normalizeCsvValue(value) {
878
900
  function csvRowToItem(row, primaryKey) {
879
901
  const normalized = Object.fromEntries(Object.entries(row).map(([key, value]) => [key, normalizeCsvValue(value)]));
880
902
  const rawKey = normalized[primaryKey];
881
- if (rawKey === null || rawKey === void 0 || String(rawKey) === "") return null;
903
+ const keyText = require_ids.fieldTextOrNull(rawKey);
904
+ if (keyText === null || keyText === "") return null;
882
905
  return {
883
906
  ...normalized,
884
- [primaryKey]: encodeCsvRecordId(String(rawKey))
907
+ [primaryKey]: encodeCsvRecordId(keyText)
885
908
  };
886
909
  }
887
910
  /** Dedupe by record id, LAST row wins (matches `csvRead`'s last-match
@@ -1039,7 +1062,7 @@ async function duckDbInstance() {
1039
1062
  return await instancePromise;
1040
1063
  } catch (err) {
1041
1064
  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`);
1065
+ throw new BackendUnavailableError(`DuckDB is unavailable on this host (@duckdb/node-api failed to load: ${String(err)}) — dataSource collections cannot be read`);
1043
1066
  }
1044
1067
  }
1045
1068
  async function queryCsv(sql, params) {
@@ -1124,6 +1147,89 @@ async function csvRunQuery(absPath, primaryKey, query, workspaceRoot) {
1124
1147
  return (await queryCsv(sql, [utf8Path, ...params])).map((row) => Object.fromEntries(Object.entries(row).map(([key, value]) => [key, normalizeCsvValue(value)])));
1125
1148
  }
1126
1149
  //#endregion
1150
+ //#region src/collection/server/watchFs.ts
1151
+ /** An atomic file replace (editor save, `mv` over the target) surfaces as
1152
+ * 2-3 events. Collapse them so one user action reports one change. */
1153
+ var REPLACE_DEBOUNCE_MS = 300;
1154
+ /** The path to hand `watch()`, with Windows 8.3 short names resolved away.
1155
+ *
1156
+ * ReadDirectoryChangesW reports filenames against the LONG path, but a watch
1157
+ * opened on a short path (`C:\Users\RUNNER~1\…` — what `os.tmpdir()` returns
1158
+ * on GitHub's Windows runners) keeps the short form. libuv's
1159
+ * `assert(!_wcsnicmp(filename, dir, dirlen))` in `src/win/fs-event.c` then
1160
+ * aborts the PROCESS on the first event — a native assert, so neither
1161
+ * `watcher.on("error")` nor a try/catch can contain it.
1162
+ *
1163
+ * POSIX is deliberately left alone: `realpath` there also collapses symlinks
1164
+ * (`/var` → `/private/var` on macOS), which we neither need nor want to
1165
+ * change. A failure falls back to the original path — worst case we are no
1166
+ * worse off than before. */
1167
+ function watchablePath(dir) {
1168
+ if (process.platform !== "win32") return dir;
1169
+ try {
1170
+ return node_fs.realpathSync.native(dir);
1171
+ } catch {
1172
+ return dir;
1173
+ }
1174
+ }
1175
+ /** Watch `dir`, reporting each accepted filename. `accept` decides what is
1176
+ * noise; a null filename always passes (the platform didn't tell us which
1177
+ * file, so the caller must assume the worst). */
1178
+ async function watchDirectory(dir, accept, onHit) {
1179
+ try {
1180
+ await (0, node_fs_promises.mkdir)(dir, { recursive: true });
1181
+ const watcher = (0, node_fs.watch)(watchablePath(dir), { persistent: false }, (_eventType, rawFilename) => {
1182
+ const filename = rawFilename === null ? null : String(rawFilename);
1183
+ if (filename !== null && !accept(filename)) return;
1184
+ onHit(filename);
1185
+ });
1186
+ watcher.on("error", (err) => {
1187
+ log.warn("collections", "fs watch error", {
1188
+ dir,
1189
+ error: String(err)
1190
+ });
1191
+ });
1192
+ return { close: () => watcher.close() };
1193
+ } catch (err) {
1194
+ log.warn("collections", "fs watch start failed", {
1195
+ dir,
1196
+ error: String(err)
1197
+ });
1198
+ return null;
1199
+ }
1200
+ }
1201
+ /** Watch the single file `absPath` by watching its PARENT directory, so an
1202
+ * atomic replace can't strand the watch on a dead inode. `alsoAccept`
1203
+ * widens the filter beyond the exact basename (sqlite's `-wal`/`-journal`
1204
+ * sidecars). Reports are debounced: one replace, one call. */
1205
+ async function watchSingleFile(absPath, alsoAccept, onChange) {
1206
+ const dir = node_path.default.dirname(absPath);
1207
+ const base = node_path.default.basename(absPath);
1208
+ let timer = null;
1209
+ const fire = () => {
1210
+ if (timer) clearTimeout(timer);
1211
+ timer = setTimeout(() => {
1212
+ timer = null;
1213
+ onChange();
1214
+ }, REPLACE_DEBOUNCE_MS);
1215
+ timer.unref?.();
1216
+ };
1217
+ const handle = await watchDirectory(dir, (filename) => filename === base || alsoAccept(base, filename), fire);
1218
+ if (!handle) return null;
1219
+ return { close: () => {
1220
+ if (timer) clearTimeout(timer);
1221
+ timer = null;
1222
+ handle.close();
1223
+ } };
1224
+ }
1225
+ /** An `FsWatchHandle` as a bare unsubscribe — `null` straight through, so an
1226
+ * unarmed watch stays distinguishable from an armed one. Lives here rather
1227
+ * than beside the store contract so both `store.ts` and the backends it
1228
+ * registers can reach it without importing each other. */
1229
+ function closerFor(handle) {
1230
+ return handle === null ? null : () => handle.close();
1231
+ }
1232
+ //#endregion
1127
1233
  //#region src/collection/server/storePage.ts
1128
1234
  /** Project `fields` (+ the primary key, always) out of each record. Thin
1129
1235
  * server-typed alias over the shared isomorphic `projectRecordFields`
@@ -1151,7 +1257,7 @@ var sqliteModule = null;
1151
1257
  function loadSqlite() {
1152
1258
  sqliteModule ??= import("node:sqlite").then((mod) => mod, (err) => {
1153
1259
  sqliteModule = null;
1154
- throw new Error(`sqlite storage needs the node:sqlite module (Node.js >= 22.5) — this runtime cannot load it: ${String(err)}`);
1260
+ throw new BackendUnavailableError(`sqlite storage needs the node:sqlite module (Node.js >= 22.5) — this runtime cannot load it: ${String(err)}`);
1155
1261
  });
1156
1262
  return sqliteModule;
1157
1263
  }
@@ -1388,7 +1494,8 @@ function sqliteStoreFor(collection, opts) {
1388
1494
  delete: (itemId) => sqliteDelete(file, itemId, {
1389
1495
  workspaceRoot: root(),
1390
1496
  slug
1391
- })
1497
+ }),
1498
+ watch: async (onChange) => closerFor(await watchSingleFile(file, (base, name) => name.startsWith(base), () => onChange({ kind: "collection" })))
1392
1499
  };
1393
1500
  }
1394
1501
  //#endregion
@@ -1398,8 +1505,8 @@ function sqliteStoreFor(collection, opts) {
1398
1505
  * is filesystem-dependent; paging needs determinism. */
1399
1506
  function sortByRecordId(items, primaryKey) {
1400
1507
  return [...items].sort((left, right) => {
1401
- const leftId = String(left[primaryKey] ?? "");
1402
- const rightId = String(right[primaryKey] ?? "");
1508
+ const leftId = require_ids.fieldText(left[primaryKey]);
1509
+ const rightId = require_ids.fieldText(right[primaryKey]);
1403
1510
  if (leftId < rightId) return -1;
1404
1511
  return leftId > rightId ? 1 : 0;
1405
1512
  });
@@ -1435,7 +1542,8 @@ function csvStoreFor(collection, opts) {
1435
1542
  list: () => listAll().then((result) => result.items),
1436
1543
  page: (pageOpts = {}) => listAll().then((result) => pageFromFullRead(result.items, pageOpts, key, result.truncated)),
1437
1544
  read: (itemId) => file === void 0 ? Promise.resolve(null) : csvRead(file, key, itemId, opts.workspaceRoot),
1438
- query: (query) => file === void 0 ? Promise.resolve([]) : csvRunQuery(file, key, query, opts.workspaceRoot)
1545
+ query: (query) => file === void 0 ? Promise.resolve([]) : csvRunQuery(file, key, query, opts.workspaceRoot),
1546
+ ...file === void 0 ? {} : { watch: async (onChange) => closerFor(await watchSingleFile(file, () => false, () => onChange({ kind: "collection" }))) }
1439
1547
  };
1440
1548
  }
1441
1549
  /** The classic file store over `<dataDir>/<itemId>.json` records. */
@@ -1458,7 +1566,11 @@ function fileStoreFor(collection, opts) {
1458
1566
  ...ioOpts,
1459
1567
  refuseOverwrite: writeOpts.refuseOverwrite
1460
1568
  }),
1461
- delete: (itemId) => deleteItem(collection.dataDir, itemId, ioOpts)
1569
+ delete: (itemId) => deleteItem(collection.dataDir, itemId, ioOpts),
1570
+ watch: async (onChange) => closerFor(await watchDirectory(collection.dataDir, (name) => name.endsWith(".json") && !name.startsWith("."), (filename) => onChange(filename === null ? { kind: "collection" } : {
1571
+ kind: "item",
1572
+ itemId: filename.slice(0, -5)
1573
+ })))
1462
1574
  };
1463
1575
  }
1464
1576
  var storeFactories = /* @__PURE__ */ new Map([
@@ -2034,7 +2146,8 @@ function fieldDrivenFromFieldCarried(schema) {
2034
2146
  if (set && Object.prototype.hasOwnProperty.call(set, driven.fromField)) {
2035
2147
  const raw = set[driven.fromField];
2036
2148
  if (raw === void 0 || raw === null || raw === "") return false;
2037
- return Object.prototype.hasOwnProperty.call(driven.map, String(raw));
2149
+ const key = require_ids.fieldTextOrNull(raw);
2150
+ return key !== null && Object.prototype.hasOwnProperty.call(driven.map, key);
2038
2151
  }
2039
2152
  return (carry ?? []).includes(driven.fromField);
2040
2153
  }
@@ -2509,6 +2622,12 @@ function toDetail(collection) {
2509
2622
  };
2510
2623
  }
2511
2624
  //#endregion
2625
+ Object.defineProperty(exports, "BackendUnavailableError", {
2626
+ enumerable: true,
2627
+ get: function() {
2628
+ return BackendUnavailableError;
2629
+ }
2630
+ });
2512
2631
  Object.defineProperty(exports, "CollectionQueryZ", {
2513
2632
  enumerable: true,
2514
2633
  get: function() {
@@ -2659,6 +2778,12 @@ Object.defineProperty(exports, "getWorkspaceRoot", {
2659
2778
  return getWorkspaceRoot;
2660
2779
  }
2661
2780
  });
2781
+ Object.defineProperty(exports, "isBackendUnavailable", {
2782
+ enumerable: true,
2783
+ get: function() {
2784
+ return isBackendUnavailable;
2785
+ }
2786
+ });
2662
2787
  Object.defineProperty(exports, "isContainedInRoot", {
2663
2788
  enumerable: true,
2664
2789
  get: function() {
@@ -2852,4 +2977,4 @@ Object.defineProperty(exports, "writeItem", {
2852
2977
  }
2853
2978
  });
2854
2979
 
2855
- //# sourceMappingURL=discovery-BxbJy2VN.cjs.map
2980
+ //# sourceMappingURL=discovery-Bklck7Ck.cjs.map