@mulmoclaude/core 1.12.0 → 1.14.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 (117) hide show
  1. package/assets/helps/error-recovery.md +110 -1
  2. package/dist/{calendarGrid-CaS9er8i.cjs → calendarGrid-BQYqOCnF.cjs} +8 -9
  3. package/dist/{calendarGrid-CaS9er8i.cjs.map → calendarGrid-BQYqOCnF.cjs.map} +1 -1
  4. package/dist/{calendarGrid-CrGxeCOG.js → calendarGrid-DivcNKkX.js} +8 -9
  5. package/dist/{calendarGrid-CrGxeCOG.js.map → calendarGrid-DivcNKkX.js.map} +1 -1
  6. package/dist/collection/core/project.d.ts +11 -0
  7. package/dist/collection/core/schemaZ.d.ts +4 -4
  8. package/dist/collection/index.cjs +5 -4
  9. package/dist/collection/index.js +5 -5
  10. package/dist/collection/registry/server/index.cjs +3 -3
  11. package/dist/collection/registry/server/index.cjs.map +1 -1
  12. package/dist/collection/registry/server/index.js +3 -3
  13. package/dist/collection/registry/server/index.js.map +1 -1
  14. package/dist/collection/server/csvQuery.d.ts +7 -2
  15. package/dist/collection/server/csvStore.d.ts +2 -1
  16. package/dist/collection/server/index.cjs +3 -3
  17. package/dist/collection/server/index.js +3 -3
  18. package/dist/collection-watchers/index.cjs +10 -25
  19. package/dist/collection-watchers/index.cjs.map +1 -1
  20. package/dist/collection-watchers/index.js +10 -25
  21. package/dist/collection-watchers/index.js.map +1 -1
  22. package/dist/{discovery-NRy3tyUA.cjs → discovery-CRDx6TS4.cjs} +88 -47
  23. package/dist/discovery-CRDx6TS4.cjs.map +1 -0
  24. package/dist/{discovery-B4CZvrXR.js → discovery-D6p4ITNR.js} +88 -47
  25. package/dist/discovery-D6p4ITNR.js.map +1 -0
  26. package/dist/{dist-pWpC-b04.cjs → dist-CsgSfWwR.cjs} +66 -1
  27. package/dist/{dist-pWpC-b04.cjs.map → dist-CsgSfWwR.cjs.map} +1 -1
  28. package/dist/{dist-Cwk0e12G.js → dist-D8zokgGo.js} +25 -2
  29. package/dist/{dist-Cwk0e12G.js.map → dist-D8zokgGo.js.map} +1 -1
  30. package/dist/feeds/index.cjs +2 -2
  31. package/dist/feeds/index.js +2 -2
  32. package/dist/feeds/server/engine.d.ts +27 -0
  33. package/dist/feeds/server/index.cjs +97 -26
  34. package/dist/feeds/server/index.cjs.map +1 -1
  35. package/dist/feeds/server/index.d.ts +1 -1
  36. package/dist/feeds/server/index.js +96 -27
  37. package/dist/feeds/server/index.js.map +1 -1
  38. package/dist/google/calendar.d.ts +23 -9
  39. package/dist/google/calendarStateLock.d.ts +23 -0
  40. package/dist/google/collectionProjection.d.ts +0 -8
  41. package/dist/google/collectionPush.d.ts +13 -8
  42. package/dist/google/collectionSync.d.ts +80 -10
  43. package/dist/google/index.cjs +366 -98
  44. package/dist/google/index.cjs.map +1 -1
  45. package/dist/google/index.d.ts +6 -4
  46. package/dist/google/index.js +357 -99
  47. package/dist/google/index.js.map +1 -1
  48. package/dist/{ingestTypes-D1GQdG8e.cjs → ingestTypes-C7IJSqv4.cjs} +2 -2
  49. package/dist/{ingestTypes-D1GQdG8e.cjs.map → ingestTypes-C7IJSqv4.cjs.map} +1 -1
  50. package/dist/{ingestTypes-XReG-li7.js → ingestTypes-DmtDLRmt.js} +2 -2
  51. package/dist/{ingestTypes-XReG-li7.js.map → ingestTypes-DmtDLRmt.js.map} +1 -1
  52. package/dist/notifier/index.cjs +1 -1
  53. package/dist/notifier/index.js +1 -1
  54. package/dist/notifier/store.d.ts +4 -4
  55. package/dist/{notifier-BdA5qzhe.js → notifier-Dxh8mEFh.js} +39 -25
  56. package/dist/notifier-Dxh8mEFh.js.map +1 -0
  57. package/dist/{notifier-tMsAXyXp.cjs → notifier-Wc4M24Bi.cjs} +39 -25
  58. package/dist/notifier-Wc4M24Bi.cjs.map +1 -0
  59. package/dist/plugin-vue/index.cjs +4 -2
  60. package/dist/plugin-vue/index.cjs.map +1 -1
  61. package/dist/plugin-vue/index.js +4 -2
  62. package/dist/plugin-vue/index.js.map +1 -1
  63. package/dist/project-B0j_Vf_Q.js +33 -0
  64. package/dist/project-B0j_Vf_Q.js.map +1 -0
  65. package/dist/project-C1ep9pvo.cjs +44 -0
  66. package/dist/project-C1ep9pvo.cjs.map +1 -0
  67. package/dist/{promptSafety-FD8cQn3e.cjs → promptSafety-B52GvaJc.cjs} +99 -85
  68. package/dist/promptSafety-B52GvaJc.cjs.map +1 -0
  69. package/dist/{promptSafety-C--op_6a.js → promptSafety-DJgaDjM4.js} +99 -85
  70. package/dist/promptSafety-DJgaDjM4.js.map +1 -0
  71. package/dist/remote-host/index.cjs +2 -1
  72. package/dist/remote-host/index.d.ts +10 -0
  73. package/dist/remote-host/index.js +2 -2
  74. package/dist/remote-host/server/index.cjs +2 -2
  75. package/dist/remote-host/server/index.cjs.map +1 -1
  76. package/dist/remote-host/server/index.js +2 -2
  77. package/dist/remote-host/server/index.js.map +1 -1
  78. package/dist/{remote-host-DkDVxNim.cjs → remote-host-CtQjagPt.cjs} +53 -1
  79. package/dist/remote-host-CtQjagPt.cjs.map +1 -0
  80. package/dist/remote-host-Dba4lF3Z.js +82 -0
  81. package/dist/remote-host-Dba4lF3Z.js.map +1 -0
  82. package/dist/remote-view/index.cjs +6 -4
  83. package/dist/remote-view/index.cjs.map +1 -1
  84. package/dist/remote-view/index.js +6 -4
  85. package/dist/remote-view/index.js.map +1 -1
  86. package/dist/scheduler/index.cjs +1 -1
  87. package/dist/scheduler/index.js +1 -1
  88. package/dist/{server-DdyIvFhz.js → server-D6EUf3BF.js} +46 -27
  89. package/dist/server-D6EUf3BF.js.map +1 -0
  90. package/dist/{server-7U-3DE2e.cjs → server-D7_oRbTJ.cjs} +46 -27
  91. package/dist/server-D7_oRbTJ.cjs.map +1 -0
  92. package/dist/utils/index.cjs +1 -1
  93. package/dist/utils/index.js +1 -1
  94. package/dist/whisper/index.cjs +2 -6
  95. package/dist/whisper/index.cjs.map +1 -1
  96. package/dist/whisper/index.js +2 -6
  97. package/dist/whisper/index.js.map +1 -1
  98. package/dist/wiki/index.cjs +2 -2
  99. package/dist/wiki/index.cjs.map +1 -1
  100. package/dist/wiki/index.js +2 -2
  101. package/dist/wiki/index.js.map +1 -1
  102. package/package.json +2 -2
  103. package/dist/discovery-B4CZvrXR.js.map +0 -1
  104. package/dist/discovery-NRy3tyUA.cjs.map +0 -1
  105. package/dist/notifier-BdA5qzhe.js.map +0 -1
  106. package/dist/notifier-tMsAXyXp.cjs.map +0 -1
  107. package/dist/project-BWI5w_BT.cjs +0 -17
  108. package/dist/project-BWI5w_BT.cjs.map +0 -1
  109. package/dist/project-bU98ycsy.js +0 -12
  110. package/dist/project-bU98ycsy.js.map +0 -1
  111. package/dist/promptSafety-C--op_6a.js.map +0 -1
  112. package/dist/promptSafety-FD8cQn3e.cjs.map +0 -1
  113. package/dist/remote-host-D_BRFHcI.js +0 -36
  114. package/dist/remote-host-D_BRFHcI.js.map +0 -1
  115. package/dist/remote-host-DkDVxNim.cjs.map +0 -1
  116. package/dist/server-7U-3DE2e.cjs.map +0 -1
  117. package/dist/server-DdyIvFhz.js.map +0 -1
