@mulmoclaude/core 1.0.1 → 1.2.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 (177) hide show
  1. package/assets/helps/collection-skills.md +28 -0
  2. package/dist/artifacts/paths.cjs +68 -0
  3. package/dist/artifacts/paths.cjs.map +1 -0
  4. package/dist/artifacts/paths.d.ts +45 -0
  5. package/dist/artifacts/paths.js +62 -0
  6. package/dist/artifacts/paths.js.map +1 -0
  7. package/dist/atomic-C_7YpMiM.cjs +102 -0
  8. package/dist/atomic-C_7YpMiM.cjs.map +1 -0
  9. package/dist/atomic-DPpdrJzO.js +89 -0
  10. package/dist/atomic-DPpdrJzO.js.map +1 -0
  11. package/dist/collection/core/chatSeed.d.ts +4 -0
  12. package/dist/collection/core/completion.d.ts +53 -0
  13. package/dist/collection/core/ids.d.ts +10 -0
  14. package/dist/collection/core/linkTargets.d.ts +14 -0
  15. package/dist/collection/core/ownProp.d.ts +9 -0
  16. package/dist/collection/core/recordKeys.d.ts +21 -0
  17. package/dist/collection/core/schemaRules.d.ts +167 -0
  18. package/dist/collection/core/schemaZ.d.ts +489 -9
  19. package/dist/collection/core/sortValueOf.d.ts +21 -0
  20. package/dist/collection/core/textSearch.d.ts +12 -0
  21. package/dist/collection/index.cjs +132 -29
  22. package/dist/collection/index.cjs.map +1 -1
  23. package/dist/collection/index.d.ts +5 -0
  24. package/dist/collection/index.js +115 -29
  25. package/dist/collection/index.js.map +1 -1
  26. package/dist/collection/registry/server/index.cjs +14 -42
  27. package/dist/collection/registry/server/index.cjs.map +1 -1
  28. package/dist/collection/registry/server/index.js +6 -34
  29. package/dist/collection/registry/server/index.js.map +1 -1
  30. package/dist/collection/server/discovery.d.ts +7 -0
  31. package/dist/collection/server/host.d.ts +3 -8
  32. package/dist/collection/server/index.cjs +10 -9
  33. package/dist/collection/server/index.d.ts +1 -0
  34. package/dist/collection/server/index.js +4 -4
  35. package/dist/collection/server/io.d.ts +0 -88
  36. package/dist/collection/server/skillAssets.d.ts +90 -0
  37. package/dist/collection-watchers/config.d.ts +2 -4
  38. package/dist/collection-watchers/index.cjs +5 -5
  39. package/dist/collection-watchers/index.cjs.map +1 -1
  40. package/dist/collection-watchers/index.js +5 -5
  41. package/dist/collection-watchers/index.js.map +1 -1
  42. package/dist/{discovery-Dpz6SWwe.cjs → discovery--kRUWed0.cjs} +444 -434
  43. package/dist/discovery--kRUWed0.cjs.map +1 -0
  44. package/dist/{discovery-BYIsLqag.js → discovery-CKR4TOX3.js} +426 -392
  45. package/dist/discovery-CKR4TOX3.js.map +1 -0
  46. package/dist/dist-Cwk0e12G.js +50 -0
  47. package/dist/dist-Cwk0e12G.js.map +1 -0
  48. package/dist/dist-pWpC-b04.cjs +61 -0
  49. package/dist/dist-pWpC-b04.cjs.map +1 -0
  50. package/dist/feeds/index.cjs +2 -2
  51. package/dist/feeds/index.js +2 -2
  52. package/dist/feeds/server/host.d.ts +5 -8
  53. package/dist/feeds/server/index.cjs +143 -61
  54. package/dist/feeds/server/index.cjs.map +1 -1
  55. package/dist/feeds/server/index.js +141 -59
  56. package/dist/feeds/server/index.js.map +1 -1
  57. package/dist/files/atomic.d.ts +34 -0
  58. package/dist/files/index.cjs +47 -0
  59. package/dist/files/index.cjs.map +1 -0
  60. package/dist/files/index.d.ts +3 -0
  61. package/dist/files/index.js +40 -0
  62. package/dist/files/index.js.map +1 -0
  63. package/dist/files/json.d.ts +4 -0
  64. package/dist/files/safe.d.ts +9 -0
  65. package/dist/google/auth.d.ts +1 -1
  66. package/dist/google/collectionDateTime.d.ts +5 -0
  67. package/dist/google/collectionSync.d.ts +2 -2
  68. package/dist/google/fsJson.d.ts +3 -2
  69. package/dist/google/host.d.ts +3 -6
  70. package/dist/google/index.cjs +64 -91
  71. package/dist/google/index.cjs.map +1 -1
  72. package/dist/google/index.d.ts +1 -0
  73. package/dist/google/index.js +57 -85
  74. package/dist/google/index.js.map +1 -1
  75. package/dist/{graph-o5FZZbXf.js → graph-BcwqiSpR.js} +2 -14
  76. package/dist/graph-BcwqiSpR.js.map +1 -0
  77. package/dist/{graph-2R4HbQAg.cjs → graph-DjE1Az1C.cjs} +2 -14
  78. package/dist/graph-DjE1Az1C.cjs.map +1 -0
  79. package/dist/host/hostSlot.d.ts +27 -0
  80. package/dist/{ids-DWmHjm17.cjs → ids-BnTh3Hl0.cjs} +29 -1
  81. package/dist/{ids-DWmHjm17.cjs.map → ids-BnTh3Hl0.cjs.map} +1 -1
  82. package/dist/{ids-D1M1T6KJ.js → ids-s4GfmE93.js} +18 -2
  83. package/dist/{ids-D1M1T6KJ.js.map → ids-s4GfmE93.js.map} +1 -1
  84. package/dist/{ingestTypes-Ci4eOgv-.js → ingestTypes-L59cIGgX.js} +2 -2
  85. package/dist/{ingestTypes-Ci4eOgv-.js.map → ingestTypes-L59cIGgX.js.map} +1 -1
  86. package/dist/{ingestTypes-DbuQNuK6.cjs → ingestTypes-xX8VpQqh.cjs} +2 -2
  87. package/dist/{ingestTypes-DbuQNuK6.cjs.map → ingestTypes-xX8VpQqh.cjs.map} +1 -1
  88. package/dist/notifier/engine.d.ts +2 -4
  89. package/dist/notifier/index.cjs +1 -1
  90. package/dist/notifier/index.js +1 -1
  91. package/dist/{notifier-ChpY0XrY.js → notifier-BdA5qzhe.js} +14 -16
  92. package/dist/notifier-BdA5qzhe.js.map +1 -0
  93. package/dist/{notifier-bS8IEeLA.cjs → notifier-tMsAXyXp.cjs} +14 -16
  94. package/dist/notifier-tMsAXyXp.cjs.map +1 -0
  95. package/dist/plugin-vue/fileWatch.d.ts +5 -0
  96. package/dist/plugin-vue/i18n.cjs +45 -0
  97. package/dist/plugin-vue/i18n.cjs.map +1 -0
  98. package/dist/plugin-vue/i18n.js +44 -0
  99. package/dist/plugin-vue/i18n.js.map +1 -0
  100. package/dist/plugin-vue/index.cjs +98 -0
  101. package/dist/plugin-vue/index.cjs.map +1 -0
  102. package/dist/plugin-vue/index.d.ts +5 -0
  103. package/dist/plugin-vue/index.js +91 -0
  104. package/dist/plugin-vue/index.js.map +1 -0
  105. package/dist/plugin-vue/markdownDoc.d.ts +10 -0
  106. package/dist/plugin-vue/pluginI18n.d.ts +22 -0
  107. package/dist/plugin-vue/useClipboardCopy.d.ts +6 -0
  108. package/dist/plugin-vue/useFileWatch.d.ts +8 -0
  109. package/dist/plugin-vue/useMarkdownDoc.d.ts +3 -0
  110. package/dist/{promptSafety-Bugq2kqL.js → promptSafety-CQ5Un4wi.js} +104 -7
  111. package/dist/promptSafety-CQ5Un4wi.js.map +1 -0
  112. package/dist/{promptSafety-DbE6eZmP.cjs → promptSafety-DfZYNtjK.cjs} +151 -6
  113. package/dist/promptSafety-DfZYNtjK.cjs.map +1 -0
  114. package/dist/remote-host/server/hostRunner.d.ts +5 -1
  115. package/dist/remote-host/server/index.cjs +75 -21
  116. package/dist/remote-host/server/index.cjs.map +1 -1
  117. package/dist/remote-host/server/index.js +72 -18
  118. package/dist/remote-host/server/index.js.map +1 -1
  119. package/dist/scheduler/index.cjs +3 -3
  120. package/dist/scheduler/index.cjs.map +1 -1
  121. package/dist/scheduler/index.js +2 -2
  122. package/dist/scheduler/index.js.map +1 -1
  123. package/dist/scheduler/task-manager.d.ts +2 -5
  124. package/dist/{server-CtCBLYgZ.js → server-BZJCw2HU.js} +209 -41
  125. package/dist/server-BZJCw2HU.js.map +1 -0
  126. package/dist/{server-BjWrcc5T.cjs → server-w5ZZRLju.cjs} +250 -46
  127. package/dist/server-w5ZZRLju.cjs.map +1 -0
  128. package/dist/skill-bridge/index.cjs +1 -1
  129. package/dist/skill-bridge/index.js +1 -1
  130. package/dist/translation/client.cjs +16 -0
  131. package/dist/translation/client.cjs.map +1 -1
  132. package/dist/translation/client.d.ts +6 -0
  133. package/dist/translation/client.js +16 -1
  134. package/dist/translation/client.js.map +1 -1
  135. package/dist/utils/errors.d.ts +2 -1
  136. package/dist/utils/fetch.cjs +36 -0
  137. package/dist/utils/fetch.cjs.map +1 -0
  138. package/dist/{collection/registry/server → utils}/fetch.d.ts +5 -3
  139. package/dist/utils/fetch.js +34 -0
  140. package/dist/utils/fetch.js.map +1 -0
  141. package/dist/utils/index.cjs +11 -3
  142. package/dist/utils/index.cjs.map +1 -0
  143. package/dist/utils/index.js +9 -1
  144. package/dist/utils/index.js.map +1 -0
  145. package/dist/whisper/index.cjs +9 -9
  146. package/dist/whisper/index.cjs.map +1 -1
  147. package/dist/whisper/index.js +3 -3
  148. package/dist/wiki/index.cjs +23 -28
  149. package/dist/wiki/index.cjs.map +1 -1
  150. package/dist/wiki/index.js +21 -26
  151. package/dist/wiki/index.js.map +1 -1
  152. package/dist/wiki/link.d.ts +7 -1
  153. package/dist/wiki/render.d.ts +5 -7
  154. package/dist/wiki/server/frontmatter.d.ts +2 -6
  155. package/dist/wiki/server/index.cjs +5 -41
  156. package/dist/wiki/server/index.cjs.map +1 -1
  157. package/dist/wiki/server/index.js +3 -39
  158. package/dist/wiki/server/index.js.map +1 -1
  159. package/dist/workspace-setup/index.js +22 -6
  160. package/dist/workspace-setup/index.js.map +1 -1
  161. package/package.json +54 -11
  162. package/dist/collection/server/atomic.d.ts +0 -1
  163. package/dist/discovery-BYIsLqag.js.map +0 -1
  164. package/dist/discovery-Dpz6SWwe.cjs.map +0 -1
  165. package/dist/errors-7P5eMOSX.cjs +0 -30
  166. package/dist/errors-7P5eMOSX.cjs.map +0 -1
  167. package/dist/errors-eid6Mes3.js +0 -19
  168. package/dist/errors-eid6Mes3.js.map +0 -1
  169. package/dist/google/fetch.d.ts +0 -8
  170. package/dist/graph-2R4HbQAg.cjs.map +0 -1
  171. package/dist/graph-o5FZZbXf.js.map +0 -1
  172. package/dist/notifier-ChpY0XrY.js.map +0 -1
  173. package/dist/notifier-bS8IEeLA.cjs.map +0 -1
  174. package/dist/promptSafety-Bugq2kqL.js.map +0 -1
  175. package/dist/promptSafety-DbE6eZmP.cjs.map +0 -1
  176. package/dist/server-BjWrcc5T.cjs.map +0 -1
  177. package/dist/server-CtCBLYgZ.js.map +0 -1