@@ -3,10 +3,33 @@
3
3
  function isRecord(value) {
4
4
  return typeof value === "object" && value !== null && !Array.isArray(value);
5
5
  }
6
+ /** Narrow `unknown` to any object (not null, arrays allowed).
7
+ * Use `isRecord` when you need to access string keys. */
8
+ function isObj(value) {
9
+ return typeof value === "object" && value !== null;
10
+ }
11
+ /** String array (every element is a string). */
12
+ function isStringArray(value) {
13
+ return Array.isArray(value) && value.every((val) => typeof val === "string");
14
+ }
15
+ /** An array of unknowns. Prefer this over a bare `Array.isArray` in typed code:
16
+ * `Array.isArray(x: unknown)` narrows to `any[]`, silently reintroducing
17
+ * `any`, whereas this keeps the element type `unknown`. */
18
+ function isUnknownArray(value) {
19
+ return Array.isArray(value);
20
+ }
21
+ /** Error-like object with a `code` property (e.g. Node.js fs errors). */
22
+ function isErrorWithCode(value) {
23
+ return isRecord(value) && typeof value.code === "string";
24
+ }
6
25
  /** Check that a record has a specific key with a string value. */
7
26
  function hasStringProp(value, key) {
8
27
  return isRecord(value) && typeof value[key] === "string";
9
28
  }
29
+ /** Check that a record has a specific key with a number value. */
30
+ function hasNumberProp(value, key) {
31
+ return isRecord(value) && typeof value[key] === "number";
32
+ }
10
33
  /** Normalise an unknown thrown value into a human-readable string. Isomorphic
11
34
  * (host, bridges, plugins, Vue) — this is the single home for the helper that
12
35
  * #2217 could only consolidate for server code, since `@mulmoclaude/core/utils`
@@ -57,5 +80,47 @@ Object.defineProperty(exports, "escapeHtml", {
57
80
  return escapeHtml;
58
81
  }
59
82
  });
83
+ Object.defineProperty(exports, "hasNumberProp", {
84
+ enumerable: true,
85
+ get: function() {
86
+ return hasNumberProp;
87
+ }
88
+ });
89
+ Object.defineProperty(exports, "hasStringProp", {
90
+ enumerable: true,
91
+ get: function() {
92
+ return hasStringProp;
93
+ }
94
+ });
95
+ Object.defineProperty(exports, "isErrorWithCode", {
96
+ enumerable: true,
97
+ get: function() {
98
+ return isErrorWithCode;
99
+ }
100
+ });
101
+ Object.defineProperty(exports, "isObj", {
102
+ enumerable: true,
103
+ get: function() {
104
+ return isObj;
105
+ }
106
+ });
107
+ Object.defineProperty(exports, "isRecord", {
108
+ enumerable: true,
109
+ get: function() {
110
+ return isRecord;
111
+ }
112
+ });
113
+ Object.defineProperty(exports, "isStringArray", {
114
+ enumerable: true,
115
+ get: function() {
116
+ return isStringArray;
117
+ }
118
+ });
119
+ Object.defineProperty(exports, "isUnknownArray", {
120
+ enumerable: true,
121
+ get: function() {
122
+ return isUnknownArray;
123
+ }
124
+ });
60
125
 
61
- //# sourceMappingURL=dist-pWpC-b04.cjs.map
126
+ //# sourceMappingURL=dist-CsgSfWwR.cjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"dist-pWpC-b04.cjs","names":[],"sources":["../../common/dist/index.js"],"sourcesContent":["// General-purpose runtime type guards, shared across the MulmoClaude host,\n// bridges, and plugins. This is a leaf package — pure and dependency-free — so\n// any tier can import it without creating an uphill edge.\n//\n// These originated as `server/utils/types.ts` (#504), which centralised 40+\n// hand-written inline `typeof x === \"object\"` checks. They are promoted here so\n// the same guards stop being re-hand-written in every bridge and plugin too.\n/** Narrow `unknown` to a plain object (not null, not array). */\nexport function isRecord(value) {\n return typeof value === \"object\" && value !== null && !Array.isArray(value);\n}\n/** Narrow `unknown` to any object (not null, arrays allowed).\n * Use `isRecord` when you need to access string keys. */\nexport function isObj(value) {\n return typeof value === \"object\" && value !== null;\n}\n/** Non-empty string after trimming whitespace. */\nexport function isNonEmptyString(value) {\n return typeof value === \"string\" && value.trim().length > 0;\n}\n/** Record whose values are all strings. */\nexport function isStringRecord(value) {\n if (!isRecord(value))\n return false;\n return Object.values(value).every((val) => typeof val === \"string\");\n}\n/** String array (every element is a string). */\nexport function isStringArray(value) {\n return Array.isArray(value) && value.every((val) => typeof val === \"string\");\n}\n/** An array of unknowns. Prefer this over a bare `Array.isArray` in typed code:\n * `Array.isArray(x: unknown)` narrows to `any[]`, silently reintroducing\n * `any`, whereas this keeps the element type `unknown`. */\nexport function isUnknownArray(value) {\n return Array.isArray(value);\n}\n/** Error-like object with a `code` property (e.g. Node.js fs errors). */\nexport function isErrorWithCode(value) {\n return isRecord(value) && typeof value.code === \"string\";\n}\n/** Check that a record has a specific key with a string value. */\nexport function hasStringProp(value, key) {\n return isRecord(value) && typeof value[key] === \"string\";\n}\n/** Check that a record has a specific key with a number value. */\nexport function hasNumberProp(value, key) {\n return isRecord(value) && typeof value[key] === \"number\";\n}\n/** Split a comma-separated env value into trimmed, non-empty entries.\n * `lowercase` folds case for identifiers compared case-insensitively\n * (JIDs, email addresses, hex pubkeys). Absent/empty input → empty list. */\nexport function parseCsvList(raw, opts) {\n return (raw ?? \"\")\n .split(\",\")\n .map((entry) => (opts?.lowercase ? entry.trim().toLowerCase() : entry.trim()))\n .filter(Boolean);\n}\n/** A comma-separated env value as a Set — the canonical allowlist shape,\n * where an empty set is the \"allow all\" sentinel (`set.size === 0`). */\nexport function parseCsvSet(raw, opts) {\n return new Set(parseCsvList(raw, opts));\n}\n/** Normalise an unknown thrown value into a human-readable string. Isomorphic\n * (host, bridges, plugins, Vue) — this is the single home for the helper that\n * #2217 could only consolidate for server code, since `@mulmoclaude/core/utils`\n * is server-only.\n *\n * A non-Error object with a non-empty string `details` (gRPC convention) or\n * `message` field surfaces that field — `details` wins — instead of the\n * `[object Object]` a bare `String(err)` would print; an empty-string field\n * falls through. `fallback` covers the error-boundary idiom where a thrown\n * non-Error should read as a descriptive message rather than `String(err)`\n * noise; omit it in logging contexts where `String(err)` is fine. */\nexport function errorMessage(err, fallback) {\n if (err instanceof Error)\n return err.message;\n if (hasStringProp(err, \"details\") && err.details)\n return err.details;\n if (hasStringProp(err, \"message\") && err.message)\n return err.message;\n if (fallback !== undefined)\n return fallback;\n return String(err);\n}\n/** `Date` → `YYYY-MM-DD` in UTC — for dates that must not shift with the\n * host's local timezone (tool-trace search dirs, API date keys). Isomorphic\n * single source (#2480): the host re-exports it from `server/utils/date.ts`,\n * x-plugin imports it directly. The `@receptron/task-scheduler` copy stays\n * local on purpose — that leaf package is published independently and kept\n * dependency-free. Wall-clock questions use the host's `toLocalIsoDate`. */\nexport function toUtcIsoDate(timestamp) {\n const year = timestamp.getUTCFullYear();\n const month = String(timestamp.getUTCMonth() + 1).padStart(2, \"0\");\n const day = String(timestamp.getUTCDate()).padStart(2, \"0\");\n return `${year}-${month}-${day}`;\n}\n// A Map, not an object literal: `{}[char]` reads through the prototype chain,\n// so a future caller widening the regex would silently get `[object Object]`\n// for keys like `constructor`.\nconst HTML_ESCAPES = new Map([\n [\"&\", \"&amp;\"],\n [\"<\", \"&lt;\"],\n [\">\", \"&gt;\"],\n ['\"', \"&quot;\"],\n [\"'\", \"&#39;\"],\n]);\n/** HTML-escape text destined for markup or an attribute value — the fixed\n * five-character map, nothing more. Lives here rather than in\n * `@mulmoclaude/core/wiki` (#2483) because `@mulmoclaude/markdown-utils` is a\n * leaf that core depends on and so cannot import back up; core/wiki\n * re-exports this, keeping its consumers' import path unchanged.\n *\n * Escaping `&` first is what makes a single pass safe — the entities this\n * introduces contain none of the other four characters, so nothing is\n * double-escaped. Not a sanitiser: it neither strips tags nor validates URLs. */\nexport function escapeHtml(value) {\n return value.replace(/[&<>\"']/g, (char) => HTML_ESCAPES.get(char) ?? char);\n}\nexport { scanEnvOptions, snakeToLowerCamel } from \"./envScan.js\";\n"],"mappings":";;AAQA,SAAgB,SAAS,OAAO;CAC5B,OAAO,OAAO,UAAU,YAAY,UAAU,QAAQ,CAAC,MAAM,QAAQ,KAAK;AAC9E;;AA+BA,SAAgB,cAAc,OAAO,KAAK;CACtC,OAAO,SAAS,KAAK,KAAK,OAAO,MAAM,SAAS;AACpD;;;;;;;;;;;;AA8BA,SAAgB,aAAa,KAAK,UAAU;CACxC,IAAI,eAAe,OACf,OAAO,IAAI;CACf,IAAI,cAAc,KAAK,SAAS,KAAK,IAAI,SACrC,OAAO,IAAI;CACf,IAAI,cAAc,KAAK,SAAS,KAAK,IAAI,SACrC,OAAO,IAAI;CACf,IAAI,aAAa,KAAA,GACb,OAAO;CACX,OAAO,OAAO,GAAG;AACrB;AAgBA,IAAM,+BAAe,IAAI,IAAI;CACzB,CAAC,KAAK,OAAO;CACb,CAAC,KAAK,MAAM;CACZ,CAAC,KAAK,MAAM;CACZ,CAAC,MAAK,QAAQ;CACd,CAAC,KAAK,OAAO;AACjB,CAAC;;;;;;;;;;AAUD,SAAgB,WAAW,OAAO;CAC9B,OAAO,MAAM,QAAQ,aAAa,SAAS,aAAa,IAAI,IAAI,KAAK,IAAI;AAC7E"}
1
+ {"version":3,"file":"dist-CsgSfWwR.cjs","names":[],"sources":["../../common/dist/index.js"],"sourcesContent":["// General-purpose runtime type guards, shared across the MulmoClaude host,\n// bridges, and plugins. This is a leaf package — pure and dependency-free — so\n// any tier can import it without creating an uphill edge.\n//\n// These originated as `server/utils/types.ts` (#504), which centralised 40+\n// hand-written inline `typeof x === \"object\"` checks. They are promoted here so\n// the same guards stop being re-hand-written in every bridge and plugin too.\n/** Narrow `unknown` to a plain object (not null, not array). */\nexport function isRecord(value) {\n return typeof value === \"object\" && value !== null && !Array.isArray(value);\n}\n/** Narrow `unknown` to any object (not null, arrays allowed).\n * Use `isRecord` when you need to access string keys. */\nexport function isObj(value) {\n return typeof value === \"object\" && value !== null;\n}\n/** Non-empty string after trimming whitespace. */\nexport function isNonEmptyString(value) {\n return typeof value === \"string\" && value.trim().length > 0;\n}\n/** Record whose values are all strings. */\nexport function isStringRecord(value) {\n if (!isRecord(value))\n return false;\n return Object.values(value).every((val) => typeof val === \"string\");\n}\n/** String array (every element is a string). */\nexport function isStringArray(value) {\n return Array.isArray(value) && value.every((val) => typeof val === \"string\");\n}\n/** An array of unknowns. Prefer this over a bare `Array.isArray` in typed code:\n * `Array.isArray(x: unknown)` narrows to `any[]`, silently reintroducing\n * `any`, whereas this keeps the element type `unknown`. */\nexport function isUnknownArray(value) {\n return Array.isArray(value);\n}\n/** Error-like object with a `code` property (e.g. Node.js fs errors). */\nexport function isErrorWithCode(value) {\n return isRecord(value) && typeof value.code === \"string\";\n}\n/** Check that a record has a specific key with a string value. */\nexport function hasStringProp(value, key) {\n return isRecord(value) && typeof value[key] === \"string\";\n}\n/** Check that a record has a specific key with a number value. */\nexport function hasNumberProp(value, key) {\n return isRecord(value) && typeof value[key] === \"number\";\n}\n/** Split a comma-separated env value into trimmed, non-empty entries.\n * `lowercase` folds case for identifiers compared case-insensitively\n * (JIDs, email addresses, hex pubkeys). Absent/empty input → empty list. */\nexport function parseCsvList(raw, opts) {\n return (raw ?? \"\")\n .split(\",\")\n .map((entry) => (opts?.lowercase ? entry.trim().toLowerCase() : entry.trim()))\n .filter(Boolean);\n}\n/** A comma-separated env value as a Set — the canonical allowlist shape,\n * where an empty set is the \"allow all\" sentinel (`set.size === 0`). */\nexport function parseCsvSet(raw, opts) {\n return new Set(parseCsvList(raw, opts));\n}\n/** Normalise an unknown thrown value into a human-readable string. Isomorphic\n * (host, bridges, plugins, Vue) — this is the single home for the helper that\n * #2217 could only consolidate for server code, since `@mulmoclaude/core/utils`\n * is server-only.\n *\n * A non-Error object with a non-empty string `details` (gRPC convention) or\n * `message` field surfaces that field — `details` wins — instead of the\n * `[object Object]` a bare `String(err)` would print; an empty-string field\n * falls through. `fallback` covers the error-boundary idiom where a thrown\n * non-Error should read as a descriptive message rather than `String(err)`\n * noise; omit it in logging contexts where `String(err)` is fine. */\nexport function errorMessage(err, fallback) {\n if (err instanceof Error)\n return err.message;\n if (hasStringProp(err, \"details\") && err.details)\n return err.details;\n if (hasStringProp(err, \"message\") && err.message)\n return err.message;\n if (fallback !== undefined)\n return fallback;\n return String(err);\n}\n/** `Date` → `YYYY-MM-DD` in UTC — for dates that must not shift with the\n * host's local timezone (tool-trace search dirs, API date keys). Isomorphic\n * single source (#2480): the host re-exports it from `server/utils/date.ts`,\n * x-plugin imports it directly. The `@receptron/task-scheduler` copy stays\n * local on purpose — that leaf package is published independently and kept\n * dependency-free. Wall-clock questions use the host's `toLocalIsoDate`. */\nexport function toUtcIsoDate(timestamp) {\n const year = timestamp.getUTCFullYear();\n const month = String(timestamp.getUTCMonth() + 1).padStart(2, \"0\");\n const day = String(timestamp.getUTCDate()).padStart(2, \"0\");\n return `${year}-${month}-${day}`;\n}\n// A Map, not an object literal: `{}[char]` reads through the prototype chain,\n// so a future caller widening the regex would silently get `[object Object]`\n// for keys like `constructor`.\nconst HTML_ESCAPES = new Map([\n [\"&\", \"&amp;\"],\n [\"<\", \"&lt;\"],\n [\">\", \"&gt;\"],\n ['\"', \"&quot;\"],\n [\"'\", \"&#39;\"],\n]);\n/** HTML-escape text destined for markup or an attribute value — the fixed\n * five-character map, nothing more. Lives here rather than in\n * `@mulmoclaude/core/wiki` (#2483) because `@mulmoclaude/markdown-utils` is a\n * leaf that core depends on and so cannot import back up; core/wiki\n * re-exports this, keeping its consumers' import path unchanged.\n *\n * Escaping `&` first is what makes a single pass safe — the entities this\n * introduces contain none of the other four characters, so nothing is\n * double-escaped. Not a sanitiser: it neither strips tags nor validates URLs. */\nexport function escapeHtml(value) {\n return value.replace(/[&<>\"']/g, (char) => HTML_ESCAPES.get(char) ?? char);\n}\nexport { scanEnvOptions, snakeToLowerCamel } from \"./envScan.js\";\n"],"mappings":";;AAQA,SAAgB,SAAS,OAAO;CAC5B,OAAO,OAAO,UAAU,YAAY,UAAU,QAAQ,CAAC,MAAM,QAAQ,KAAK;AAC9E;;;AAGA,SAAgB,MAAM,OAAO;CACzB,OAAO,OAAO,UAAU,YAAY,UAAU;AAClD;;AAYA,SAAgB,cAAc,OAAO;CACjC,OAAO,MAAM,QAAQ,KAAK,KAAK,MAAM,OAAO,QAAQ,OAAO,QAAQ,QAAQ;AAC/E;;;;AAIA,SAAgB,eAAe,OAAO;CAClC,OAAO,MAAM,QAAQ,KAAK;AAC9B;;AAEA,SAAgB,gBAAgB,OAAO;CACnC,OAAO,SAAS,KAAK,KAAK,OAAO,MAAM,SAAS;AACpD;;AAEA,SAAgB,cAAc,OAAO,KAAK;CACtC,OAAO,SAAS,KAAK,KAAK,OAAO,MAAM,SAAS;AACpD;;AAEA,SAAgB,cAAc,OAAO,KAAK;CACtC,OAAO,SAAS,KAAK,KAAK,OAAO,MAAM,SAAS;AACpD;;;;;;;;;;;;AA0BA,SAAgB,aAAa,KAAK,UAAU;CACxC,IAAI,eAAe,OACf,OAAO,IAAI;CACf,IAAI,cAAc,KAAK,SAAS,KAAK,IAAI,SACrC,OAAO,IAAI;CACf,IAAI,cAAc,KAAK,SAAS,KAAK,IAAI,SACrC,OAAO,IAAI;CACf,IAAI,aAAa,KAAA,GACb,OAAO;CACX,OAAO,OAAO,GAAG;AACrB;AAgBA,IAAM,+BAAe,IAAI,IAAI;CACzB,CAAC,KAAK,OAAO;CACb,CAAC,KAAK,MAAM;CACZ,CAAC,KAAK,MAAM;CACZ,CAAC,MAAK,QAAQ;CACd,CAAC,KAAK,OAAO;AACjB,CAAC;;;;;;;;;;AAUD,SAAgB,WAAW,OAAO;CAC9B,OAAO,MAAM,QAAQ,aAAa,SAAS,aAAa,IAAI,IAAI,KAAK,IAAI;AAC7E"}
@@ -3,10 +3,33 @@
3
3
  function isRecord(value) {
4
4
  return typeof value === "object" && value !== null && !Array.isArray(value);
5
5
  }