@@ -0,0 +1,50 @@
1
+ //#region ../common/dist/index.js
2
+ /** Narrow `unknown` to a plain object (not null, not array). */
3
+ function isRecord(value) {
4
+ return typeof value === "object" && value !== null && !Array.isArray(value);
5
+ }
6
+ /** Check that a record has a specific key with a string value. */
7
+ function hasStringProp(value, key) {
8
+ return isRecord(value) && typeof value[key] === "string";
9
+ }
10
+ /** Normalise an unknown thrown value into a human-readable string. Isomorphic
11
+ * (host, bridges, plugins, Vue) — this is the single home for the helper that
12
+ * #2217 could only consolidate for server code, since `@mulmoclaude/core/utils`
13
+ * is server-only.
14
+ *
15
+ * A non-Error object with a non-empty string `details` (gRPC convention) or
16
+ * `message` field surfaces that field — `details` wins — instead of the
17
+ * `[object Object]` a bare `String(err)` would print; an empty-string field
18
+ * falls through. `fallback` covers the error-boundary idiom where a thrown
19
+ * non-Error should read as a descriptive message rather than `String(err)`
20
+ * noise; omit it in logging contexts where `String(err)` is fine. */
21
+ function errorMessage(err, fallback) {
22
+ if (err instanceof Error) return err.message;
23
+ if (hasStringProp(err, "details") && err.details) return err.details;
24
+ if (hasStringProp(err, "message") && err.message) return err.message;
25
+ if (fallback !== void 0) return fallback;
26
+ return String(err);
27
+ }
28
+ var HTML_ESCAPES = /* @__PURE__ */ new Map([
29
+ ["&", "&"],
30
+ ["<", "&lt;"],
31
+ [">", "&gt;"],
32
+ ["\"", "&quot;"],
33
+ ["'", "&#39;"]
34
+ ]);
35
+ /** HTML-escape text destined for markup or an attribute value — the fixed
36
+ * five-character map, nothing more. Lives here rather than in
37
+ * `@mulmoclaude/core/wiki` (#2483) because `@mulmoclaude/markdown-utils` is a
38
+ * leaf that core depends on and so cannot import back up; core/wiki
39
+ * re-exports this, keeping its consumers' import path unchanged.
40
+ *
41
+ * Escaping `&` first is what makes a single pass safe — the entities this
42
+ * introduces contain none of the other four characters, so nothing is
43
+ * double-escaped. Not a sanitiser: it neither strips tags nor validates URLs. */
44
+ function escapeHtml(value) {
45
+ return value.replace(/[&<>"']/g, (char) => HTML_ESCAPES.get(char) ?? char);
46
+ }
47
+ //#endregion
48
+ export { escapeHtml as n, errorMessage as t };
49
+
50
+ //# sourceMappingURL=dist-Cwk0e12G.js.map
@@ -0,0 +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"}
@@ -0,0 +1,61 @@
1
+ //#region ../common/dist/index.js
2
+ /** Narrow `unknown` to a plain object (not null, not array). */
3
+ function isRecord(value) {
4
+ return typeof value === "object" && value !== null && !Array.isArray(value);
5
+ }
6
+ /** Check that a record has a specific key with a string value. */
7
+ function hasStringProp(value, key) {
8
+ return isRecord(value) && typeof value[key] === "string";
9
+ }
10
+ /** Normalise an unknown thrown value into a human-readable string. Isomorphic
11
+ * (host, bridges, plugins, Vue) — this is the single home for the helper that
12
+ * #2217 could only consolidate for server code, since `@mulmoclaude/core/utils`
13
+ * is server-only.
14
+ *
15
+ * A non-Error object with a non-empty string `details` (gRPC convention) or
16
+ * `message` field surfaces that field — `details` wins — instead of the
17
+ * `[object Object]` a bare `String(err)` would print; an empty-string field
18
+ * falls through. `fallback` covers the error-boundary idiom where a thrown
19
+ * non-Error should read as a descriptive message rather than `String(err)`
20
+ * noise; omit it in logging contexts where `String(err)` is fine. */
21
+ function errorMessage(err, fallback) {
22
+ if (err instanceof Error) return err.message;
23
+ if (hasStringProp(err, "details") && err.details) return err.details;
24
+ if (hasStringProp(err, "message") && err.message) return err.message;
25
+ if (fallback !== void 0) return fallback;
26
+ return String(err);
27
+ }
28
+ var HTML_ESCAPES = /* @__PURE__ */ new Map([
29
+ ["&", "&amp;"],
30
+ ["<", "&lt;"],
31
+ [">", "&gt;"],
32
+ ["\"", "&quot;"],
33
+ ["'", "&#39;"]
34
+ ]);
35
+ /** HTML-escape text destined for markup or an attribute value — the fixed
36
+ * five-character map, nothing more. Lives here rather than in
37
+ * `@mulmoclaude/core/wiki` (#2483) because `@mulmoclaude/markdown-utils` is a
38
+ * leaf that core depends on and so cannot import back up; core/wiki
39
+ * re-exports this, keeping its consumers' import path unchanged.
40
+ *
41
+ * Escaping `&` first is what makes a single pass safe — the entities this
42
+ * introduces contain none of the other four characters, so nothing is
43
+ * double-escaped. Not a sanitiser: it neither strips tags nor validates URLs. */
44
+ function escapeHtml(value) {
45
+ return value.replace(/[&<>"']/g, (char) => HTML_ESCAPES.get(char) ?? char);
46
+ }
47
+ //#endregion
48
+ Object.defineProperty(exports, "errorMessage", {
49
+ enumerable: true,
50
+ get: function() {
51
+ return errorMessage;
52
+ }
53
+ });
54
+ Object.defineProperty(exports, "escapeHtml", {
55
+ enumerable: true,
56
+ get: function() {
57
+ return escapeHtml;
58
+ }
59
+ });
60
+
61
+ //# sourceMappingURL=dist-pWpC-b04.cjs.map
@@ -0,0 +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,6 +1,6 @@
1
1
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
- const require_ids = require("../ids-DWmHjm17.cjs");
3
- const require_ingestTypes = require("../ingestTypes-DbuQNuK6.cjs");
2
+ const require_ids = require("../ids-BnTh3Hl0.cjs");
3
+ const require_ingestTypes = require("../ingestTypes-xX8VpQqh.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_ids.FEED_SCHEDULES;
@@ -1,3 +1,3 @@
1
- import { c as INGEST_KINDS, s as FEED_SCHEDULES } from "../ids-D1M1T6KJ.js";
2
- import { n as DEFAULT_FEED_MAX_ITEMS, r as isFeedSchedule, t as AGENT_INGEST_KIND } from "../ingestTypes-Ci4eOgv-.js";
1
+ import { l as FEED_SCHEDULES, u as INGEST_KINDS } from "../ids-s4GfmE93.js";
2
+ import { n as DEFAULT_FEED_MAX_ITEMS, r as isFeedSchedule, t as AGENT_INGEST_KIND } from "../ingestTypes-L59cIGgX.js";
3
3
  export { AGENT_INGEST_KIND, DEFAULT_FEED_MAX_ITEMS, FEED_SCHEDULES, INGEST_KINDS, isFeedSchedule };
@@ -1,3 +1,4 @@
1
+ import { StructuredLogger } from '../../host/hostSlot.js';
1
2
  /** Outcome of launching one hidden/visible agent-ingest worker. `chatId` lets
2
3
  * the caller register a completion hook so a failed refresh doesn't die
3
4
  * silently. */
@@ -23,13 +24,8 @@ export type AgentWorkerRunner = (args: {
23
24
  didError: boolean;
24
25
  }) => void | Promise<void>;
25
26
  }) => Promise<AgentWorkerResult>;
26
- /** Structured logger, `(prefix, msg, data?)` — same shape as `CollectionLogger`. */
27
- export interface FeedsLogger {
28
- error: (prefix: string, message: string, data?: Record<string, unknown>) => void;
29
- warn: (prefix: string, message: string, data?: Record<string, unknown>) => void;
30
- info: (prefix: string, message: string, data?: Record<string, unknown>) => void;
31
- debug: (prefix: string, message: string, data?: Record<string, unknown>) => void;
32
- }
27
+ /** Public alias of the shared `StructuredLogger` — same shape as `CollectionLogger`. */
28
+ export type FeedsLogger = StructuredLogger;
33
29
  export interface FeedsHost {
34
30
  /** Absolute workspace root — the default for `refreshDue()` and state paths. */
35
31
  workspaceRoot: string;
@@ -47,5 +43,6 @@ export declare function requireFeedsHost(): FeedsHost;
47
43
  /** Test-only: clear the configured host. */
48
44
  export declare function resetFeedsHostForTesting(): void;
49
45
  /** Forwarding logger so engine modules can `import { log }` without each
50
- * reaching for `requireFeedsHost().log`. */
46
+ * reaching for `requireFeedsHost().log`. Non-critical: calls before the host
47
+ * is wired are dropped (not thrown), matching the collection/google seams. */
51
48
  export declare const log: FeedsLogger;
@@ -1,43 +1,37 @@
1
1
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
2
  const require_rolldown_runtime = require("../../rolldown-runtime-D6vf50IK.cjs");
3
- const require_ids = require("../../ids-DWmHjm17.cjs");
4
- const require_discovery = require("../../discovery-Dpz6SWwe.cjs");
5
- require("../../server-BjWrcc5T.cjs");
6
- const require_ingestTypes = require("../../ingestTypes-DbuQNuK6.cjs");
3
+ const require_ids = require("../../ids-BnTh3Hl0.cjs");
4
+ const require_discovery = require("../../discovery--kRUWed0.cjs");
5
+ const require_server = require("../../server-w5ZZRLju.cjs");
6
+ const require_ingestTypes = require("../../ingestTypes-xX8VpQqh.cjs");
7
7
  const require_feeds_paths = require("../paths.cjs");
8
- const require_notifier = require("../../notifier-bS8IEeLA.cjs");
8
+ const require_notifier = require("../../notifier-tMsAXyXp.cjs");
9
9
  let node_path = require("node:path");
10
10
  node_path = require_rolldown_runtime.__toESM(node_path, 1);
11
- let node_fs_promises = require("node:fs/promises");
12
11
  let node_crypto = require("node:crypto");
12
+ let node_fs_promises = require("node:fs/promises");
13
13
  let node_dns_promises = require("node:dns/promises");
14
14
  let node_net = require("node:net");
15
15
  let fast_xml_parser = require("fast-xml-parser");
16
16
  let _receptron_task_scheduler = require("@receptron/task-scheduler");
17
17
  //#region src/feeds/server/host.ts
18
- var current = null;
18
+ var hostSlot = require_discovery.createHostSlot("@mulmoclaude/core/feeds: configureFeedsHost()");
19
19
  /** Wire the feeds engine to a host. Call once at startup, before any refresh. */
20
20
  function configureFeedsHost(host) {
21
- if (current && current !== host) throw new Error("@mulmoclaude/core/feeds: configureFeedsHost() was already called with a different host");
22
- current = host;
21
+ hostSlot.set(host);
23
22
  }
24
23
  /** The configured host, or throw if `configureFeedsHost` was never called. */
25
24
  function requireFeedsHost() {
26
- if (!current) throw new Error("@mulmoclaude/core/feeds: configureFeedsHost() was not called by the host");
27
- return current;
25
+ return hostSlot.get();
28
26
  }
29
27
  /** Test-only: clear the configured host. */
30
28
  function resetFeedsHostForTesting() {
31
- current = null;
29
+ hostSlot.reset();
32
30
  }
33
31
  /** Forwarding logger so engine modules can `import { log }` without each
34
- * reaching for `requireFeedsHost().log`. */
35
- var log = {
36
- error: (prefix, msg, data) => requireFeedsHost().log.error(prefix, msg, data),
37
- warn: (prefix, msg, data) => requireFeedsHost().log.warn(prefix, msg, data),
38
- info: (prefix, msg, data) => requireFeedsHost().log.info(prefix, msg, data),
39
- debug: (prefix, msg, data) => requireFeedsHost().log.debug(prefix, msg, data)
40
- };
32
+ * reaching for `requireFeedsHost().log`. Non-critical: calls before the host
33
+ * is wired are dropped (not thrown), matching the collection/google seams. */
34
+ var log = require_discovery.createForwardingLogger(() => hostSlot.peek()?.log ?? null);
41
35
  //#endregion
42
36
  //#region src/feeds/server/registry.ts
43
37
  /** Every registered feed, as a discovered collection (carrying its
@@ -87,69 +81,156 @@ function getRetriever(kind) {
87
81
  return registry.get(kind);
88
82
  }
89
83
  //#endregion
90
- //#region src/feeds/server/fetch/httpClient.ts
91
- var ONE_SECOND_MS = 1e3;
92
- /** Identifies the bot to site operators. */
93
- var FEED_USER_AGENT = "MulmoClaude-FeedBot/1.0 (+https://github.com/receptron/mulmoclaude)";
94
- /** Per-request wall-clock cap so a hung server can't wedge a refresh. */
95
- var DEFAULT_FEED_TIMEOUT_MS = 30 * ONE_SECOND_MS;
96
- /** Cap on redirect hops followed (each re-checked for SSRF). */
97
- var MAX_REDIRECTS = 5;
98
- var BLOCKED_V4_CIDRS = [
84
+ //#region ../common/dist/ssrf.js
85
+ var ALLOWED_PROTOCOLS = /* @__PURE__ */ new Set(["http:", "https:"]);
86
+ var BLOCKED_HOSTNAMES = /* @__PURE__ */ new Set([
87
+ "localhost",
88
+ "ip6-localhost",
89
+ "ip6-loopback"
90
+ ]);
91
+ var BLOCKED_HOSTNAME_SUFFIXES = [
92
+ ".localhost",
93
+ ".local",
94
+ ".internal"
95
+ ];
96
+ /** IPv4 ranges that must never be fetched, as [firstAddress, prefixLength].
97
+ * The union of the pre-#2459 per-guard tables — removing an entry weakens
98
+ * every guard in the repo at once, so treat edits as security changes. */
99
+ var BLOCKED_IPV4_RANGES = [
99
100
  ["0.0.0.0", 8],
100
101
  ["10.0.0.0", 8],
101
102
  ["100.64.0.0", 10],
102
103
  ["127.0.0.0", 8],
103
104
  ["169.254.0.0", 16],
104
105
  ["172.16.0.0", 12],
105
- ["192.168.0.0", 16]
106
+ ["192.0.0.0", 24],
107
+ ["192.168.0.0", 16],
108
+ ["198.18.0.0", 15],
109
+ ["224.0.0.0", 4],
110
+ ["240.0.0.0", 4]
106
111
  ];
112
+ /** Dotted-decimal IPv4 → unsigned 32-bit value; null for anything else
113
+ * (hex / octal octets, whitespace, wrong part count). */
107
114
  function ipv4ToInt(address) {
108
- const octets = address.split(".").map(Number);
109
- if (octets.length !== 4 || octets.some((part) => !Number.isInteger(part) || part < 0 || part > 255)) return null;
110
- return (octets[0] << 24 | octets[1] << 16 | octets[2] << 8 | octets[3]) >>> 0;
115
+ const parts = address.split(".");
116
+ if (parts.length !== 4) return null;
117
+ const octets = parts.map((part) => /^\d{1,3}$/.test(part) ? Number(part) : NaN);
118
+ if (octets.some((octet) => Number.isNaN(octet) || octet > 255)) return null;
119
+ return octets.reduce((value, octet) => value * 256 + octet, 0);
120
+ }
121
+ function isBlockedIpv4Value(value) {
122
+ return BLOCKED_IPV4_RANGES.some(([base, prefix]) => {
123
+ const baseValue = ipv4ToInt(base);
124
+ if (baseValue === null) return false;
125
+ const mask = prefix === 0 ? 0 : -1 << 32 - prefix >>> 0;
126
+ return (value & mask) === (baseValue & mask);
127
+ });
111
128
  }
129
+ /** True when this parses as IPv4 AND falls inside a blocked range. Non-IPv4
130
+ * input is false — "not a blocked literal", not "safe"; a caller that must
131
+ * reject non-IP input entirely does so at its own boundary. */
112
132
  function isBlockedIpv4(address) {
113
133
  const value = ipv4ToInt(address);
114
- if (value === null) return true;
115
- return BLOCKED_V4_CIDRS.some(([base, bits]) => {
116
- const baseInt = ipv4ToInt(base) ?? 0;
117
- const mask = bits === 0 ? 0 : 4294967295 << 32 - bits >>> 0;
118
- return (value & mask) === (baseInt & mask);
119
- });
134
+ if (value === null) return false;
135
+ return isBlockedIpv4Value(value);
136
+ }
137
+ /** `URL.hostname` keeps the brackets on an IPv6 literal (`[::1]`); strip them
138
+ * so the address checks see the bare address. */
139
+ function stripIpv6Brackets(hostname) {
140
+ return hostname.replace(/^\[|\]$/g, "");
141
+ }
142
+ /** The 32-bit value inside an IPv4-mapped IPv6 literal, or null when the
143
+ * address is not v4-mapped. The hex spelling matters: WHATWG URL serializes
144
+ * `[::ffff:127.0.0.1]` as `::ffff:7f00:1`, so matching only the dotted form
145
+ * would wave the mapped loopback through any URL-sourced check. */
146
+ function mappedIpv4Value(lower) {
147
+ const dotted = /^::ffff:(\d+\.\d+\.\d+\.\d+)$/.exec(lower);
148
+ if (dotted) return ipv4ToInt(dotted[1]);
149
+ const hex = /^::ffff:([0-9a-f]{1,4}):([0-9a-f]{1,4})$/.exec(lower);
150
+ if (hex) return Number.parseInt(hex[1], 16) * 65536 + Number.parseInt(hex[2], 16);
151
+ return null;
120
152
  }
153
+ /** Blocks loopback/unspecified, fc00::/7 unique-local, fe80::/10 link-local,
154
+ * and IPv4-mapped forms of blocked v4 addresses. Mask-based on the leading
155
+ * group — prefix string matching would miss e.g. fe9a:: (inside fe80::/10). */
121
156
  function isBlockedIpv6(address) {
122
- const lower = address.toLowerCase();
157
+ const lower = stripIpv6Brackets(address.toLowerCase());
123
158
  if (lower === "::1" || lower === "::") return true;
124
- if (lower.startsWith("fe80")) return true;
125
- if (lower.startsWith("fc") || lower.startsWith("fd")) return true;
126
- const mapped = /^::ffff:(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})$/.exec(lower);
127
- return mapped ? isBlockedIpv4(mapped[1]) : false;
128
- }
129
- /** True for any address we must not fetch (also blocks non-IP input). */
159
+ const mapped = mappedIpv4Value(lower);
160
+ if (mapped !== null) return isBlockedIpv4Value(mapped);
161
+ const [head] = lower.split(":");
162
+ if (head.length === 0) return false;
163
+ const leading = Number.parseInt(head, 16);
164
+ if (Number.isNaN(leading)) return false;
165
+ if ((leading & 65024) === 64512) return true;
166
+ if ((leading & 65472) === 65152) return true;
167
+ return false;
168
+ }
169
+ /** True when this literal address must never be fetched. Non-IP input is
170
+ * false — see `isBlockedIpv4`. */
130
171
  function isBlockedIp(address) {
172
+ return address.includes(":") ? isBlockedIpv6(address) : isBlockedIpv4(address);
173
+ }
174
+ /** Names that are internal by convention (localhost, *.local, *.internal…),
175
+ * rejected without waiting for DNS to prove it. */
176
+ function isBlockedHostname(hostname) {
177
+ const lower = hostname.toLowerCase().replace(/\.$/, "");
178
+ if (BLOCKED_HOSTNAMES.has(lower)) return true;
179
+ return BLOCKED_HOSTNAME_SUFFIXES.some((suffix) => lower.endsWith(suffix));
180
+ }
181
+ function parseUrl(raw) {
182
+ try {
183
+ return new URL(raw);
184
+ } catch {
185
+ return null;
186
+ }
187
+ }
188
+ /** Parse and apply every check that doesn't need the network: shape, scheme,
189
+ * obviously-internal hostname, and literal addresses. Returns null on reject.
190
+ * Callers must still vet what non-literal hostnames resolve to. */
191
+ function parseSafeUrlShape(raw) {
192
+ const url = parseUrl(raw);
193
+ if (url === null) return null;
194
+ if (!ALLOWED_PROTOCOLS.has(url.protocol)) return null;
195
+ const hostname = stripIpv6Brackets(url.hostname);
196
+ if (hostname.length === 0) return null;
197
+ if (isBlockedHostname(hostname)) return null;
198
+ if (isBlockedIp(hostname)) return null;
199
+ return url;
200
+ }
201
+ //#endregion
202
+ //#region src/feeds/server/fetch/httpClient.ts
203
+ var ONE_SECOND_MS = 1e3;
204
+ /** Identifies the bot to site operators. */
205
+ var FEED_USER_AGENT = "MulmoClaude-FeedBot/1.0 (+https://github.com/receptron/mulmoclaude)";
206
+ /** Per-request wall-clock cap so a hung server can't wedge a refresh. */
207
+ var DEFAULT_FEED_TIMEOUT_MS = 30 * ONE_SECOND_MS;
208
+ /** Cap on redirect hops followed (each re-checked for SSRF). */
209
+ var MAX_REDIRECTS = 5;
210
+ /** DNS answers should be IP literals; anything else is blocked outright. */
211
+ function isBlockedResolvedIp(address) {
131
212
  const kind = (0, node_net.isIP)(address);
132
213
  if (kind === 4) return isBlockedIpv4(address);
133
214
  if (kind === 6) return isBlockedIpv6(address);
134
215
  return true;
135
216
  }
136
- /** Reject non-http(s) URLs and URLs that resolve to a private/loopback
137
- * address (SSRF guard). Throws with a clear reason; returns void on pass. */
138
- async function assertFetchableUrl(rawUrl) {
139
- if (!/^https?:\/\//i.test(rawUrl)) throw new Error(`refusing non-http(s) URL: ${rawUrl}`);
140
- const { hostname } = new URL(rawUrl);
141
- const host = hostname.startsWith("[") && hostname.endsWith("]") ? hostname.slice(1, -1) : hostname;
142
- if ((0, node_net.isIP)(host)) {
143
- if (isBlockedIp(host)) throw new Error(`refusing to fetch a private/loopback address: ${host}`);
144
- return;
145
- }
146
- let addresses;
217
+ async function resolveAddresses(host) {
147
218
  try {
148
- addresses = await (0, node_dns_promises.lookup)(host, { all: true });
219
+ return await (0, node_dns_promises.lookup)(host, { all: true });
149
220
  } catch (err) {
150
221
  throw new Error(`could not resolve host '${host}': ${String(err)}`);
151
222
  }
152
- const blocked = addresses.find((entry) => isBlockedIp(entry.address));
223
+ }
224
+ /** Reject non-http(s) URLs, internal-by-convention hostnames, and URLs whose
225
+ * host is / resolves to a private/loopback address (SSRF guard). Throws with
226
+ * a clear reason; returns void on pass. */
227
+ async function assertFetchableUrl(rawUrl) {
228
+ if (!/^https?:\/\//i.test(rawUrl)) throw new Error(`refusing non-http(s) URL: ${rawUrl}`);
229
+ const url = parseSafeUrlShape(rawUrl);
230
+ if (url === null) throw new Error(`refusing to fetch a private/loopback/internal address: ${rawUrl}`);
231
+ const host = stripIpv6Brackets(url.hostname);
232
+ if ((0, node_net.isIP)(host)) return;
233
+ const blocked = (await resolveAddresses(host)).find((entry) => isBlockedResolvedIp(entry.address));
153
234
  if (blocked) throw new Error(`refusing to fetch '${host}' — resolves to a private/loopback address (${blocked.address})`);
154
235
  }
155
236
  async function fetchOnce(url, timeoutMs) {
@@ -308,7 +389,8 @@ function step(current, token) {
308
389
  if (current === null || current === void 0) return void 0;
309
390
  if (token.kind === "key") {
310
391
  if (typeof current !== "object" || Array.isArray(current)) return void 0;
311
- return current[token.key];
392
+ const record = current;
393
+ return Object.hasOwn(record, token.key) ? record[token.key] : void 0;
312
394
  }
313
395
  return Array.isArray(current) ? current[token.index] : void 0;
314
396
  }
@@ -497,10 +579,10 @@ async function refreshViaAgent(workspaceRoot, collection, opts) {
497
579
  if (!ingest || ingest.kind !== "agent") return result(slug, { errors: ["collection has no agent ingest config"] });
498
580
  const workerRunner = workerRunnerOrNull();
499
581
  if (!workerRunner) return result(slug, { errors: ["agent ingest worker runner not configured"] });
500
- const template = await require_discovery.readSkillTemplate(collection.skillDir, ingest.template);
582
+ const template = await require_server.readSkillTemplate(collection.skillDir, ingest.template);
501
583
  if (template === null) return result(slug, { errors: [`ingest template '${ingest.template}' could not be read`] });
502
584
  const items = await require_discovery.listItems(collection.dataDir, { workspaceRoot });
503
- const message = require_discovery.buildCollectionActionSeedPrompt(items, collection.schema, template, require_discovery.promptPathsFor(collection, workspaceRoot));
585
+ const message = require_server.buildCollectionActionSeedPrompt(items, collection.schema, template, require_server.promptPathsFor(collection, workspaceRoot));
504
586
  let launch;
505
587
  try {
506
588
  launch = await workerRunner({