6
+ /** Narrow `unknown` to any object (not null, arrays allowed).
7
+ * Use `isRecord` when you need to access string keys. */
8
+ function isObj(value) {
9
+ return typeof value === "object" && value !== null;
10
+ }
11
+ /** String array (every element is a string). */
12
+ function isStringArray(value) {
13
+ return Array.isArray(value) && value.every((val) => typeof val === "string");
14
+ }
15
+ /** An array of unknowns. Prefer this over a bare `Array.isArray` in typed code:
16
+ * `Array.isArray(x: unknown)` narrows to `any[]`, silently reintroducing
17
+ * `any`, whereas this keeps the element type `unknown`. */
18
+ function isUnknownArray(value) {
19
+ return Array.isArray(value);
20
+ }
21
+ /** Error-like object with a `code` property (e.g. Node.js fs errors). */
22
+ function isErrorWithCode(value) {
23
+ return isRecord(value) && typeof value.code === "string";
24
+ }
6
25
  /** Check that a record has a specific key with a string value. */
7
26
  function hasStringProp(value, key) {
8
27
  return isRecord(value) && typeof value[key] === "string";
9
28
  }
29
+ /** Check that a record has a specific key with a number value. */
30
+ function hasNumberProp(value, key) {
31
+ return isRecord(value) && typeof value[key] === "number";
32
+ }
10
33
  /** Normalise an unknown thrown value into a human-readable string. Isomorphic
11
34
  * (host, bridges, plugins, Vue) — this is the single home for the helper that
12
35
  * #2217 could only consolidate for server code, since `@mulmoclaude/core/utils`
@@ -45,6 +68,6 @@ function escapeHtml(value) {
45
68
  return value.replace(/[&<>"']/g, (char) => HTML_ESCAPES.get(char) ?? char);
46
69
  }
47
70
  //#endregion
48
- export { escapeHtml as n, errorMessage as t };
71
+ export { isErrorWithCode as a, isStringArray as c, hasStringProp as i, isUnknownArray as l, escapeHtml as n, isObj as o, hasNumberProp as r, isRecord as s, errorMessage as t };
49
72
 
50
- //# sourceMappingURL=dist-Cwk0e12G.js.map
73
+ //# sourceMappingURL=dist-D8zokgGo.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"dist-Cwk0e12G.js","names":[],"sources":["../../common/dist/index.js"],"sourcesContent":["// General-purpose runtime type guards, shared across the MulmoClaude host,\n// bridges, and plugins. This is a leaf package — pure and dependency-free — so\n// any tier can import it without creating an uphill edge.\n//\n// These originated as `server/utils/types.ts` (#504), which centralised 40+\n// hand-written inline `typeof x === \"object\"` checks. They are promoted here so\n// the same guards stop being re-hand-written in every bridge and plugin too.\n/** Narrow `unknown` to a plain object (not null, not array). */\nexport function isRecord(value) {\n return typeof value === \"object\" && value !== null && !Array.isArray(value);\n}\n/** Narrow `unknown` to any object (not null, arrays allowed).\n * Use `isRecord` when you need to access string keys. */\nexport function isObj(value) {\n return typeof value === \"object\" && value !== null;\n}\n/** Non-empty string after trimming whitespace. */\nexport function isNonEmptyString(value) {\n return typeof value === \"string\" && value.trim().length > 0;\n}\n/** Record whose values are all strings. */\nexport function isStringRecord(value) {\n if (!isRecord(value))\n return false;\n return Object.values(value).every((val) => typeof val === \"string\");\n}\n/** String array (every element is a string). */\nexport function isStringArray(value) {\n return Array.isArray(value) && value.every((val) => typeof val === \"string\");\n}\n/** An array of unknowns. Prefer this over a bare `Array.isArray` in typed code:\n * `Array.isArray(x: unknown)` narrows to `any[]`, silently reintroducing\n * `any`, whereas this keeps the element type `unknown`. */\nexport function isUnknownArray(value) {\n return Array.isArray(value);\n}\n/** Error-like object with a `code` property (e.g. Node.js fs errors). */\nexport function isErrorWithCode(value) {\n return isRecord(value) && typeof value.code === \"string\";\n}\n/** Check that a record has a specific key with a string value. */\nexport function hasStringProp(value, key) {\n return isRecord(value) && typeof value[key] === \"string\";\n}\n/** Check that a record has a specific key with a number value. */\nexport function hasNumberProp(value, key) {\n return isRecord(value) && typeof value[key] === \"number\";\n}\n/** Split a comma-separated env value into trimmed, non-empty entries.\n * `lowercase` folds case for identifiers compared case-insensitively\n * (JIDs, email addresses, hex pubkeys). Absent/empty input → empty list. */\nexport function parseCsvList(raw, opts) {\n return (raw ?? \"\")\n .split(\",\")\n .map((entry) => (opts?.lowercase ? entry.trim().toLowerCase() : entry.trim()))\n .filter(Boolean);\n}\n/** A comma-separated env value as a Set — the canonical allowlist shape,\n * where an empty set is the \"allow all\" sentinel (`set.size === 0`). */\nexport function parseCsvSet(raw, opts) {\n return new Set(parseCsvList(raw, opts));\n}\n/** Normalise an unknown thrown value into a human-readable string. Isomorphic\n * (host, bridges, plugins, Vue) — this is the single home for the helper that\n * #2217 could only consolidate for server code, since `@mulmoclaude/core/utils`\n * is server-only.\n *\n * A non-Error object with a non-empty string `details` (gRPC convention) or\n * `message` field surfaces that field — `details` wins — instead of the\n * `[object Object]` a bare `String(err)` would print; an empty-string field\n * falls through. `fallback` covers the error-boundary idiom where a thrown\n * non-Error should read as a descriptive message rather than `String(err)`\n * noise; omit it in logging contexts where `String(err)` is fine. */\nexport function errorMessage(err, fallback) {\n if (err instanceof Error)\n return err.message;\n if (hasStringProp(err, \"details\") && err.details)\n return err.details;\n if (hasStringProp(err, \"message\") && err.message)\n return err.message;\n if (fallback !== undefined)\n return fallback;\n return String(err);\n}\n/** `Date` → `YYYY-MM-DD` in UTC — for dates that must not shift with the\n * host's local timezone (tool-trace search dirs, API date keys). Isomorphic\n * single source (#2480): the host re-exports it from `server/utils/date.ts`,\n * x-plugin imports it directly. The `@receptron/task-scheduler` copy stays\n * local on purpose — that leaf package is published independently and kept\n * dependency-free. Wall-clock questions use the host's `toLocalIsoDate`. */\nexport function toUtcIsoDate(timestamp) {\n const year = timestamp.getUTCFullYear();\n const month = String(timestamp.getUTCMonth() + 1).padStart(2, \"0\");\n const day = String(timestamp.getUTCDate()).padStart(2, \"0\");\n return `${year}-${month}-${day}`;\n}\n// A Map, not an object literal: `{}[char]` reads through the prototype chain,\n// so a future caller widening the regex would silently get `[object Object]`\n// for keys like `constructor`.\nconst HTML_ESCAPES = new Map([\n [\"&\", \"&amp;\"],\n [\"<\", \"&lt;\"],\n [\">\", \"&gt;\"],\n ['\"', \"&quot;\"],\n [\"'\", \"&#39;\"],\n]);\n/** HTML-escape text destined for markup or an attribute value — the fixed\n * five-character map, nothing more. Lives here rather than in\n * `@mulmoclaude/core/wiki` (#2483) because `@mulmoclaude/markdown-utils` is a\n * leaf that core depends on and so cannot import back up; core/wiki\n * re-exports this, keeping its consumers' import path unchanged.\n *\n * Escaping `&` first is what makes a single pass safe — the entities this\n * introduces contain none of the other four characters, so nothing is\n * double-escaped. Not a sanitiser: it neither strips tags nor validates URLs. */\nexport function escapeHtml(value) {\n return value.replace(/[&<>\"']/g, (char) => HTML_ESCAPES.get(char) ?? char);\n}\nexport { scanEnvOptions, snakeToLowerCamel } from \"./envScan.js\";\n"],"mappings":";;AAQA,SAAgB,SAAS,OAAO;CAC5B,OAAO,OAAO,UAAU,YAAY,UAAU,QAAQ,CAAC,MAAM,QAAQ,KAAK;AAC9E;;AA+BA,SAAgB,cAAc,OAAO,KAAK;CACtC,OAAO,SAAS,KAAK,KAAK,OAAO,MAAM,SAAS;AACpD;;;;;;;;;;;;AA8BA,SAAgB,aAAa,KAAK,UAAU;CACxC,IAAI,eAAe,OACf,OAAO,IAAI;CACf,IAAI,cAAc,KAAK,SAAS,KAAK,IAAI,SACrC,OAAO,IAAI;CACf,IAAI,cAAc,KAAK,SAAS,KAAK,IAAI,SACrC,OAAO,IAAI;CACf,IAAI,aAAa,KAAA,GACb,OAAO;CACX,OAAO,OAAO,GAAG;AACrB;AAgBA,IAAM,+BAAe,IAAI,IAAI;CACzB,CAAC,KAAK,OAAO;CACb,CAAC,KAAK,MAAM;CACZ,CAAC,KAAK,MAAM;CACZ,CAAC,MAAK,QAAQ;CACd,CAAC,KAAK,OAAO;AACjB,CAAC;;;;;;;;;;AAUD,SAAgB,WAAW,OAAO;CAC9B,OAAO,MAAM,QAAQ,aAAa,SAAS,aAAa,IAAI,IAAI,KAAK,IAAI;AAC7E"}
1
+ {"version":3,"file":"dist-D8zokgGo.js","names":[],"sources":["../../common/dist/index.js"],"sourcesContent":["// General-purpose runtime type guards, shared across the MulmoClaude host,\n// bridges, and plugins. This is a leaf package — pure and dependency-free — so\n// any tier can import it without creating an uphill edge.\n//\n// These originated as `server/utils/types.ts` (#504), which centralised 40+\n// hand-written inline `typeof x === \"object\"` checks. They are promoted here so\n// the same guards stop being re-hand-written in every bridge and plugin too.\n/** Narrow `unknown` to a plain object (not null, not array). */\nexport function isRecord(value) {\n return typeof value === \"object\" && value !== null && !Array.isArray(value);\n}\n/** Narrow `unknown` to any object (not null, arrays allowed).\n * Use `isRecord` when you need to access string keys. */\nexport function isObj(value) {\n return typeof value === \"object\" && value !== null;\n}\n/** Non-empty string after trimming whitespace. */\nexport function isNonEmptyString(value) {\n return typeof value === \"string\" && value.trim().length > 0;\n}\n/** Record whose values are all strings. */\nexport function isStringRecord(value) {\n if (!isRecord(value))\n return false;\n return Object.values(value).every((val) => typeof val === \"string\");\n}\n/** String array (every element is a string). */\nexport function isStringArray(value) {\n return Array.isArray(value) && value.every((val) => typeof val === \"string\");\n}\n/** An array of unknowns. Prefer this over a bare `Array.isArray` in typed code:\n * `Array.isArray(x: unknown)` narrows to `any[]`, silently reintroducing\n * `any`, whereas this keeps the element type `unknown`. */\nexport function isUnknownArray(value) {\n return Array.isArray(value);\n}\n/** Error-like object with a `code` property (e.g. Node.js fs errors). */\nexport function isErrorWithCode(value) {\n return isRecord(value) && typeof value.code === \"string\";\n}\n/** Check that a record has a specific key with a string value. */\nexport function hasStringProp(value, key) {\n return isRecord(value) && typeof value[key] === \"string\";\n}\n/** Check that a record has a specific key with a number value. */\nexport function hasNumberProp(value, key) {\n return isRecord(value) && typeof value[key] === \"number\";\n}\n/** Split a comma-separated env value into trimmed, non-empty entries.\n * `lowercase` folds case for identifiers compared case-insensitively\n * (JIDs, email addresses, hex pubkeys). Absent/empty input → empty list. */\nexport function parseCsvList(raw, opts) {\n return (raw ?? \"\")\n .split(\",\")\n .map((entry) => (opts?.lowercase ? entry.trim().toLowerCase() : entry.trim()))\n .filter(Boolean);\n}\n/** A comma-separated env value as a Set — the canonical allowlist shape,\n * where an empty set is the \"allow all\" sentinel (`set.size === 0`). */\nexport function parseCsvSet(raw, opts) {\n return new Set(parseCsvList(raw, opts));\n}\n/** Normalise an unknown thrown value into a human-readable string. Isomorphic\n * (host, bridges, plugins, Vue) — this is the single home for the helper that\n * #2217 could only consolidate for server code, since `@mulmoclaude/core/utils`\n * is server-only.\n *\n * A non-Error object with a non-empty string `details` (gRPC convention) or\n * `message` field surfaces that field — `details` wins — instead of the\n * `[object Object]` a bare `String(err)` would print; an empty-string field\n * falls through. `fallback` covers the error-boundary idiom where a thrown\n * non-Error should read as a descriptive message rather than `String(err)`\n * noise; omit it in logging contexts where `String(err)` is fine. */\nexport function errorMessage(err, fallback) {\n if (err instanceof Error)\n return err.message;\n if (hasStringProp(err, \"details\") && err.details)\n return err.details;\n if (hasStringProp(err, \"message\") && err.message)\n return err.message;\n if (fallback !== undefined)\n return fallback;\n return String(err);\n}\n/** `Date` → `YYYY-MM-DD` in UTC — for dates that must not shift with the\n * host's local timezone (tool-trace search dirs, API date keys). Isomorphic\n * single source (#2480): the host re-exports it from `server/utils/date.ts`,\n * x-plugin imports it directly. The `@receptron/task-scheduler` copy stays\n * local on purpose — that leaf package is published independently and kept\n * dependency-free. Wall-clock questions use the host's `toLocalIsoDate`. */\nexport function toUtcIsoDate(timestamp) {\n const year = timestamp.getUTCFullYear();\n const month = String(timestamp.getUTCMonth() + 1).padStart(2, \"0\");\n const day = String(timestamp.getUTCDate()).padStart(2, \"0\");\n return `${year}-${month}-${day}`;\n}\n// A Map, not an object literal: `{}[char]` reads through the prototype chain,\n// so a future caller widening the regex would silently get `[object Object]`\n// for keys like `constructor`.\nconst HTML_ESCAPES = new Map([\n [\"&\", \"&amp;\"],\n [\"<\", \"&lt;\"],\n [\">\", \"&gt;\"],\n ['\"', \"&quot;\"],\n [\"'\", \"&#39;\"],\n]);\n/** HTML-escape text destined for markup or an attribute value — the fixed\n * five-character map, nothing more. Lives here rather than in\n * `@mulmoclaude/core/wiki` (#2483) because `@mulmoclaude/markdown-utils` is a\n * leaf that core depends on and so cannot import back up; core/wiki\n * re-exports this, keeping its consumers' import path unchanged.\n *\n * Escaping `&` first is what makes a single pass safe — the entities this\n * introduces contain none of the other four characters, so nothing is\n * double-escaped. Not a sanitiser: it neither strips tags nor validates URLs. */\nexport function escapeHtml(value) {\n return value.replace(/[&<>\"']/g, (char) => HTML_ESCAPES.get(char) ?? char);\n}\nexport { scanEnvOptions, snakeToLowerCamel } from \"./envScan.js\";\n"],"mappings":";;AAQA,SAAgB,SAAS,OAAO;CAC5B,OAAO,OAAO,UAAU,YAAY,UAAU,QAAQ,CAAC,MAAM,QAAQ,KAAK;AAC9E;;;AAGA,SAAgB,MAAM,OAAO;CACzB,OAAO,OAAO,UAAU,YAAY,UAAU;AAClD;;AAYA,SAAgB,cAAc,OAAO;CACjC,OAAO,MAAM,QAAQ,KAAK,KAAK,MAAM,OAAO,QAAQ,OAAO,QAAQ,QAAQ;AAC/E;;;;AAIA,SAAgB,eAAe,OAAO;CAClC,OAAO,MAAM,QAAQ,KAAK;AAC9B;;AAEA,SAAgB,gBAAgB,OAAO;CACnC,OAAO,SAAS,KAAK,KAAK,OAAO,MAAM,SAAS;AACpD;;AAEA,SAAgB,cAAc,OAAO,KAAK;CACtC,OAAO,SAAS,KAAK,KAAK,OAAO,MAAM,SAAS;AACpD;;AAEA,SAAgB,cAAc,OAAO,KAAK;CACtC,OAAO,SAAS,KAAK,KAAK,OAAO,MAAM,SAAS;AACpD;;;;;;;;;;;;AA0BA,SAAgB,aAAa,KAAK,UAAU;CACxC,IAAI,eAAe,OACf,OAAO,IAAI;CACf,IAAI,cAAc,KAAK,SAAS,KAAK,IAAI,SACrC,OAAO,IAAI;CACf,IAAI,cAAc,KAAK,SAAS,KAAK,IAAI,SACrC,OAAO,IAAI;CACf,IAAI,aAAa,KAAA,GACb,OAAO;CACX,OAAO,OAAO,GAAG;AACrB;AAgBA,IAAM,+BAAe,IAAI,IAAI;CACzB,CAAC,KAAK,OAAO;CACb,CAAC,KAAK,MAAM;CACZ,CAAC,KAAK,MAAM;CACZ,CAAC,MAAK,QAAQ;CACd,CAAC,KAAK,OAAO;AACjB,CAAC;;;;;;;;;;AAUD,SAAgB,WAAW,OAAO;CAC9B,OAAO,MAAM,QAAQ,aAAa,SAAS,aAAa,IAAI,IAAI,KAAK,IAAI;AAC7E"}
@@ -1,6 +1,6 @@
1
1
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
- const require_calendarGrid = require("../calendarGrid-CaS9er8i.cjs");
3
- const require_ingestTypes = require("../ingestTypes-D1GQdG8e.cjs");
2
+ const require_calendarGrid = require("../calendarGrid-BQYqOCnF.cjs");
3
+ const require_ingestTypes = require("../ingestTypes-C7IJSqv4.cjs");
4
4
  exports.AGENT_INGEST_KIND = require_ingestTypes.AGENT_INGEST_KIND;
5
5
  exports.DEFAULT_FEED_MAX_ITEMS = require_ingestTypes.DEFAULT_FEED_MAX_ITEMS;
6
6
  exports.FEED_SCHEDULES = require_calendarGrid.FEED_SCHEDULES;
@@ -1,3 +1,3 @@
1
- import { C as FEED_SCHEDULES, w as INGEST_KINDS } from "../calendarGrid-CrGxeCOG.js";
2
- import { n as DEFAULT_FEED_MAX_ITEMS, r as isFeedSchedule, t as AGENT_INGEST_KIND } from "../ingestTypes-XReG-li7.js";
1
+ import { C as FEED_SCHEDULES, w as INGEST_KINDS } from "../calendarGrid-DivcNKkX.js";
2
+ import { n as DEFAULT_FEED_MAX_ITEMS, r as isFeedSchedule, t as AGENT_INGEST_KIND } from "../ingestTypes-DmtDLRmt.js";
3
3
  export { AGENT_INGEST_KIND, DEFAULT_FEED_MAX_ITEMS, FEED_SCHEDULES, INGEST_KINDS, isFeedSchedule };
@@ -1,6 +1,33 @@
1
1
  import { LoadedCollection } from '../../collection/server/index.js';
2
+ import { CollectionItem } from '../../collection/index.js';
3
+ import { IngestSpec } from '../ingestTypes.js';
2
4
  import { RefreshResult } from './refreshResult.js';
3
5
  export type { RefreshResult } from './refreshResult.js';
6
+ /** Fields the feed itself produces: what it actually returned this run, the
7
+ * declared map's targets, and the primary key the retriever derives. Anything
8
+ * else on a record got there some other way — the user.
9
+ *
10
+ * Both sources, because neither alone is enough. The in-tree retrievers project
11
+ * through `map`, so it describes them exactly — but `registerRetriever` lets a
12
+ * host add one that returns whatever it likes, and only the observed keys catch
13
+ * that. Conversely a run that fetched nothing has no keys to observe, and only
14
+ * `map` keeps the cap working through it. */
15
+ export declare const ingestedFields: (items: readonly CollectionItem[], mappedTargets: readonly string[], primaryKey: string) => ReadonlySet<string>;
16
+ /** The collection fields a declarative `map` names. Read as `unknown` and
17
+ * narrowed here rather than through the zod-derived union, whose `map` does not
18
+ * survive the `kind` narrowing well enough to index safely. Only the keys are
19
+ * wanted, so the value type never has to be reconstructed. */
20
+ export declare function mappedTargetsOf(ingest: IngestSpec | undefined): string[];
21
+ /** Whether a record carries content the feed did not put there.
22
+ *
23
+ * Only a NON-EMPTY value counts. A record saved through the UI can carry every
24
+ * declared field as an empty string, and treating those as local content would
25
+ * make every touched record un-prunable — the cap would quietly stop working.
26
+ *
27
+ * An edit to a MAPPED field deliberately does not count: a feed is
28
+ * one-directional, so the next refresh writes over it by design. The local
29
+ * COLUMN is the durable half, and the half worth protecting. */
30
+ export declare function hasLocalContent(item: CollectionItem, ingested: ReadonlySet<string>): boolean;
4
31
  /** Fetch one feed now, upsert its records, then enforce the maxItems cap.
5
32
  * Failure-isolated: returns an errors array rather than throwing. */
6
33
  export declare function refreshOne(workspaceRoot: string, feed: LoadedCollection, opts?: {
@@ -1,11 +1,13 @@
1
1
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
2
  const require_rolldown_runtime = require("../../rolldown-runtime-D6vf50IK.cjs");
3
- const require_calendarGrid = require("../../calendarGrid-CaS9er8i.cjs");
4
- const require_discovery = require("../../discovery-NRy3tyUA.cjs");
5
- const require_server = require("../../server-7U-3DE2e.cjs");
3
+ const require_dist = require("../../dist-CsgSfWwR.cjs");
4
+ const require_calendarGrid = require("../../calendarGrid-BQYqOCnF.cjs");
5
+ const require_project = require("../../project-C1ep9pvo.cjs");
6
+ const require_discovery = require("../../discovery-CRDx6TS4.cjs");
7
+ const require_server = require("../../server-D7_oRbTJ.cjs");
6
8
  const require_feeds_paths = require("../paths.cjs");
7
- const require_ingestTypes = require("../../ingestTypes-D1GQdG8e.cjs");
8
- const require_notifier = require("../../notifier-tMsAXyXp.cjs");
9
+ const require_ingestTypes = require("../../ingestTypes-C7IJSqv4.cjs");
10
+ const require_notifier = require("../../notifier-Wc4M24Bi.cjs");
9
11
  let node_path = require("node:path");
10
12
  node_path = require_rolldown_runtime.__toESM(node_path, 1);
11
13
  let node_crypto = require("node:crypto");
@@ -388,11 +390,10 @@ function tokenize(path) {
388
390
  function step(current, token) {
389
391
  if (current === null || current === void 0) return void 0;
390
392
  if (token.kind === "key") {
391
- if (typeof current !== "object" || Array.isArray(current)) return void 0;
392
- const record = current;
393
- return Object.hasOwn(record, token.key) ? record[token.key] : void 0;
393
+ if (!require_dist.isRecord(current)) return void 0;
394
+ return Object.hasOwn(current, token.key) ? current[token.key] : void 0;
394
395
  }
395
- return Array.isArray(current) ? current[token.index] : void 0;
396
+ return require_dist.isUnknownArray(current) ? current[token.index] : void 0;
396
397
  }
397
398
  /** Resolve a dot/bracket path against `root`, or `undefined` on any miss. */
398
399
  function getByPath(root, path) {
@@ -421,10 +422,9 @@ function asKeyString(value) {
421
422
  * shape — so a tag like `<guid isPermaLink="false">id</guid>` maps to
422
423
  * its text without the caller needing to write `.#text`. */
423
424
  function unwrapTextNode(value) {
424
- if (value && typeof value === "object" && !Array.isArray(value)) {
425
- const obj = value;
426
- if (typeof obj["#text"] === "string") return obj["#text"];
427
- if (typeof obj["#cdata"] === "string") return obj["#cdata"];
425
+ if (require_dist.isRecord(value)) {
426
+ if (typeof value["#text"] === "string") return value["#text"];
427
+ if (typeof value["#cdata"] === "string") return value["#cdata"];
428
428
  }
429
429
  return value;
430
430
  }
@@ -511,9 +511,16 @@ function defaultFeedState(slug) {
511
511
  consecutiveFailures: 0
512
512
  };
513
513
  }
514
+ /** The stored cursor, keeping only the string entries its type promises. A
515
+ * hand-edited state file can hold anything; a non-string entry is unreadable
516
+ * to every retriever, so carrying it forward would only re-persist junk. */
517
+ function readCursor(value) {
518
+ if (!require_dist.isRecord(value)) return {};
519
+ return Object.fromEntries(Object.entries(value).filter((entry) => typeof entry[1] === "string"));
520
+ }
514
521
  function normalizeState(slug, parsed) {
515
522
  const base = defaultFeedState(slug);
516
- const cursor = parsed.cursor && typeof parsed.cursor === "object" ? parsed.cursor : base.cursor;
523
+ const cursor = parsed.cursor === void 0 ? base.cursor : readCursor(parsed.cursor);
517
524
  return {
518
525
  slug,
519
526
  lastFetchedAt: typeof parsed.lastFetchedAt === "string" ? parsed.lastFetchedAt : base.lastFetchedAt,
@@ -528,9 +535,10 @@ async function readFeedState(workspaceRoot, target) {
528
535
  const { slug } = target;
529
536
  try {
530
537
  const raw = await (0, node_fs_promises.readFile)(stateFilePath(target, workspaceRoot), "utf-8");
531
- return normalizeState(slug, JSON.parse(raw));
538
+ const parsed = JSON.parse(raw);
539
+ return normalizeState(slug, require_dist.isRecord(parsed) ? parsed : {});
532
540
  } catch (err) {
533
- if (err.code !== "ENOENT") log.warn("feeds", "failed to read feed state, using default", {
541
+ if (!require_dist.isErrorWithCode(err) || err.code !== "ENOENT") log.warn("feeds", "failed to read feed state, using default", {
534
542
  slug,
535
543
  error: String(err)
536
544
  });
@@ -575,8 +583,8 @@ function result(slug, patch) {
575
583
  async function refreshViaAgent(workspaceRoot, collection, opts) {
576
584
  const hidden = opts?.hidden ?? true;
577
585
  const { slug } = collection;
578
- const ingest = collection.schema.ingest;
579
- if (!ingest || ingest.kind !== "agent") return result(slug, { errors: ["collection has no agent ingest config"] });
586
+ const { ingest } = collection.schema;
587
+ if (ingest?.kind !== "agent") return result(slug, { errors: ["collection has no agent ingest config"] });
580
588
  const workerRunner = workerRunnerOrNull();
581
589
  if (!workerRunner) return result(slug, { errors: ["agent ingest worker runner not configured"] });
582
590
  const template = await require_server.readSkillTemplate(collection.skillDir, ingest.template);
@@ -680,10 +688,9 @@ async function onWorkerSuccess(state, collection) {
680
688
  //#region src/feeds/server/engine.ts
681
689
  var ONE_HOUR_MS = 36e5;
682
690
  var ONE_DAY_MS = 864e5;
683
- /** Feed schemas carry the rich `IngestSpec` (validated at discovery —
684
- * `source === "feed"` requires `ingest`), but the canonical
685
- * `CollectionSchema.ingest` only promises the minimal `CollectionIngest`.
686
- * Narrow here so the engine can read the retrieval fields type-safely. */
691
+ /** A feed's retrieval config, or undefined. `CollectionSchema.ingest` and the
692
+ * engine's `IngestSpec` are both `IngestZ`'s inferred union, so this is a
693
+ * rename, not a narrowing — the engine keeps its own vocabulary at the seam. */
687
694
  function feedIngest(schema) {
688
695
  return schema.ingest;
689
696
  }
@@ -702,6 +709,60 @@ function writableFeedStore(workspaceRoot, feed) {
702
709
  delete: remove
703
710
  };
704
711
  }
712
+ /** Fields the feed itself produces: what it actually returned this run, the
713
+ * declared map's targets, and the primary key the retriever derives. Anything
714
+ * else on a record got there some other way — the user.
715
+ *
716
+ * Both sources, because neither alone is enough. The in-tree retrievers project
717
+ * through `map`, so it describes them exactly — but `registerRetriever` lets a
718
+ * host add one that returns whatever it likes, and only the observed keys catch
719
+ * that. Conversely a run that fetched nothing has no keys to observe, and only
720
+ * `map` keeps the cap working through it. */
721
+ var ingestedFields = (items, mappedTargets, primaryKey) => /* @__PURE__ */ new Set([
722
+ primaryKey,
723
+ ...mappedTargets,
724
+ ...items.flatMap((item) => Object.keys(item))
725
+ ]);
726
+ /** The collection fields a declarative `map` names. Read as `unknown` and
727
+ * narrowed here rather than through the zod-derived union, whose `map` does not
728
+ * survive the `kind` narrowing well enough to index safely. Only the keys are
729
+ * wanted, so the value type never has to be reconstructed. */
730
+ function mappedTargetsOf(ingest) {
731
+ const map = ingest === void 0 || ingest.kind === "agent" ? void 0 : ingest.map;
732
+ return typeof map === "object" && map !== null ? Object.keys(map) : [];
733
+ }
734
+ /** Whether a record carries content the feed did not put there.
735
+ *
736
+ * Only a NON-EMPTY value counts. A record saved through the UI can carry every
737
+ * declared field as an empty string, and treating those as local content would
738
+ * make every touched record un-prunable — the cap would quietly stop working.
739
+ *
740
+ * An edit to a MAPPED field deliberately does not count: a feed is
741
+ * one-directional, so the next refresh writes over it by design. The local
742
+ * COLUMN is the durable half, and the half worth protecting. */
743
+ function hasLocalContent(item, ingested) {
744
+ return Object.entries(item).some(([field, value]) => !ingested.has(field) && value !== void 0 && value !== null && value !== "");
745
+ }
746
+ /** The stored record, or null when it cannot be read.
747
+ *
748
+ * `readItem` answers null for a MISSING record but THROWS on unparsable JSON
749
+ * or an IO error. Letting that escape would fail the whole refresh over one bad
750
+ * file — and write nothing at all, since the loop aborts partway. Before the
751
+ * merge landed, the ingest simply overwrote such a file and the feed healed
752
+ * itself; treating "unreadable" as "absent" keeps that. (Observed during Claude
753
+ * review; both review bots were rate-limited on this PR.) */
754
+ async function readableRecord(store, feed, itemId) {
755
+ try {
756
+ return await store.read(itemId);
757
+ } catch (error) {
758
+ log.warn("feeds", "unreadable feed record — the retrieved item will replace it", {
759
+ slug: feed.slug,
760
+ itemId,
761
+ error: String(error)
762
+ });
763
+ return null;
764
+ }
765
+ }
705
766
  async function upsertItems(workspaceRoot, feed, items) {
706
767
  const store = writableFeedStore(workspaceRoot, feed);
707
768
  if (!store) return 0;
@@ -709,7 +770,7 @@ async function upsertItems(workspaceRoot, feed, items) {
709
770
  for (const item of items) {
710
771
  const itemId = item[feed.schema.primaryKey];
711
772
  if (typeof itemId !== "string" || itemId.length === 0) continue;
712
- const result = await store.write(itemId, item);
773
+ const result = await store.write(itemId, require_project.mergeIntoExisting(await readableRecord(store, feed, itemId), item));
713
774
  if (result.kind === "ok") written += 1;
714
775
  else log.warn("feeds", "feed item write skipped", {
715
776
  slug: feed.slug,
@@ -733,7 +794,7 @@ function recordTime(item, field) {
733
794
  /** Enforce `ingest.maxItems` (default 100): keep the newest N records by
734
795
  * the schema's date field, delete the rest. No-op when the cap is 0/absent
735
796
  * of a date field, or when under the cap. Returns the number deleted. */
736
- async function pruneFeed(workspaceRoot, feed) {
797
+ async function pruneFeed(workspaceRoot, feed, fetched) {
737
798
  const ingest = feedIngest(feed.schema);
738
799
  const cap = (ingest && ingest.kind !== "agent" ? ingest.maxItems : void 0) ?? 100;
739
800
  if (cap <= 0) return 0;
@@ -746,7 +807,15 @@ async function pruneFeed(workspaceRoot, feed) {
746
807
  if (!store) return 0;
747
808
  const items = await store.list();
748
809
  if (items.length <= cap) return 0;
749
- const stale = [...items].sort((left, right) => recordTime(right, dateField) - recordTime(left, dateField)).slice(cap);
810
+ const ingested = ingestedFields(fetched, mappedTargetsOf(ingest), feed.schema.primaryKey);
811
+ const aged = [...items].sort((left, right) => recordTime(right, dateField) - recordTime(left, dateField)).slice(cap);
812
+ const stale = aged.filter((item) => !hasLocalContent(item, ingested));
813
+ const kept = aged.length - stale.length;
814
+ if (kept > 0) log.info("feeds", "keeping aged-out records that carry local content", {
815
+ slug: feed.slug,
816
+ kept,
817
+ cap
818
+ });
750
819
  let removed = 0;
751
820
  for (const item of stale) {
752
821
  const itemId = item[feed.schema.primaryKey];
@@ -789,7 +858,7 @@ async function refreshOne(workspaceRoot, feed, opts) {
789
858
  cursor: result.cursor,
790
859
  consecutiveFailures: 0
791
860
  });
792
- const removed = await pruneFeed(workspaceRoot, feed);
861
+ const removed = await pruneFeed(workspaceRoot, feed, result.items);
793
862
  log.info("feeds", "feed refreshed", {
794
863
  slug,
795
864
  written,
@@ -919,8 +988,10 @@ exports.feedDir = require_feeds_paths.feedDir;
919
988
  exports.feedRefreshTaskDef = feedRefreshTaskDef;
920
989
  exports.feedStatePath = require_feeds_paths.feedStatePath;
921
990
  exports.feedsRoot = require_feeds_paths.feedsRoot;
991
+ exports.hasLocalContent = hasLocalContent;
922
992
  exports.ingestStateDir = require_feeds_paths.ingestStateDir;
923
993
  exports.ingestStatePath = require_feeds_paths.ingestStatePath;
994
+ exports.ingestedFields = ingestedFields;
924
995
  exports.isFeedSchedule = require_ingestTypes.isFeedSchedule;
925
996
  exports.listFeeds = listFeeds;
926
997
  exports.readFeedState = readFeedState;