@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
@@ -1,30 +0,0 @@
1
- //#region src/utils/errors.ts
2
- function errorMessage(err, fallback) {
3
- if (err instanceof Error) return err.message;
4
- if (err !== null && typeof err === "object") {
5
- const obj = err;
6
- if (typeof obj.details === "string" && obj.details) return obj.details;
7
- if (typeof obj.message === "string" && obj.message) return obj.message;
8
- }
9
- if (fallback !== void 0) return fallback;
10
- return String(err);
11
- }
12
- function toError(err, fallback) {
13
- if (err instanceof Error) return err;
14
- return new Error(fallback ?? errorMessage(err));
15
- }
16
- //#endregion
17
- Object.defineProperty(exports, "errorMessage", {
18
- enumerable: true,
19
- get: function() {
20
- return errorMessage;
21
- }
22
- });
23
- Object.defineProperty(exports, "toError", {
24
- enumerable: true,
25
- get: function() {
26
- return toError;
27
- }
28
- });
29
-
30
- //# sourceMappingURL=errors-7P5eMOSX.cjs.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"errors-7P5eMOSX.cjs","names":[],"sources":["../src/utils/errors.ts"],"sourcesContent":["// Canonical error helpers for the whole monorepo. Host (`server/`, `src/`)\n// re-exports these rather than keeping its own copy — before #2217 the same\n// function existed 14 times across 4 behaviours, and the two that mattered\n// disagreed on gRPC-shaped errors: `{ code, details, metadata }` surfaced as\n// \"quota exceeded\" through the host copy and as \"[object Object]\" through the\n// core ones. Browser-safe: pure string work, no node imports.\n\n// Non-Error objects with a `details` (gRPC convention) or `message` string\n// field have that field surfaced — without this they stringify to\n// `[object Object]`.\n//\n// `fallback` covers the route-handler idiom where a throw of a plain non-Error\n// value should surface as a descriptive message (\"rebuild failed\") rather than\n// `String(err)` noise. Pass one at error-response boundaries; omit it for\n// logging contexts where `String(err)` is fine.\nexport function errorMessage(err: unknown, fallback?: string): string {\n if (err instanceof Error) return err.message;\n if (err !== null && typeof err === \"object\") {\n const obj = err as { details?: unknown; message?: unknown };\n if (typeof obj.details === \"string\" && obj.details) return obj.details;\n if (typeof obj.message === \"string\" && obj.message) return obj.message;\n }\n if (fallback !== undefined) return fallback;\n return String(err);\n}\n\n// Coerce an unknown caught value into an Error, preserving the original if it\n// already was one. Use in error boundaries / Promise rejections / event-handler\n// onerror callbacks where the downstream API wants an Error object.\n//\n// `fallback` is the message used when coercing a non-Error value — pass a\n// descriptive string for cases where `String(err)` would just produce noise\n// (e.g. `<img>.onerror` hands you an Event, not the underlying load failure).\nexport function toError(err: unknown, fallback?: string): Error {\n if (err instanceof Error) return err;\n return new Error(fallback ?? errorMessage(err));\n}\n"],"mappings":";AAeA,SAAgB,aAAa,KAAc,UAA2B;CACpE,IAAI,eAAe,OAAO,OAAO,IAAI;CACrC,IAAI,QAAQ,QAAQ,OAAO,QAAQ,UAAU;EAC3C,MAAM,MAAM;EACZ,IAAI,OAAO,IAAI,YAAY,YAAY,IAAI,SAAS,OAAO,IAAI;EAC/D,IAAI,OAAO,IAAI,YAAY,YAAY,IAAI,SAAS,OAAO,IAAI;CACjE;CACA,IAAI,aAAa,KAAA,GAAW,OAAO;CACnC,OAAO,OAAO,GAAG;AACnB;AASA,SAAgB,QAAQ,KAAc,UAA0B;CAC9D,IAAI,eAAe,OAAO,OAAO;CACjC,OAAO,IAAI,MAAM,YAAY,aAAa,GAAG,CAAC;AAChD"}
@@ -1,19 +0,0 @@
1
- //#region src/utils/errors.ts
2
- function errorMessage(err, fallback) {
3
- if (err instanceof Error) return err.message;
4
- if (err !== null && typeof err === "object") {
5
- const obj = err;
6
- if (typeof obj.details === "string" && obj.details) return obj.details;
7
- if (typeof obj.message === "string" && obj.message) return obj.message;
8
- }
9
- if (fallback !== void 0) return fallback;
10
- return String(err);
11
- }
12
- function toError(err, fallback) {
13
- if (err instanceof Error) return err;
14
- return new Error(fallback ?? errorMessage(err));
15
- }
16
- //#endregion
17
- export { toError as n, errorMessage as t };
18
-
19
- //# sourceMappingURL=errors-eid6Mes3.js.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"errors-eid6Mes3.js","names":[],"sources":["../src/utils/errors.ts"],"sourcesContent":["// Canonical error helpers for the whole monorepo. Host (`server/`, `src/`)\n// re-exports these rather than keeping its own copy — before #2217 the same\n// function existed 14 times across 4 behaviours, and the two that mattered\n// disagreed on gRPC-shaped errors: `{ code, details, metadata }` surfaced as\n// \"quota exceeded\" through the host copy and as \"[object Object]\" through the\n// core ones. Browser-safe: pure string work, no node imports.\n\n// Non-Error objects with a `details` (gRPC convention) or `message` string\n// field have that field surfaced — without this they stringify to\n// `[object Object]`.\n//\n// `fallback` covers the route-handler idiom where a throw of a plain non-Error\n// value should surface as a descriptive message (\"rebuild failed\") rather than\n// `String(err)` noise. Pass one at error-response boundaries; omit it for\n// logging contexts where `String(err)` is fine.\nexport function errorMessage(err: unknown, fallback?: string): string {\n if (err instanceof Error) return err.message;\n if (err !== null && typeof err === \"object\") {\n const obj = err as { details?: unknown; message?: unknown };\n if (typeof obj.details === \"string\" && obj.details) return obj.details;\n if (typeof obj.message === \"string\" && obj.message) return obj.message;\n }\n if (fallback !== undefined) return fallback;\n return String(err);\n}\n\n// Coerce an unknown caught value into an Error, preserving the original if it\n// already was one. Use in error boundaries / Promise rejections / event-handler\n// onerror callbacks where the downstream API wants an Error object.\n//\n// `fallback` is the message used when coercing a non-Error value — pass a\n// descriptive string for cases where `String(err)` would just produce noise\n// (e.g. `<img>.onerror` hands you an Event, not the underlying load failure).\nexport function toError(err: unknown, fallback?: string): Error {\n if (err instanceof Error) return err;\n return new Error(fallback ?? errorMessage(err));\n}\n"],"mappings":";AAeA,SAAgB,aAAa,KAAc,UAA2B;CACpE,IAAI,eAAe,OAAO,OAAO,IAAI;CACrC,IAAI,QAAQ,QAAQ,OAAO,QAAQ,UAAU;EAC3C,MAAM,MAAM;EACZ,IAAI,OAAO,IAAI,YAAY,YAAY,IAAI,SAAS,OAAO,IAAI;EAC/D,IAAI,OAAO,IAAI,YAAY,YAAY,IAAI,SAAS,OAAO,IAAI;CACjE;CACA,IAAI,aAAa,KAAA,GAAW,OAAO;CACnC,OAAO,OAAO,GAAG;AACnB;AASA,SAAgB,QAAQ,KAAc,UAA0B;CAC9D,IAAI,eAAe,OAAO,OAAO;CACjC,OAAO,IAAI,MAAM,YAAY,aAAa,GAAG,CAAC;AAChD"}
@@ -1,8 +0,0 @@
1
- export declare const DEFAULT_FETCH_TIMEOUT_MS: number;
2
- export type FetchWithTimeoutInit = Parameters<typeof fetch>[1] & {
3
- timeoutMs?: number;
4
- };
5
- /** `fetch` with a finite timeout. Rejects with a `TimeoutError` once
6
- * `timeoutMs` elapses. Composes with a caller-supplied `signal` so external
7
- * cancellation still works. */
8
- export declare function fetchWithTimeout(url: string | URL, init?: FetchWithTimeoutInit): Promise<Response>;
@@ -1 +0,0 @@
1
- {"version":3,"file":"graph-2R4HbQAg.cjs","names":[],"sources":["../src/wiki/link.ts","../src/wiki/index-parse.ts","../src/wiki/lint.ts","../src/wiki/graph.ts"],"sourcesContent":["// Pure helpers for `[[wiki-link]]` syntax. Used by the lint\n// (`server/api/routes/wiki.ts`), the page resolver, and the\n// frontend renderer (`src/plugins/wiki/helpers.ts`) — before this\n// module the three sites each had their own implementation and\n// none of them handled the `[[target|display]]` form, producing\n// false-positive \"broken link\" warnings on slug-aliased links\n// (issue #1297).\n//\n// All functions are pure string operations; safe to import from\n// browser bundles. No `node:*` imports anywhere in this file.\n\n/** Inline `[[...]]` link pattern. The capture group is the raw\n * text between the brackets — the caller is expected to feed it\n * through `parseWikiLink` to split off any `|display` suffix.\n *\n * Body length is capped at 200 chars to keep the regex linear:\n * catastrophic backtracking isn't possible (no nested\n * alternation), but a malicious page with thousands of\n * unmatched `[[` could still pin the CPU. 200 is well above the\n * longest legitimate title we've seen and matches the cap the\n * server-side `WIKI_LINK_PATTERN` constant has carried since the\n * original implementation in #951. */\nexport const WIKI_LINK_PATTERN = /\\[\\[([^\\][\\r\\n]{1,200})\\]\\]/g;\n\n/** The structural shape of an inline `[[target|display]]` link.\n * `target` is what the resolver / lint compares against page\n * slugs; `display` is what the renderer shows to the user.\n * When the user writes `[[foo]]` (no pipe), both fields hold\n * `foo`. */\nexport interface WikiLink {\n target: string;\n display: string;\n}\n\n/** Split the raw text inside `[[...]]` into target + display.\n *\n * - `[[foo|Bar Baz]]` → `{ target: \"foo\", display: \"Bar Baz\" }`\n * - `[[foo]]` → `{ target: \"foo\", display: \"foo\" }`\n * - `[[|empty target]]` → `{ target: \"\", display: \"empty target\" }`\n * (caller decides whether an empty target is meaningful;\n * typically lint flags it, renderer shows the display text raw)\n * - `[[foo|]]` → `{ target: \"foo\", display: \"\" }`\n * (same — caller decides)\n *\n * Whitespace around the pipe is preserved on the display side\n * (so the user can write `[[foo| ある記事 ]]` and get the\n * spacing intentionally) but TRIMMED on the target side, since\n * a slug-comparable target with leading/trailing whitespace is\n * always a typo.\n *\n * Only the FIRST pipe splits. `[[a|b|c]]` becomes\n * `{ target: \"a\", display: \"b|c\" }` so display strings can\n * legitimately contain a literal `|`. */\nexport function parseWikiLink(inner: string): WikiLink {\n const pipeIdx = inner.indexOf(\"|\");\n if (pipeIdx === -1) {\n return { target: inner, display: inner };\n }\n const target = inner.slice(0, pipeIdx).trim();\n const display = inner.slice(pipeIdx + 1);\n return { target, display };\n}\n","// Pure parsers for `data/wiki/index.md` content. Used by:\n// - server/api/routes/wiki.ts — page resolver + lint\n// - frontend lint preview (future) — same parser, no fork\n//\n// All exports here are pure string ops; no `node:*` imports allowed.\n// If a future caller needs to walk the filesystem, that wrapper goes\n// under `@mulmoclaude/core/wiki/server` (today only `server/paths.ts`\n// touches `node:path`; nothing here touches disk).\n\nimport { parseWikiLink } from \"./link.js\";\nimport { wikiSlugify } from \"./slug.js\";\n\nexport interface WikiPageEntry {\n title: string;\n slug: string;\n description: string;\n tags: string[];\n}\n\n// ── Patterns ──────────────────────────────────────────────────\n// Bullet patterns live here (not server/utils/regex.ts) because the\n// index parser is the only consumer and the regexes are part of the\n// public surface that callers may want to reason about in tests.\n// Linear in line length: every `[^x]+` runs over a fixed exclusion\n// set with a hard delimiter; the optional summary group's `(.*)` is\n// a single greedy run with no nested overlap.\n\n// eslint-disable-next-line security/detect-unsafe-regex -- bullet-link parser; bounded captures with hard delimiters\nexport const BULLET_LINK_PATTERN = /^[-*]\\s+\\[([^\\]]+)\\]\\(([^)]*)\\)(?:\\s*[—–-]\\s*(.*))?/;\n// eslint-disable-next-line security/detect-unsafe-regex -- same shape as BULLET_LINK_PATTERN\nexport const BULLET_WIKI_LINK_PATTERN = /^[-*]\\s+\\[\\[([^\\]]+)\\]\\](?:\\s*[—–-]\\s*(.*))?/;\n\nconst TABLE_SEPARATOR_PATTERN = /^\\|[\\s|:-]+\\|$/;\n\n// Unicode-aware tag body: any letter or number in any script (so\n// Japanese / Chinese / Korean tags like `#クラウド` or `#可視化` work),\n// plus `-` and `_` as internal joiners. First char is a letter or\n// number only — no leading punctuation.\nconst HASHTAG_PATTERN = /(?:^|\\s)#([\\p{L}\\p{N}][\\p{L}\\p{N}_-]*)/gu;\n\n// ── Tag extraction ────────────────────────────────────────────\n\n/** Extract `#tag` tokens from a bullet description, returning the\n * stripped description and a sorted, deduped, lowercased tag list.\n * Only matches at word boundaries so mid-word `#` (e.g. anchor\n * URLs) is left alone. */\nexport function extractHashTags(text: string): { description: string; tags: string[] } {\n const tags: string[] = [];\n HASHTAG_PATTERN.lastIndex = 0;\n let match: RegExpExecArray | null;\n while ((match = HASHTAG_PATTERN.exec(text)) !== null) {\n tags.push(match[1].toLowerCase());\n }\n const description = text.replace(HASHTAG_PATTERN, \"\").replace(/\\s+/g, \" \").trim();\n const deduped = [...new Set(tags)].sort();\n return { description, tags: deduped };\n}\n\n/** Split a table Tags cell — tolerates comma, whitespace, or `#`\n * prefixes. Empty cell yields an empty list. */\nexport function parseTagsCell(cell: string): string[] {\n const tokens = cell\n .split(/[,\\s]+/)\n .map((token) => token.trim().replace(/^#/, \"\").toLowerCase())\n .filter((token) => token.length > 0);\n return [...new Set(tokens)].sort();\n}\n\n// ── Table parser ──────────────────────────────────────────────\n\n/** Map header cell names → column indices, case- and whitespace-\n * tolerant. Used by the row parser to locate the Tags column (and\n * any other named column) without assuming a fixed position, so\n * older 3- and 4-column tables keep working. */\nexport function buildTableColumnMap(headerRow: string): Map<string, number> {\n const cells = headerRow\n .split(\"|\")\n .slice(1, -1)\n // Mirror parseTableRow's cell-normalising: strip the surrounding\n // backticks that commonly wrap cell values in wiki tables.\n // Without this, a `| \\`tags\\` |` header maps to the key \"`tags`\"\n // and the subsequent `columnMap.get(\"tags\")` lookup silently\n // misses the column, falling back to `tags: []`.\n .map((cell) => cell.trim().replace(/^`|`$/g, \"\").toLowerCase());\n const map = new Map<string, number>();\n cells.forEach((cell, i) => {\n if (cell) map.set(cell, i);\n });\n return map;\n}\n\ninterface TableColumnIndices {\n slug: number;\n title: number;\n summary: number;\n /** Undefined when the table has no `tags` column — caller skips\n * the tags lookup entirely and the row gets `tags: []`. */\n tags: number | undefined;\n}\n\n/** Resolve the per-column indices the row parser needs. Falls back\n * to positional defaults (0/1/2) when the table has no header map.\n * \"summary\" is the canonical column name; \"description\" is accepted\n * as a legacy alias used by older fixtures. */\nfunction resolveTableColumnIndices(columnMap: Map<string, number> | null): TableColumnIndices {\n return {\n slug: columnMap?.get(\"slug\") ?? 0,\n title: columnMap?.get(\"title\") ?? 1,\n summary: columnMap?.get(\"summary\") ?? columnMap?.get(\"description\") ?? 2,\n tags: columnMap?.get(\"tags\"),\n };\n}\n\nfunction parseTableRow(trimmed: string, columnMap: Map<string, number> | null): WikiPageEntry | null {\n const cols = trimmed\n .split(\"|\")\n .slice(1, -1)\n .map((column) => column.trim().replace(/^`|`$/g, \"\"));\n if (cols.length < 2) return null;\n\n const idx = resolveTableColumnIndices(columnMap);\n const slug = cols[idx.slug] ?? \"\";\n const title = cols[idx.title] || slug;\n if (!slug || !title) return null;\n\n const description = cols[idx.summary] ?? \"\";\n const tags = idx.tags !== undefined ? parseTagsCell(cols[idx.tags] ?? \"\") : [];\n return { title, slug, description, tags };\n}\n\n// ── Bullet-row parsers ────────────────────────────────────────\n\n/** Extract the slug segment from a bullet link's href. Accepts the\n * canonical `pages/<slug>.md`, a bare `<slug>.md`, or just `<slug>`\n * — the three forms produced by different historical writers of\n * index.md. Returns \"\" for hrefs that don't look like a wiki page\n * reference (e.g. `https://example.com`) so the caller can fall\n * back to title-based slugification. */\nexport function extractSlugFromBulletHref(rawHref: string): string {\n const href = rawHref.trim();\n if (!href) return \"\";\n if (/^[a-z]+:\\/\\//i.test(href)) return \"\";\n const lastSegment = href.split(\"/\").pop() ?? href;\n return lastSegment.replace(/\\.md$/i, \"\");\n}\n\nfunction parseBulletLinkRow(trimmed: string): WikiPageEntry | null {\n const match = BULLET_LINK_PATTERN.exec(trimmed);\n if (!match) return null;\n const title = match[1].trim();\n const href = match[2] ?? \"\";\n const raw = match[3]?.trim() ?? \"\";\n const { description, tags } = extractHashTags(raw);\n // Prefer the slug embedded in the href so non-ASCII titles keep\n // a navigable slug. Fall back to slugifying the title only when\n // the href has no recognisable slug (rare — usually means the\n // author put an external URL here).\n const slug = extractSlugFromBulletHref(href) || wikiSlugify(title);\n return { title, slug, description, tags };\n}\n\nfunction parseBulletWikiLinkRow(trimmed: string): WikiPageEntry | null {\n const match = BULLET_WIKI_LINK_PATTERN.exec(trimmed);\n if (!match) return null;\n // Split `[[target|display]]` so the bullet entry's slug derives\n // from the TARGET (which is already a slug-shaped identifier\n // when the author wrote the alias form) and the title shows\n // the DISPLAY half. Pre-#1297 the parser slugified the full\n // `target|display` body, which collapses `|` to \"\" and\n // produces a wrong slug — Codex review on PR #1312.\n const { target, display } = parseWikiLink(match[1]);\n const title = (display || target).trim();\n const slug = target ? wikiSlugify(target) : wikiSlugify(title);\n const raw = match[2]?.trim() ?? \"\";\n const { description, tags } = extractHashTags(raw);\n return { title, slug, description, tags };\n}\n\n// ── Top-level parser ──────────────────────────────────────────\n\n/** Parse entries from index.md. Supports three formats:\n * 1. Table: `| slug | Title | Summary | (Tags) |`\n * 2. Bullet link: `- [Title](pages/slug.md) — description`\n * 3. Wiki link: `- [[Title]] — description`\n *\n * Returns entries in source order. Any unrecognised line is\n * silently skipped — index.md is freeform markdown otherwise. */\nexport function parseIndexEntries(content: string): WikiPageEntry[] {\n const entries: WikiPageEntry[] = [];\n let inTable = false;\n let columnMap: Map<string, number> | null = null;\n\n for (const line of content.split(\"\\n\")) {\n const trimmed = line.trim();\n\n if (trimmed.startsWith(\"|\")) {\n if (TABLE_SEPARATOR_PATTERN.test(trimmed)) {\n inTable = true;\n continue;\n }\n if (!inTable) {\n // First `|`-line before the separator is the header. Capture\n // the column map so parseTableRow can locate the Tags\n // column (if any) by name rather than position.\n columnMap = buildTableColumnMap(trimmed);\n inTable = true;\n continue;\n }\n const entry = parseTableRow(trimmed, columnMap);\n if (entry) entries.push(entry);\n continue;\n }\n\n inTable = false;\n columnMap = null;\n\n const bullet = parseBulletLinkRow(trimmed) ?? parseBulletWikiLinkRow(trimmed);\n if (bullet) entries.push(bullet);\n }\n return entries;\n}\n","// Pure wiki lint rules.\n//\n// Each rule takes plain inputs (Sets, page-entry arrays, raw file\n// contents) so the whole pipeline can be unit-tested without\n// filesystem fixtures. The Express route at\n// `server/api/routes/wiki.ts` is a thin shell that reads the disk,\n// calls these rules, formats the result. A future frontend \"lint\n// preview before save\" would call the same functions over an\n// in-memory snapshot.\n\nimport type { WikiPageEntry } from \"./index-parse.js\";\nimport { WIKI_LINK_PATTERN, parseWikiLink } from \"./link.js\";\nimport { wikiSlugify } from \"./slug.js\";\n\n/** Files on disk that aren't referenced by index.md. */\nexport function findOrphanPages(fileSlugs: ReadonlySet<string>, indexedSlugs: ReadonlySet<string>): string[] {\n const issues: string[] = [];\n for (const slug of fileSlugs) {\n if (!indexedSlugs.has(slug)) {\n issues.push(`- **Orphan page**: \\`${slug}.md\\` exists but is missing from index.md`);\n }\n }\n return issues;\n}\n\n/** Slugs referenced by index.md that have no corresponding file. */\nexport function findMissingFiles(pageEntries: readonly WikiPageEntry[], fileSlugs: ReadonlySet<string>): string[] {\n const issues: string[] = [];\n for (const entry of pageEntries) {\n if (!fileSlugs.has(entry.slug)) {\n issues.push(`- **Missing file**: index.md references \\`${entry.slug}\\` but the file does not exist`);\n }\n }\n return issues;\n}\n\n/** Walk a page's body for `[[…]]` links and flag any whose\n * resolved slug doesn't exist in the file set.\n *\n * Critically: this routes through `parseWikiLink` so\n * `[[slug|display]]` is split correctly — the lint slugifies the\n * TARGET, not the full bracket body. Pre-#1297 the lint\n * slugified the entire content (`slug|display`), which collapsed\n * to a slug that always missed and produced ~168 false-positive\n * \"broken link\" warnings. */\nexport function findBrokenLinksInPage(fileName: string, content: string, fileSlugs: ReadonlySet<string>): string[] {\n const issues: string[] = [];\n const matches = [...content.matchAll(WIKI_LINK_PATTERN)];\n for (const match of matches) {\n const { target } = parseWikiLink(match[1]);\n const linkSlug = wikiSlugify(target);\n // Empty target is its own diagnostic — `[[|display]]` or\n // `[[]]` slugifies to \"\" and would otherwise be flagged\n // identically to a real broken link. Keep the original raw\n // bracket body in the report so the user can grep their pages\n // for the malformed link.\n if (linkSlug.length === 0) {\n issues.push(`- **Broken link** in \\`${fileName}\\`: [[${match[1]}]] → empty target`);\n continue;\n }\n if (!fileSlugs.has(linkSlug)) {\n issues.push(`- **Broken link** in \\`${fileName}\\`: [[${match[1]}]] → \\`${linkSlug}.md\\` not found`);\n }\n }\n return issues;\n}\n\nfunction formatTagList(tags: readonly string[]): string {\n return `[${[...tags].sort().join(\", \")}]`;\n}\n\n/** Flag any slug whose index.md tags differ from the page's own\n * frontmatter `tags:` field. Comparison is set-based and order-\n * insensitive; both sides are lowercased at parse time. Slugs\n * missing from `frontmatterTagsBySlug` are ignored here — the\n * missing file itself is already reported by `findMissingFiles`. */\nexport function findTagDrift(pageEntries: readonly WikiPageEntry[], frontmatterTagsBySlug: ReadonlyMap<string, readonly string[]>): string[] {\n const issues: string[] = [];\n for (const entry of pageEntries) {\n // Lowercase on lookup — the caller keys the map with\n // lowercased slugs, so a `MyPage.md` filename still matches\n // an `entry.slug` of `mypage` produced by `wikiSlugify` on the\n // wiki-link parser path.\n const pageTags = frontmatterTagsBySlug.get(entry.slug.toLowerCase());\n if (pageTags === undefined) continue;\n const pageSet = new Set(pageTags);\n const indexSet = new Set(entry.tags);\n if (pageSet.size !== indexSet.size || [...pageSet].some((tag) => !indexSet.has(tag))) {\n issues.push(`- **Tag drift**: \\`${entry.slug}.md\\` frontmatter has ${formatTagList(pageTags)} but index.md has ${formatTagList(entry.tags)}`);\n }\n }\n return issues;\n}\n\n/** Render the final markdown report from the accumulated issues\n * list. Empty input yields the \"wiki is healthy\" sentinel so the\n * caller can write the same file every time without branching on\n * presence. */\nexport function formatLintReport(issues: readonly string[]): string {\n if (issues.length === 0) {\n return \"# Wiki Lint Report\\n\\n✓ No issues found. Wiki is healthy.\";\n }\n const noun = `issue${issues.length !== 1 ? \"s\" : \"\"}`;\n return `# Wiki Lint Report\\n\\n${issues.length} ${noun} found:\\n\\n${issues.join(\"\\n\")}`;\n}\n","// Pure builder for the wiki page→page link graph. Used by:\n// - server/api/routes/wiki.ts — the `graph` action endpoint\n// - src/plugins/wiki/View.vue — backlinks (\"linked references\") +\n// the Graph tab\n//\n// All functions are pure string / collection ops; no `node:*` imports,\n// so the frontend bundle can import them directly (same discipline as\n// the sibling `link.ts` / `lint.ts` / `index-parse.ts` modules).\n//\n// NOTE: this is page→page link structure, distinct from the\n// `server/workspace/wiki-backlinks/` module, which appends *session*\n// backlinks (a page → the chat that edited it, #109). Different concept\n// — do not conflate.\n\nimport { WIKI_LINK_PATTERN, parseWikiLink } from \"./link.js\";\nimport { wikiSlugify } from \"./slug.js\";\nimport type { WikiPageEntry } from \"./index-parse.js\";\n\nexport interface WikiGraphNode {\n slug: string;\n title: string;\n}\n\nexport interface WikiGraphEdge {\n from: string;\n to: string;\n}\n\nexport interface WikiGraph {\n nodes: WikiGraphNode[];\n edges: WikiGraphEdge[];\n}\n\n/** One page's raw body plus its canonical (file-derived) slug. */\nexport interface WikiPageContent {\n slug: string;\n content: string;\n}\n\n/** Resolve a raw `[[link]]` target to an existing page slug, or null.\n * Mirrors the route resolver's strategy: slugify first, then fall\n * back to matching an index entry title so non-ASCII targets like\n * `[[さくらインターネット]]` resolve to their ASCII file slug. */\nexport function resolveLinkTarget(target: string, fileSlugs: ReadonlySet<string>, slugByTitle: ReadonlyMap<string, string>): string | null {\n const slug = wikiSlugify(target);\n if (slug.length > 0 && fileSlugs.has(slug)) return slug;\n const byTitle = slugByTitle.get(target.trim());\n if (byTitle !== undefined && fileSlugs.has(byTitle)) return byTitle;\n return null;\n}\n\n/** Resolved, deduped outgoing slugs for one page body. Self-links and\n * links to non-existent pages are dropped (the latter are already a\n * lint \"broken link\", not a graph edge). */\nexport function pageOutgoingSlugs(fromSlug: string, content: string, fileSlugs: ReadonlySet<string>, slugByTitle: ReadonlyMap<string, string>): string[] {\n const out = new Set<string>();\n for (const match of content.matchAll(WIKI_LINK_PATTERN)) {\n const { target } = parseWikiLink(match[1]);\n const resolved = resolveLinkTarget(target, fileSlugs, slugByTitle);\n if (resolved !== null && resolved !== fromSlug) out.add(resolved);\n }\n return [...out];\n}\n\nfunction buildTitleMaps(entries: readonly WikiPageEntry[]): { titleBySlug: Map<string, string>; slugByTitle: Map<string, string> } {\n const titleBySlug = new Map<string, string>();\n const slugByTitle = new Map<string, string>();\n for (const entry of entries) {\n if (!titleBySlug.has(entry.slug)) titleBySlug.set(entry.slug, entry.title);\n if (entry.title.length > 0 && !slugByTitle.has(entry.title)) slugByTitle.set(entry.title, entry.slug);\n }\n return { titleBySlug, slugByTitle };\n}\n\n/** Build the full page→page graph. Nodes are the existing page files\n * (titled from index.md, falling back to the slug for un-indexed\n * pages); edges are the resolved `[[links]]`, deduped per (from,to). */\nexport function buildWikiGraph(pages: readonly WikiPageContent[], entries: readonly WikiPageEntry[]): WikiGraph {\n const fileSlugs = new Set(pages.map((page) => page.slug));\n const { titleBySlug, slugByTitle } = buildTitleMaps(entries);\n const nodes: WikiGraphNode[] = pages.map((page) => ({ slug: page.slug, title: titleBySlug.get(page.slug) ?? page.slug }));\n const edges: WikiGraphEdge[] = [];\n const seen = new Set<string>();\n for (const page of pages) {\n for (const toSlug of pageOutgoingSlugs(page.slug, page.content, fileSlugs, slugByTitle)) {\n // Newline cannot appear in a wiki slug, so it is a safe pair key.\n const key = [page.slug, toSlug].join(\"\\n\");\n if (seen.has(key)) continue;\n seen.add(key);\n edges.push({ from: page.slug, to: toSlug });\n }\n }\n return { nodes, edges };\n}\n\n/** Pages that link TO `slug` (incoming edges), deduped, as nodes. */\nexport function incomingLinks(graph: WikiGraph, slug: string): WikiGraphNode[] {\n const nodeBySlug = new Map(graph.nodes.map((node) => [node.slug, node]));\n const result: WikiGraphNode[] = [];\n const seen = new Set<string>();\n for (const edge of graph.edges) {\n if (edge.to !== slug || seen.has(edge.from)) continue;\n seen.add(edge.from);\n const node = nodeBySlug.get(edge.from);\n if (node) result.push(node);\n }\n return result;\n}\n"],"mappings":"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"}
@@ -1 +0,0 @@
1
- {"version":3,"file":"graph-o5FZZbXf.js","names":[],"sources":["../src/wiki/link.ts","../src/wiki/index-parse.ts","../src/wiki/lint.ts","../src/wiki/graph.ts"],"sourcesContent":["// Pure helpers for `[[wiki-link]]` syntax. Used by the lint\n// (`server/api/routes/wiki.ts`), the page resolver, and the\n// frontend renderer (`src/plugins/wiki/helpers.ts`) — before this\n// module the three sites each had their own implementation and\n// none of them handled the `[[target|display]]` form, producing\n// false-positive \"broken link\" warnings on slug-aliased links\n// (issue #1297).\n//\n// All functions are pure string operations; safe to import from\n// browser bundles. No `node:*` imports anywhere in this file.\n\n/** Inline `[[...]]` link pattern. The capture group is the raw\n * text between the brackets — the caller is expected to feed it\n * through `parseWikiLink` to split off any `|display` suffix.\n *\n * Body length is capped at 200 chars to keep the regex linear:\n * catastrophic backtracking isn't possible (no nested\n * alternation), but a malicious page with thousands of\n * unmatched `[[` could still pin the CPU. 200 is well above the\n * longest legitimate title we've seen and matches the cap the\n * server-side `WIKI_LINK_PATTERN` constant has carried since the\n * original implementation in #951. */\nexport const WIKI_LINK_PATTERN = /\\[\\[([^\\][\\r\\n]{1,200})\\]\\]/g;\n\n/** The structural shape of an inline `[[target|display]]` link.\n * `target` is what the resolver / lint compares against page\n * slugs; `display` is what the renderer shows to the user.\n * When the user writes `[[foo]]` (no pipe), both fields hold\n * `foo`. */\nexport interface WikiLink {\n target: string;\n display: string;\n}\n\n/** Split the raw text inside `[[...]]` into target + display.\n *\n * - `[[foo|Bar Baz]]` → `{ target: \"foo\", display: \"Bar Baz\" }`\n * - `[[foo]]` → `{ target: \"foo\", display: \"foo\" }`\n * - `[[|empty target]]` → `{ target: \"\", display: \"empty target\" }`\n * (caller decides whether an empty target is meaningful;\n * typically lint flags it, renderer shows the display text raw)\n * - `[[foo|]]` → `{ target: \"foo\", display: \"\" }`\n * (same — caller decides)\n *\n * Whitespace around the pipe is preserved on the display side\n * (so the user can write `[[foo| ある記事 ]]` and get the\n * spacing intentionally) but TRIMMED on the target side, since\n * a slug-comparable target with leading/trailing whitespace is\n * always a typo.\n *\n * Only the FIRST pipe splits. `[[a|b|c]]` becomes\n * `{ target: \"a\", display: \"b|c\" }` so display strings can\n * legitimately contain a literal `|`. */\nexport function parseWikiLink(inner: string): WikiLink {\n const pipeIdx = inner.indexOf(\"|\");\n if (pipeIdx === -1) {\n return { target: inner, display: inner };\n }\n const target = inner.slice(0, pipeIdx).trim();\n const display = inner.slice(pipeIdx + 1);\n return { target, display };\n}\n","// Pure parsers for `data/wiki/index.md` content. Used by:\n// - server/api/routes/wiki.ts — page resolver + lint\n// - frontend lint preview (future) — same parser, no fork\n//\n// All exports here are pure string ops; no `node:*` imports allowed.\n// If a future caller needs to walk the filesystem, that wrapper goes\n// under `@mulmoclaude/core/wiki/server` (today only `server/paths.ts`\n// touches `node:path`; nothing here touches disk).\n\nimport { parseWikiLink } from \"./link.js\";\nimport { wikiSlugify } from \"./slug.js\";\n\nexport interface WikiPageEntry {\n title: string;\n slug: string;\n description: string;\n tags: string[];\n}\n\n// ── Patterns ──────────────────────────────────────────────────\n// Bullet patterns live here (not server/utils/regex.ts) because the\n// index parser is the only consumer and the regexes are part of the\n// public surface that callers may want to reason about in tests.\n// Linear in line length: every `[^x]+` runs over a fixed exclusion\n// set with a hard delimiter; the optional summary group's `(.*)` is\n// a single greedy run with no nested overlap.\n\n// eslint-disable-next-line security/detect-unsafe-regex -- bullet-link parser; bounded captures with hard delimiters\nexport const BULLET_LINK_PATTERN = /^[-*]\\s+\\[([^\\]]+)\\]\\(([^)]*)\\)(?:\\s*[—–-]\\s*(.*))?/;\n// eslint-disable-next-line security/detect-unsafe-regex -- same shape as BULLET_LINK_PATTERN\nexport const BULLET_WIKI_LINK_PATTERN = /^[-*]\\s+\\[\\[([^\\]]+)\\]\\](?:\\s*[—–-]\\s*(.*))?/;\n\nconst TABLE_SEPARATOR_PATTERN = /^\\|[\\s|:-]+\\|$/;\n\n// Unicode-aware tag body: any letter or number in any script (so\n// Japanese / Chinese / Korean tags like `#クラウド` or `#可視化` work),\n// plus `-` and `_` as internal joiners. First char is a letter or\n// number only — no leading punctuation.\nconst HASHTAG_PATTERN = /(?:^|\\s)#([\\p{L}\\p{N}][\\p{L}\\p{N}_-]*)/gu;\n\n// ── Tag extraction ────────────────────────────────────────────\n\n/** Extract `#tag` tokens from a bullet description, returning the\n * stripped description and a sorted, deduped, lowercased tag list.\n * Only matches at word boundaries so mid-word `#` (e.g. anchor\n * URLs) is left alone. */\nexport function extractHashTags(text: string): { description: string; tags: string[] } {\n const tags: string[] = [];\n HASHTAG_PATTERN.lastIndex = 0;\n let match: RegExpExecArray | null;\n while ((match = HASHTAG_PATTERN.exec(text)) !== null) {\n tags.push(match[1].toLowerCase());\n }\n const description = text.replace(HASHTAG_PATTERN, \"\").replace(/\\s+/g, \" \").trim();\n const deduped = [...new Set(tags)].sort();\n return { description, tags: deduped };\n}\n\n/** Split a table Tags cell — tolerates comma, whitespace, or `#`\n * prefixes. Empty cell yields an empty list. */\nexport function parseTagsCell(cell: string): string[] {\n const tokens = cell\n .split(/[,\\s]+/)\n .map((token) => token.trim().replace(/^#/, \"\").toLowerCase())\n .filter((token) => token.length > 0);\n return [...new Set(tokens)].sort();\n}\n\n// ── Table parser ──────────────────────────────────────────────\n\n/** Map header cell names → column indices, case- and whitespace-\n * tolerant. Used by the row parser to locate the Tags column (and\n * any other named column) without assuming a fixed position, so\n * older 3- and 4-column tables keep working. */\nexport function buildTableColumnMap(headerRow: string): Map<string, number> {\n const cells = headerRow\n .split(\"|\")\n .slice(1, -1)\n // Mirror parseTableRow's cell-normalising: strip the surrounding\n // backticks that commonly wrap cell values in wiki tables.\n // Without this, a `| \\`tags\\` |` header maps to the key \"`tags`\"\n // and the subsequent `columnMap.get(\"tags\")` lookup silently\n // misses the column, falling back to `tags: []`.\n .map((cell) => cell.trim().replace(/^`|`$/g, \"\").toLowerCase());\n const map = new Map<string, number>();\n cells.forEach((cell, i) => {\n if (cell) map.set(cell, i);\n });\n return map;\n}\n\ninterface TableColumnIndices {\n slug: number;\n title: number;\n summary: number;\n /** Undefined when the table has no `tags` column — caller skips\n * the tags lookup entirely and the row gets `tags: []`. */\n tags: number | undefined;\n}\n\n/** Resolve the per-column indices the row parser needs. Falls back\n * to positional defaults (0/1/2) when the table has no header map.\n * \"summary\" is the canonical column name; \"description\" is accepted\n * as a legacy alias used by older fixtures. */\nfunction resolveTableColumnIndices(columnMap: Map<string, number> | null): TableColumnIndices {\n return {\n slug: columnMap?.get(\"slug\") ?? 0,\n title: columnMap?.get(\"title\") ?? 1,\n summary: columnMap?.get(\"summary\") ?? columnMap?.get(\"description\") ?? 2,\n tags: columnMap?.get(\"tags\"),\n };\n}\n\nfunction parseTableRow(trimmed: string, columnMap: Map<string, number> | null): WikiPageEntry | null {\n const cols = trimmed\n .split(\"|\")\n .slice(1, -1)\n .map((column) => column.trim().replace(/^`|`$/g, \"\"));\n if (cols.length < 2) return null;\n\n const idx = resolveTableColumnIndices(columnMap);\n const slug = cols[idx.slug] ?? \"\";\n const title = cols[idx.title] || slug;\n if (!slug || !title) return null;\n\n const description = cols[idx.summary] ?? \"\";\n const tags = idx.tags !== undefined ? parseTagsCell(cols[idx.tags] ?? \"\") : [];\n return { title, slug, description, tags };\n}\n\n// ── Bullet-row parsers ────────────────────────────────────────\n\n/** Extract the slug segment from a bullet link's href. Accepts the\n * canonical `pages/<slug>.md`, a bare `<slug>.md`, or just `<slug>`\n * — the three forms produced by different historical writers of\n * index.md. Returns \"\" for hrefs that don't look like a wiki page\n * reference (e.g. `https://example.com`) so the caller can fall\n * back to title-based slugification. */\nexport function extractSlugFromBulletHref(rawHref: string): string {\n const href = rawHref.trim();\n if (!href) return \"\";\n if (/^[a-z]+:\\/\\//i.test(href)) return \"\";\n const lastSegment = href.split(\"/\").pop() ?? href;\n return lastSegment.replace(/\\.md$/i, \"\");\n}\n\nfunction parseBulletLinkRow(trimmed: string): WikiPageEntry | null {\n const match = BULLET_LINK_PATTERN.exec(trimmed);\n if (!match) return null;\n const title = match[1].trim();\n const href = match[2] ?? \"\";\n const raw = match[3]?.trim() ?? \"\";\n const { description, tags } = extractHashTags(raw);\n // Prefer the slug embedded in the href so non-ASCII titles keep\n // a navigable slug. Fall back to slugifying the title only when\n // the href has no recognisable slug (rare — usually means the\n // author put an external URL here).\n const slug = extractSlugFromBulletHref(href) || wikiSlugify(title);\n return { title, slug, description, tags };\n}\n\nfunction parseBulletWikiLinkRow(trimmed: string): WikiPageEntry | null {\n const match = BULLET_WIKI_LINK_PATTERN.exec(trimmed);\n if (!match) return null;\n // Split `[[target|display]]` so the bullet entry's slug derives\n // from the TARGET (which is already a slug-shaped identifier\n // when the author wrote the alias form) and the title shows\n // the DISPLAY half. Pre-#1297 the parser slugified the full\n // `target|display` body, which collapses `|` to \"\" and\n // produces a wrong slug — Codex review on PR #1312.\n const { target, display } = parseWikiLink(match[1]);\n const title = (display || target).trim();\n const slug = target ? wikiSlugify(target) : wikiSlugify(title);\n const raw = match[2]?.trim() ?? \"\";\n const { description, tags } = extractHashTags(raw);\n return { title, slug, description, tags };\n}\n\n// ── Top-level parser ──────────────────────────────────────────\n\n/** Parse entries from index.md. Supports three formats:\n * 1. Table: `| slug | Title | Summary | (Tags) |`\n * 2. Bullet link: `- [Title](pages/slug.md) — description`\n * 3. Wiki link: `- [[Title]] — description`\n *\n * Returns entries in source order. Any unrecognised line is\n * silently skipped — index.md is freeform markdown otherwise. */\nexport function parseIndexEntries(content: string): WikiPageEntry[] {\n const entries: WikiPageEntry[] = [];\n let inTable = false;\n let columnMap: Map<string, number> | null = null;\n\n for (const line of content.split(\"\\n\")) {\n const trimmed = line.trim();\n\n if (trimmed.startsWith(\"|\")) {\n if (TABLE_SEPARATOR_PATTERN.test(trimmed)) {\n inTable = true;\n continue;\n }\n if (!inTable) {\n // First `|`-line before the separator is the header. Capture\n // the column map so parseTableRow can locate the Tags\n // column (if any) by name rather than position.\n columnMap = buildTableColumnMap(trimmed);\n inTable = true;\n continue;\n }\n const entry = parseTableRow(trimmed, columnMap);\n if (entry) entries.push(entry);\n continue;\n }\n\n inTable = false;\n columnMap = null;\n\n const bullet = parseBulletLinkRow(trimmed) ?? parseBulletWikiLinkRow(trimmed);\n if (bullet) entries.push(bullet);\n }\n return entries;\n}\n","// Pure wiki lint rules.\n//\n// Each rule takes plain inputs (Sets, page-entry arrays, raw file\n// contents) so the whole pipeline can be unit-tested without\n// filesystem fixtures. The Express route at\n// `server/api/routes/wiki.ts` is a thin shell that reads the disk,\n// calls these rules, formats the result. A future frontend \"lint\n// preview before save\" would call the same functions over an\n// in-memory snapshot.\n\nimport type { WikiPageEntry } from \"./index-parse.js\";\nimport { WIKI_LINK_PATTERN, parseWikiLink } from \"./link.js\";\nimport { wikiSlugify } from \"./slug.js\";\n\n/** Files on disk that aren't referenced by index.md. */\nexport function findOrphanPages(fileSlugs: ReadonlySet<string>, indexedSlugs: ReadonlySet<string>): string[] {\n const issues: string[] = [];\n for (const slug of fileSlugs) {\n if (!indexedSlugs.has(slug)) {\n issues.push(`- **Orphan page**: \\`${slug}.md\\` exists but is missing from index.md`);\n }\n }\n return issues;\n}\n\n/** Slugs referenced by index.md that have no corresponding file. */\nexport function findMissingFiles(pageEntries: readonly WikiPageEntry[], fileSlugs: ReadonlySet<string>): string[] {\n const issues: string[] = [];\n for (const entry of pageEntries) {\n if (!fileSlugs.has(entry.slug)) {\n issues.push(`- **Missing file**: index.md references \\`${entry.slug}\\` but the file does not exist`);\n }\n }\n return issues;\n}\n\n/** Walk a page's body for `[[…]]` links and flag any whose\n * resolved slug doesn't exist in the file set.\n *\n * Critically: this routes through `parseWikiLink` so\n * `[[slug|display]]` is split correctly — the lint slugifies the\n * TARGET, not the full bracket body. Pre-#1297 the lint\n * slugified the entire content (`slug|display`), which collapsed\n * to a slug that always missed and produced ~168 false-positive\n * \"broken link\" warnings. */\nexport function findBrokenLinksInPage(fileName: string, content: string, fileSlugs: ReadonlySet<string>): string[] {\n const issues: string[] = [];\n const matches = [...content.matchAll(WIKI_LINK_PATTERN)];\n for (const match of matches) {\n const { target } = parseWikiLink(match[1]);\n const linkSlug = wikiSlugify(target);\n // Empty target is its own diagnostic — `[[|display]]` or\n // `[[]]` slugifies to \"\" and would otherwise be flagged\n // identically to a real broken link. Keep the original raw\n // bracket body in the report so the user can grep their pages\n // for the malformed link.\n if (linkSlug.length === 0) {\n issues.push(`- **Broken link** in \\`${fileName}\\`: [[${match[1]}]] → empty target`);\n continue;\n }\n if (!fileSlugs.has(linkSlug)) {\n issues.push(`- **Broken link** in \\`${fileName}\\`: [[${match[1]}]] → \\`${linkSlug}.md\\` not found`);\n }\n }\n return issues;\n}\n\nfunction formatTagList(tags: readonly string[]): string {\n return `[${[...tags].sort().join(\", \")}]`;\n}\n\n/** Flag any slug whose index.md tags differ from the page's own\n * frontmatter `tags:` field. Comparison is set-based and order-\n * insensitive; both sides are lowercased at parse time. Slugs\n * missing from `frontmatterTagsBySlug` are ignored here — the\n * missing file itself is already reported by `findMissingFiles`. */\nexport function findTagDrift(pageEntries: readonly WikiPageEntry[], frontmatterTagsBySlug: ReadonlyMap<string, readonly string[]>): string[] {\n const issues: string[] = [];\n for (const entry of pageEntries) {\n // Lowercase on lookup — the caller keys the map with\n // lowercased slugs, so a `MyPage.md` filename still matches\n // an `entry.slug` of `mypage` produced by `wikiSlugify` on the\n // wiki-link parser path.\n const pageTags = frontmatterTagsBySlug.get(entry.slug.toLowerCase());\n if (pageTags === undefined) continue;\n const pageSet = new Set(pageTags);\n const indexSet = new Set(entry.tags);\n if (pageSet.size !== indexSet.size || [...pageSet].some((tag) => !indexSet.has(tag))) {\n issues.push(`- **Tag drift**: \\`${entry.slug}.md\\` frontmatter has ${formatTagList(pageTags)} but index.md has ${formatTagList(entry.tags)}`);\n }\n }\n return issues;\n}\n\n/** Render the final markdown report from the accumulated issues\n * list. Empty input yields the \"wiki is healthy\" sentinel so the\n * caller can write the same file every time without branching on\n * presence. */\nexport function formatLintReport(issues: readonly string[]): string {\n if (issues.length === 0) {\n return \"# Wiki Lint Report\\n\\n✓ No issues found. Wiki is healthy.\";\n }\n const noun = `issue${issues.length !== 1 ? \"s\" : \"\"}`;\n return `# Wiki Lint Report\\n\\n${issues.length} ${noun} found:\\n\\n${issues.join(\"\\n\")}`;\n}\n","// Pure builder for the wiki page→page link graph. Used by:\n// - server/api/routes/wiki.ts — the `graph` action endpoint\n// - src/plugins/wiki/View.vue — backlinks (\"linked references\") +\n// the Graph tab\n//\n// All functions are pure string / collection ops; no `node:*` imports,\n// so the frontend bundle can import them directly (same discipline as\n// the sibling `link.ts` / `lint.ts` / `index-parse.ts` modules).\n//\n// NOTE: this is page→page link structure, distinct from the\n// `server/workspace/wiki-backlinks/` module, which appends *session*\n// backlinks (a page → the chat that edited it, #109). Different concept\n// — do not conflate.\n\nimport { WIKI_LINK_PATTERN, parseWikiLink } from \"./link.js\";\nimport { wikiSlugify } from \"./slug.js\";\nimport type { WikiPageEntry } from \"./index-parse.js\";\n\nexport interface WikiGraphNode {\n slug: string;\n title: string;\n}\n\nexport interface WikiGraphEdge {\n from: string;\n to: string;\n}\n\nexport interface WikiGraph {\n nodes: WikiGraphNode[];\n edges: WikiGraphEdge[];\n}\n\n/** One page's raw body plus its canonical (file-derived) slug. */\nexport interface WikiPageContent {\n slug: string;\n content: string;\n}\n\n/** Resolve a raw `[[link]]` target to an existing page slug, or null.\n * Mirrors the route resolver's strategy: slugify first, then fall\n * back to matching an index entry title so non-ASCII targets like\n * `[[さくらインターネット]]` resolve to their ASCII file slug. */\nexport function resolveLinkTarget(target: string, fileSlugs: ReadonlySet<string>, slugByTitle: ReadonlyMap<string, string>): string | null {\n const slug = wikiSlugify(target);\n if (slug.length > 0 && fileSlugs.has(slug)) return slug;\n const byTitle = slugByTitle.get(target.trim());\n if (byTitle !== undefined && fileSlugs.has(byTitle)) return byTitle;\n return null;\n}\n\n/** Resolved, deduped outgoing slugs for one page body. Self-links and\n * links to non-existent pages are dropped (the latter are already a\n * lint \"broken link\", not a graph edge). */\nexport function pageOutgoingSlugs(fromSlug: string, content: string, fileSlugs: ReadonlySet<string>, slugByTitle: ReadonlyMap<string, string>): string[] {\n const out = new Set<string>();\n for (const match of content.matchAll(WIKI_LINK_PATTERN)) {\n const { target } = parseWikiLink(match[1]);\n const resolved = resolveLinkTarget(target, fileSlugs, slugByTitle);\n if (resolved !== null && resolved !== fromSlug) out.add(resolved);\n }\n return [...out];\n}\n\nfunction buildTitleMaps(entries: readonly WikiPageEntry[]): { titleBySlug: Map<string, string>; slugByTitle: Map<string, string> } {\n const titleBySlug = new Map<string, string>();\n const slugByTitle = new Map<string, string>();\n for (const entry of entries) {\n if (!titleBySlug.has(entry.slug)) titleBySlug.set(entry.slug, entry.title);\n if (entry.title.length > 0 && !slugByTitle.has(entry.title)) slugByTitle.set(entry.title, entry.slug);\n }\n return { titleBySlug, slugByTitle };\n}\n\n/** Build the full page→page graph. Nodes are the existing page files\n * (titled from index.md, falling back to the slug for un-indexed\n * pages); edges are the resolved `[[links]]`, deduped per (from,to). */\nexport function buildWikiGraph(pages: readonly WikiPageContent[], entries: readonly WikiPageEntry[]): WikiGraph {\n const fileSlugs = new Set(pages.map((page) => page.slug));\n const { titleBySlug, slugByTitle } = buildTitleMaps(entries);\n const nodes: WikiGraphNode[] = pages.map((page) => ({ slug: page.slug, title: titleBySlug.get(page.slug) ?? page.slug }));\n const edges: WikiGraphEdge[] = [];\n const seen = new Set<string>();\n for (const page of pages) {\n for (const toSlug of pageOutgoingSlugs(page.slug, page.content, fileSlugs, slugByTitle)) {\n // Newline cannot appear in a wiki slug, so it is a safe pair key.\n const key = [page.slug, toSlug].join(\"\\n\");\n if (seen.has(key)) continue;\n seen.add(key);\n edges.push({ from: page.slug, to: toSlug });\n }\n }\n return { nodes, edges };\n}\n\n/** Pages that link TO `slug` (incoming edges), deduped, as nodes. */\nexport function incomingLinks(graph: WikiGraph, slug: string): WikiGraphNode[] {\n const nodeBySlug = new Map(graph.nodes.map((node) => [node.slug, node]));\n const result: WikiGraphNode[] = [];\n const seen = new Set<string>();\n for (const edge of graph.edges) {\n if (edge.to !== slug || seen.has(edge.from)) continue;\n seen.add(edge.from);\n const node = nodeBySlug.get(edge.from);\n if (node) result.push(node);\n }\n return result;\n}\n"],"mappings":"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"}
@@ -1 +0,0 @@
1
- {"version":3,"file":"notifier-ChpY0XrY.js","names":[],"sources":["../src/notifier/types.ts","../src/notifier/store.ts","../src/notifier/validate.ts","../src/notifier/engine.ts"],"sourcesContent":["// Notifier value types. Kept dependency-free (no node, no fs, no\n// pubsub) so the host's API-route layer and any future browser\n// consumer can validate inbound payloads against the same enum\n// constants the engine accepts without pulling in the engine's I/O.\n\n/** Two notification shapes, distinguished by who fires the close call:\n *\n * `fyi` — informational. The host (bell panel) clears it when the\n * user dismisses the row. No deep-link target.\n * `action` — pending obligation. The plugin clears it when the\n * underlying domain state changes (the user paid the tax,\n * viewed the digest, etc.). The bell row navigates to\n * `navigateTarget` on click.\n *\n * The engine reads `lifecycle` only to enforce two publish-time rules\n * (everything downstream — pubsub fan-out, persistence, history — is\n * lifecycle-blind):\n *\n * 1. `action` requires a non-empty `navigateTarget`. Without one,\n * clicking the row does nothing and the entry is a degraded fyi.\n * 2. `action` cannot use `info` severity. A low-priority obligation\n * is incoherent — fyi if it's a ping, `nudge`/`urgent` if it's a\n * real obligation worth a landing page.\n *\n * Both rules are mirrored in the HTTP layer so plugin-runtime callers\n * and HTTP callers hit the same wall. */\nexport const NOTIFIER_LIFECYCLES = [\"fyi\", \"action\"] as const;\nexport type NotifierLifecycle = (typeof NOTIFIER_LIFECYCLES)[number];\n\n/** Severity drives badge color (gray / amber / red, worst-wins) and\n * in a future iteration channel routing. Mostly stored verbatim by\n * the engine; the one engine-visible interaction is the rule that\n * `action` lifecycle cannot pair with `info` severity (see\n * `NotifierLifecycle` above). */\nexport const NOTIFIER_SEVERITIES = [\"info\", \"nudge\", \"urgent\"] as const;\nexport type NotifierSeverity = (typeof NOTIFIER_SEVERITIES)[number];\n\nexport interface NotifierEntry<TPluginData = unknown> {\n /** Engine-assigned UUID. Generated synchronously inside `publish()`\n * so the caller can use it before persistence completes. */\n id: string;\n /** Plugin namespace (e.g. `\"encore\"`, `\"debug__system\"`). The\n * engine never inspects it — used only for `listFor()` filtering\n * and as a UI grouping key. */\n pluginPkg: string;\n severity: NotifierSeverity;\n lifecycle?: NotifierLifecycle;\n title: string;\n body?: string;\n /** Optional in-app deep-link target (relative URL). The bell popup\n * routes here on row click, with `&notificationId=<id>` appended\n * so the landing page can identify which entry to clear. The\n * engine doesn't read this — it's a UI hint stored on the entry. */\n navigateTarget?: string;\n /** Opaque to the engine. Round-trips through JSON unchanged; only\n * the originating plugin's UI knows the shape. */\n pluginData?: TPluginData;\n /** ISO-8601 timestamp set at `publish()` time. */\n createdAt: string;\n}\n\n/** A history entry — a `NotifierEntry` after it has been cleared or\n * cancelled, with the terminal type and timestamp recorded. The\n * bell popup's \"History\" section renders these read-only. */\nexport interface NotifierHistoryEntry<TPluginData = unknown> extends NotifierEntry<TPluginData> {\n terminalType: \"cleared\" | \"cancelled\";\n terminalAt: string;\n}\n\n/** Caller-supplied input for `publish()`. The engine fills in `id`\n * and `createdAt`; everything else flows through verbatim.\n *\n * Two publish-time rules apply to `action` lifecycle (see\n * `NotifierLifecycle`):\n *\n * - `navigateTarget` MUST be a non-empty string.\n * - `severity` MUST NOT be `\"info\"`.\n *\n * Violations cause `publish()` to throw. Currently expressed as\n * runtime validation rather than a discriminated-union type, so the\n * fields below are all individually optional / loose at the\n * type-level. */\nexport interface PublishInput<TPluginData = unknown> {\n pluginPkg: string;\n severity: NotifierSeverity;\n title: string;\n body?: string;\n lifecycle?: NotifierLifecycle;\n navigateTarget?: string;\n pluginData?: TPluginData;\n}\n\n/** On-disk shape of `~/mulmoclaude/data/notifier/active.json`. Holds\n * only entries that haven't been cleared or cancelled — the file is\n * a snapshot, not an event log. */\nexport interface NotifierFile {\n entries: Record<string, NotifierEntry>;\n}\n\n/** On-disk shape of `~/mulmoclaude/data/notifier/history.json`. Array\n * of terminated entries newest-first, capped at `HISTORY_CAP` with\n * FIFO eviction (push at index 0, slice from the tail). */\nexport interface NotifierHistoryFile {\n entries: NotifierHistoryEntry[];\n}\n\n/** History size cap. The bell popup's History section renders this\n * many entries; older ones fall off when new terminations land. */\nexport const HISTORY_CAP = 50;\n\n/** Pub-sub event published on the host's notifier channel after every\n * successful state change. Discriminated union — subscribers switch\n * on `type` to keep TypeScript narrowing the rest of the payload.\n *\n * `updated` carries the post-mutation entry — the receiver swaps\n * the matching `id` in their local active set. Reserved for in-\n * place edits via `updateForPlugin`; no history record is written\n * because the entry is still active, just with refreshed content. */\nexport type NotifierEvent =\n { type: \"published\"; entry: NotifierEntry } | { type: \"cleared\"; id: string } | { type: \"cancelled\"; id: string } | { type: \"updated\"; entry: NotifierEntry };\n","// Low-level file I/O for the notifier. Reads use node:fs directly;\n// writes go through an injected atomic-JSON writer (the host owns the\n// rename-based atomic write so it stays single-sourced with its other\n// writers). Kept separate from `engine.ts` so the path can be\n// overridden in tests without monkey-patching.\n\nimport { promises as fsPromises } from \"node:fs\";\nimport type { NotifierFile, NotifierHistoryFile } from \"./types.js\";\n\n/** Injected atomic JSON writer — the host's `writeJsonAtomic`. */\nexport type WriteJson = (filePath: string, data: unknown) => Promise<void>;\n\nfunction isNotFoundError(err: unknown): boolean {\n return typeof err === \"object\" && err !== null && (err as { code?: unknown }).code === \"ENOENT\";\n}\n\n/** Read the active-entries file. Returns an empty store when the file\n * doesn't exist yet (first ever call on a fresh workspace). Any other\n * read or parse failure throws — the caller has to decide whether to\n * surface or recover, since silently treating \"malformed file\" as\n * \"no entries\" would lose data. */\nexport async function loadActive(filePath: string): Promise<NotifierFile> {\n let text: string;\n try {\n text = await fsPromises.readFile(filePath, \"utf-8\");\n } catch (err) {\n if (isNotFoundError(err)) return { entries: {} };\n throw err;\n }\n const parsed: unknown = JSON.parse(text);\n // `typeof null === \"object\"` and `Array.isArray([])` is also true,\n // so a naive `typeof entries !== \"object\"` check would let\n // `{ entries: null }` and `{ entries: [] }` through, which then\n // crash downstream `engine.get` / `list*` mutations. Reject both\n // shapes here at load time so the failure surfaces as a clear\n // \"malformed file\" error.\n if (typeof parsed !== \"object\" || parsed === null || !(\"entries\" in parsed)) {\n throw new Error(`notifier: malformed active.json at ${filePath}`);\n }\n const { entries } = parsed as { entries: unknown };\n if (typeof entries !== \"object\" || entries === null || Array.isArray(entries)) {\n throw new Error(`notifier: malformed active.json at ${filePath}`);\n }\n return parsed as NotifierFile;\n}\n\n/** Write the active-entries file via the injected atomic writer so a\n * half-written file is never visible to readers. The caller serialises\n * writes (engine.ts queues mutations) — this function makes no\n * concurrency guarantees of its own. */\nexport async function saveActive(writeJson: WriteJson, filePath: string, state: NotifierFile): Promise<void> {\n await writeJson(filePath, state);\n}\n\n/** Read the history file. Empty array on first run. Same parse-error\n * policy as `loadActive`. */\nexport async function loadHistory(filePath: string): Promise<NotifierHistoryFile> {\n let text: string;\n try {\n text = await fsPromises.readFile(filePath, \"utf-8\");\n } catch (err) {\n if (isNotFoundError(err)) return { entries: [] };\n throw err;\n }\n const parsed: unknown = JSON.parse(text);\n if (typeof parsed !== \"object\" || parsed === null || !(\"entries\" in parsed) || !Array.isArray((parsed as { entries: unknown }).entries)) {\n throw new Error(`notifier: malformed history.json at ${filePath}`);\n }\n return parsed as NotifierHistoryFile;\n}\n\nexport async function saveHistory(writeJson: WriteJson, filePath: string, state: NotifierHistoryFile): Promise<void> {\n await writeJson(filePath, state);\n}\n","// Publish-input validation — pure, dependency-free. Shared by\n// `engine.publish` (throws on error) and the host's HTTP route\n// (returns 400 on error). Single source of truth so plugin-runtime\n// callers and HTTP callers can't drift.\n\nimport type { PublishInput } from \"./types.js\";\n\n/** Hard caps on publish-input fields. The engine reads each entry on\n * every list/get call (no in-memory cache), so unbounded fields hurt\n * every reader. Caps chosen to be generous for legitimate UX copy\n * while bounding active.json growth: a notification fundamentally is\n * a short blurb, not a document. */\nexport const NOTIFIER_LIMITS = {\n titleMax: 200,\n bodyMax: 4000,\n navigateTargetMax: 1000,\n pluginDataMaxBytes: 16 * 1024,\n} as const;\n\nfunction validateTitle(title: string): string | null {\n if (typeof title !== \"string\" || title.length === 0) return \"title must be a non-empty string\";\n if (title.length > NOTIFIER_LIMITS.titleMax) return `title exceeds max length of ${NOTIFIER_LIMITS.titleMax} chars`;\n return null;\n}\n\nfunction validateBody(body: string | undefined): string | null {\n if (body === undefined) return null;\n if (body.length > NOTIFIER_LIMITS.bodyMax) return `body exceeds max length of ${NOTIFIER_LIMITS.bodyMax} chars`;\n return null;\n}\n\nfunction validateNavigateTarget(target: string | undefined): string | null {\n if (target === undefined) return null;\n if (target.length === 0) return \"navigateTarget must be a non-empty relative path when set\";\n if (target.length > NOTIFIER_LIMITS.navigateTargetMax) {\n return `navigateTarget exceeds max length of ${NOTIFIER_LIMITS.navigateTargetMax} chars`;\n }\n // Must be a same-origin relative path. Reject schemes\n // (`javascript:`, `https://...`) and scheme-relative URLs\n // (`//evil.com/...`, which an `<a href>` would resolve to the\n // attacker's origin). One leading \"/\" only.\n if (!target.startsWith(\"/\") || target.startsWith(\"//\")) {\n return \"navigateTarget must be a relative path beginning with a single '/' (no scheme, no '//')\";\n }\n return null;\n}\n\nfunction validatePluginData(pluginData: unknown): string | null {\n if (pluginData === undefined) return null;\n let serialized: string | undefined;\n try {\n serialized = JSON.stringify(pluginData);\n } catch (err) {\n return `pluginData is not JSON-serialisable: ${String(err)}`;\n }\n // `JSON.stringify` returns `undefined` for non-serialisable roots\n // (e.g. a bare function or symbol). Treat that as a serialisation\n // failure so it doesn't slip through as an empty-string size.\n if (typeof serialized !== \"string\") return \"pluginData is not JSON-serialisable\";\n if (serialized.length > NOTIFIER_LIMITS.pluginDataMaxBytes) {\n return `pluginData JSON exceeds ${NOTIFIER_LIMITS.pluginDataMaxBytes} bytes`;\n }\n return null;\n}\n\nfunction validateActionCoherence(input: PublishInput): string | null {\n if (input.lifecycle !== \"action\") return null;\n if (input.severity === \"info\") {\n return \"action lifecycle is incompatible with info severity (use fyi for low-priority pings)\";\n }\n if (typeof input.navigateTarget !== \"string\" || input.navigateTarget.length === 0) {\n return \"action lifecycle requires a non-empty navigateTarget\";\n }\n return null;\n}\n\n/** Validate a `PublishInput`. Returns `null` if OK, or a\n * human-readable error string. Order matters — shape/size errors are\n * reported before lifecycle/severity coherence errors so the message\n * the caller sees points at the most fundamental problem first. */\nexport function validatePublishInput(input: PublishInput): string | null {\n return (\n validateTitle(input.title) ??\n validateBody(input.body) ??\n validateNavigateTarget(input.navigateTarget) ??\n validatePluginData(input.pluginData) ??\n validateActionCoherence(input)\n );\n}\n","// Notifier engine — single-process, two-file (active + history),\n// single-channel. Host-agnostic: file paths, the atomic JSON writer,\n// the pub-sub event sink, and the logger are all injected via\n// `configureNotifier` + `setNotifierFilePaths` so MulmoClaude and\n// MulmoTerminal share one notification engine over their own\n// workspaces and pub-sub fabrics.\n//\n// API surface: publish / clear / cancel / get / listFor / listAll /\n// listHistory (+ plugin-scoped variants). Mutations queue through a\n// writing-flag + waiter-queue coordinator so concurrent callers can't\n// race on the atomic write's rename. Reads bypass the queue (rename\n// atomicity makes half-reads impossible) and trade strict\n// linearisability for simpler code: the contract is \"after\n// `await publish(x)` resolves, subsequent reads see x\" — which holds\n// because `publish` awaits the persist before returning.\n//\n// `clear` / `cancel` push to history *before* removing from active.\n// History persistence is best-effort: if it fails, the active write\n// still wins and the failure is logged. Active is the source of\n// truth; history is an audit aid.\n\nimport { randomUUID } from \"node:crypto\";\nimport { loadActive, loadHistory, saveActive, saveHistory, type WriteJson } from \"./store.js\";\nimport { validatePublishInput } from \"./validate.js\";\nimport {\n HISTORY_CAP,\n type NotifierEntry,\n type NotifierEvent,\n type NotifierFile,\n type NotifierHistoryEntry,\n type NotifierSeverity,\n type PublishInput,\n} from \"./types.js\";\n\nexport { NOTIFIER_LIMITS, validatePublishInput } from \"./validate.js\";\n\n// ── Dependency injection ──────────────────────────────────────────\n\n/** Minimal logger the engine needs. The host passes its structured\n * logger; absent one, failures are swallowed (the engine never throws\n * on a fan-out/persist-best-effort path). */\nexport interface NotifierLogger {\n warn: (message: string, data?: Record<string, unknown>) => void;\n error: (message: string, data?: Record<string, unknown>) => void;\n}\n\nexport interface NotifierConfig {\n /** Atomic JSON writer (the host's `writeJsonAtomic`). */\n writeJson: WriteJson;\n /** Fan-out sink — the host binds this to `pubsub.publish(channel, event)`. */\n publishEvent: (event: NotifierEvent) => void;\n /** Optional logger. */\n log?: NotifierLogger;\n}\n\nconst NOOP_LOG: NotifierLogger = { warn: () => {}, error: () => {} };\n\nlet config: NotifierConfig | null = null;\nlet activeFilePath = \"\";\nlet historyFilePath = \"\";\n\nfunction logger(): NotifierLogger {\n return config?.log ?? NOOP_LOG;\n}\n\n/** Wire the engine's I/O deps. Call once at startup, before the first\n * mutation. Does NOT set file paths — those are set independently via\n * `setNotifierFilePaths` so a host can bind production paths at module\n * load and a test can override them without re-supplying the deps. */\nexport function configureNotifier(injected: NotifierConfig): void {\n config = injected;\n}\n\n// ── In-process event listeners ────────────────────────────────────\n//\n// Separate from the socket.io pubsub so server-side adapters (macOS\n// push, future Encore) can react to state changes without going\n// through a websocket round-trip. The host's pubsub is fan-out-only\n// with no server-side subscribe, so this listener registry is the\n// in-process equivalent. Listeners run synchronously inside `emit`,\n// before the pubsub fan-out.\n\ntype NotifierEventListener = (event: NotifierEvent) => void;\nconst listeners: NotifierEventListener[] = [];\n\n/** Register an in-process listener for engine events. Returns an\n * unsubscribe function the caller can use during teardown. */\nexport function onEvent(listener: NotifierEventListener): () => void {\n listeners.push(listener);\n return () => {\n const idx = listeners.indexOf(listener);\n if (idx >= 0) listeners.splice(idx, 1);\n };\n}\n\nfunction emit(event: NotifierEvent): void {\n // In-process fan-out first. Each listener is wrapped: a throwing\n // listener must not poison the rest, and must not propagate out of\n // `processBatch` and strand the still-unsettled waiters (their\n // resolve/reject is called *after* this emit loop). Fan-out is\n // best-effort by contract — losing one subscriber must not lose\n // the write that already committed.\n for (const listener of listeners) {\n try {\n listener(event);\n } catch (err) {\n logger().error(\"in-process listener failed\", { type: event.type, error: String(err) });\n }\n }\n if (!config) {\n logger().warn(\"emit before init\", { type: event.type });\n return;\n }\n try {\n config.publishEvent(event);\n } catch (err) {\n logger().error(\"emit failed\", { type: event.type, error: String(err) });\n }\n}\n\n// ── Write coordinator ─────────────────────────────────────────────\n\n/** A mutation function applied to the in-memory state object during\n * drain. Returns either:\n *\n * - `null` — no state change (e.g., `clear` on an unknown id).\n * The drainer skips the disk write and the emit if every\n * mutation in a batch returned `null`.\n * - `{ event, historyEntry? }` — state changed. The drainer emits\n * the event after the active write succeeds, and prepends\n * `historyEntry` to history (best-effort) when present.\n *\n * Mutations MUST NOT modify state when returning `null`. Violating\n * this invariant produces a write skip with stale on-disk state. */\ntype MutationOutcome = { event: NotifierEvent; historyEntry?: NotifierHistoryEntry } | null;\ntype Mutation = (state: NotifierFile) => MutationOutcome;\n\ninterface Waiter {\n mutate: Mutation;\n resolve: () => void;\n reject: (err: unknown) => void;\n}\n\ntype MutationResult = { ok: true; outcome: MutationOutcome } | { ok: false; error: unknown };\n\nlet writing = false;\nlet waiters: Waiter[] = [];\n\n/** Point the engine at its active/history files. Resets the write\n * queue, so callers must not have in-flight mutations. The host calls\n * this once with the workspace paths; tests call it per-case with temp\n * files. */\nexport function setNotifierFilePaths(paths: { active: string; history: string }): void {\n activeFilePath = paths.active;\n historyFilePath = paths.history;\n writing = false;\n waiters = [];\n}\n\n/** Test-only: clear config + queue so each suite starts clean. */\nexport function resetNotifier(): void {\n config = null;\n activeFilePath = \"\";\n historyFilePath = \"\";\n writing = false;\n waiters = [];\n listeners.length = 0;\n}\n\nfunction requireWriteJson(): WriteJson {\n if (!config) throw new Error(\"notifier: configureNotifier() not called\");\n return config.writeJson;\n}\n\nfunction applyBatchMutations(batch: Waiter[], state: NotifierFile): MutationResult[] {\n return batch.map((waiter) => {\n try {\n return { ok: true, outcome: waiter.mutate(state) };\n } catch (err) {\n return { ok: false, error: err };\n }\n });\n}\n\nfunction collectEvents(results: MutationResult[]): NotifierEvent[] {\n const events: NotifierEvent[] = [];\n for (const result of results) {\n if (result.ok && result.outcome !== null) events.push(result.outcome.event);\n }\n return events;\n}\n\nfunction collectHistoryEntries(results: MutationResult[]): NotifierHistoryEntry[] {\n const entries: NotifierHistoryEntry[] = [];\n for (const result of results) {\n if (result.ok && result.outcome !== null && result.outcome.historyEntry) {\n entries.push(result.outcome.historyEntry);\n }\n }\n return entries;\n}\n\nfunction settleBatch(batch: Waiter[], results: MutationResult[]): void {\n // Resolves come AFTER any emits so subscribers see the event\n // before the caller's `await` returns.\n for (let index = 0; index < batch.length; index += 1) {\n const result = results[index];\n if (result.ok) batch[index].resolve();\n else batch[index].reject(result.error);\n }\n}\n\nfunction rejectBatch(batch: Waiter[], err: unknown): void {\n for (const waiter of batch) waiter.reject(err);\n}\n\nasync function persistHistory(newEntries: NotifierHistoryEntry[]): Promise<void> {\n const existing = await loadHistory(historyFilePath);\n // Newest-first ordering: a batch contains terminations in arrival\n // order; we want the last one to land at index 0 of history.\n const merged = [...newEntries.slice().reverse(), ...existing.entries].slice(0, HISTORY_CAP);\n await saveHistory(requireWriteJson(), historyFilePath, { entries: merged });\n}\n\nasync function processBatch(batch: Waiter[]): Promise<void> {\n let state: NotifierFile;\n try {\n state = await loadActive(activeFilePath);\n } catch (err) {\n logger().error(\"load failed\", { error: String(err) });\n rejectBatch(batch, err);\n return;\n }\n const results = applyBatchMutations(batch, state);\n const events = collectEvents(results);\n const historyEntries = collectHistoryEntries(results);\n\n if (events.length > 0) {\n try {\n await saveActive(requireWriteJson(), activeFilePath, state);\n } catch (err) {\n logger().error(\"active write failed\", { error: String(err) });\n rejectBatch(batch, err);\n return;\n }\n if (historyEntries.length > 0) {\n // Best-effort: active is the source of truth, history is an\n // audit aid. A failed history write is logged but doesn't\n // unwind the active commit.\n try {\n await persistHistory(historyEntries);\n } catch (err) {\n logger().error(\"history write failed\", { error: String(err) });\n }\n }\n for (const event of events) emit(event);\n }\n settleBatch(batch, results);\n}\n\nasync function drain(): Promise<void> {\n writing = true;\n try {\n while (waiters.length > 0) {\n const batch = waiters;\n waiters = [];\n await processBatch(batch);\n }\n } finally {\n writing = false;\n }\n}\n\nfunction enqueue(mutate: Mutation): Promise<void> {\n return new Promise<void>((resolve, reject) => {\n waiters.push({ mutate, resolve, reject });\n if (!writing) void drain();\n });\n}\n\nfunction removeEntry(state: NotifierFile, entryId: string): NotifierFile[\"entries\"] {\n // Object-rest excludes the key without invoking `delete`.\n const { [entryId]: __removed, ...remaining } = state.entries;\n return remaining;\n}\n\nfunction buildHistoryEntry(entry: NotifierEntry, terminalType: \"cleared\" | \"cancelled\"): NotifierHistoryEntry {\n return { ...entry, terminalType, terminalAt: new Date().toISOString() };\n}\n\n// ── Public API ────────────────────────────────────────────────────\n\nexport async function publish<TPluginData = unknown>(input: PublishInput<TPluginData>): Promise<{ id: string }> {\n // Validate at the engine boundary so plugin-runtime callers and\n // HTTP callers hit the same wall.\n const validationError = validatePublishInput(input as PublishInput);\n if (validationError) {\n throw new Error(`notifier.publish: ${validationError}`);\n }\n const entryId = randomUUID();\n const entry: NotifierEntry<TPluginData> = {\n id: entryId,\n pluginPkg: input.pluginPkg,\n severity: input.severity,\n lifecycle: input.lifecycle,\n title: input.title,\n body: input.body,\n navigateTarget: input.navigateTarget,\n pluginData: input.pluginData,\n createdAt: new Date().toISOString(),\n };\n await enqueue((state) => {\n state.entries[entryId] = entry as NotifierEntry;\n return { event: { type: \"published\", entry: entry as NotifierEntry } };\n });\n return { id: entryId };\n}\n\nexport async function clear(entryId: string): Promise<void> {\n await enqueue((state) => {\n const entry = state.entries[entryId];\n if (!entry) return null;\n state.entries = removeEntry(state, entryId);\n return {\n event: { type: \"cleared\", id: entryId },\n historyEntry: buildHistoryEntry(entry, \"cleared\"),\n };\n });\n}\n\nexport async function cancel(entryId: string): Promise<void> {\n await enqueue((state) => {\n const entry = state.entries[entryId];\n if (!entry) return null;\n state.entries = removeEntry(state, entryId);\n return {\n event: { type: \"cancelled\", id: entryId },\n historyEntry: buildHistoryEntry(entry, \"cancelled\"),\n };\n });\n}\n\n/** In-place update for an active entry. Only the fields present on\n * `patch` are rewritten; `id`, `pluginPkg`, `lifecycle`, and\n * `createdAt` stay fixed. Emits a single `\"updated\"` event with the\n * post-mutation entry — no history record is written because the\n * entry is still active, just with refreshed content.\n *\n * No-ops (no throw) when the id is unknown, the entry belongs to a\n * different plugin, or the merged shape would violate\n * `validatePublishInput`. The silent skip matches `clearForPlugin`'s\n * isolation semantics; validation failures are logged for diagnosis. */\nexport async function updateForPlugin<TPluginData = unknown>(\n pluginPkg: string,\n entryId: string,\n patch: {\n severity?: NotifierSeverity;\n title?: string;\n body?: string;\n navigateTarget?: string;\n pluginData?: TPluginData;\n },\n): Promise<void> {\n await enqueue((state) => {\n const entry = state.entries[entryId];\n if (!entry) return null;\n if (entry.pluginPkg !== pluginPkg) return null;\n const next: NotifierEntry = {\n ...entry,\n ...(patch.severity !== undefined ? { severity: patch.severity } : {}),\n ...(patch.title !== undefined ? { title: patch.title } : {}),\n ...(patch.body !== undefined ? { body: patch.body } : {}),\n ...(patch.navigateTarget !== undefined ? { navigateTarget: patch.navigateTarget } : {}),\n ...(patch.pluginData !== undefined ? { pluginData: patch.pluginData } : {}),\n };\n // Re-validate the merged shape so an update can't degrade the\n // entry below publish-time invariants.\n const validationError = validatePublishInput({\n pluginPkg: next.pluginPkg,\n severity: next.severity,\n title: next.title,\n body: next.body,\n lifecycle: next.lifecycle,\n navigateTarget: next.navigateTarget,\n pluginData: next.pluginData,\n });\n if (validationError) {\n logger().warn(\"update rejected by validation\", { entryId, pluginPkg, error: validationError });\n return null;\n }\n state.entries[entryId] = next;\n return { event: { type: \"updated\", entry: next } };\n });\n}\n\n/** Plugin-scoped point lookup. Returns the entry by id, but only if it\n * belongs to the caller's plugin; otherwise undefined. Cross-plugin\n * reads return undefined for isolation — same property as\n * `clearForPlugin` / `updateForPlugin`. */\nexport async function getForPlugin(pluginPkg: string, entryId: string): Promise<NotifierEntry | undefined> {\n const state = await loadActive(activeFilePath);\n const entry = state.entries[entryId];\n if (!entry) return undefined;\n if (entry.pluginPkg !== pluginPkg) return undefined;\n return entry;\n}\n\n/** Plugin-scoped clear. Same as `clear` but no-ops if the entry's\n * `pluginPkg` doesn't match the caller's, so a plugin can't dismiss\n * another plugin's notification by guessing or scraping its id. */\nexport async function clearForPlugin(pluginPkg: string, entryId: string): Promise<void> {\n await enqueue((state) => {\n const entry = state.entries[entryId];\n if (!entry) return null;\n if (entry.pluginPkg !== pluginPkg) return null;\n state.entries = removeEntry(state, entryId);\n return {\n event: { type: \"cleared\", id: entryId },\n historyEntry: buildHistoryEntry(entry, \"cleared\"),\n };\n });\n}\n\nexport async function get(entryId: string): Promise<NotifierEntry | undefined> {\n const state = await loadActive(activeFilePath);\n return state.entries[entryId];\n}\n\nexport async function listFor(pluginPkg: string): Promise<NotifierEntry[]> {\n const state = await loadActive(activeFilePath);\n return Object.values(state.entries).filter((entry) => entry.pluginPkg === pluginPkg);\n}\n\nexport async function listAll(): Promise<NotifierEntry[]> {\n const state = await loadActive(activeFilePath);\n return Object.values(state.entries);\n}\n\nexport async function listHistory(): Promise<NotifierHistoryEntry[]> {\n const state = await loadHistory(historyFilePath);\n return state.entries;\n}\n"],"mappings":"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"}
@@ -1 +0,0 @@
1
- {"version":3,"file":"notifier-bS8IEeLA.cjs","names":[],"sources":["../src/notifier/types.ts","../src/notifier/store.ts","../src/notifier/validate.ts","../src/notifier/engine.ts"],"sourcesContent":["// Notifier value types. Kept dependency-free (no node, no fs, no\n// pubsub) so the host's API-route layer and any future browser\n// consumer can validate inbound payloads against the same enum\n// constants the engine accepts without pulling in the engine's I/O.\n\n/** Two notification shapes, distinguished by who fires the close call:\n *\n * `fyi` — informational. The host (bell panel) clears it when the\n * user dismisses the row. No deep-link target.\n * `action` — pending obligation. The plugin clears it when the\n * underlying domain state changes (the user paid the tax,\n * viewed the digest, etc.). The bell row navigates to\n * `navigateTarget` on click.\n *\n * The engine reads `lifecycle` only to enforce two publish-time rules\n * (everything downstream — pubsub fan-out, persistence, history — is\n * lifecycle-blind):\n *\n * 1. `action` requires a non-empty `navigateTarget`. Without one,\n * clicking the row does nothing and the entry is a degraded fyi.\n * 2. `action` cannot use `info` severity. A low-priority obligation\n * is incoherent — fyi if it's a ping, `nudge`/`urgent` if it's a\n * real obligation worth a landing page.\n *\n * Both rules are mirrored in the HTTP layer so plugin-runtime callers\n * and HTTP callers hit the same wall. */\nexport const NOTIFIER_LIFECYCLES = [\"fyi\", \"action\"] as const;\nexport type NotifierLifecycle = (typeof NOTIFIER_LIFECYCLES)[number];\n\n/** Severity drives badge color (gray / amber / red, worst-wins) and\n * in a future iteration channel routing. Mostly stored verbatim by\n * the engine; the one engine-visible interaction is the rule that\n * `action` lifecycle cannot pair with `info` severity (see\n * `NotifierLifecycle` above). */\nexport const NOTIFIER_SEVERITIES = [\"info\", \"nudge\", \"urgent\"] as const;\nexport type NotifierSeverity = (typeof NOTIFIER_SEVERITIES)[number];\n\nexport interface NotifierEntry<TPluginData = unknown> {\n /** Engine-assigned UUID. Generated synchronously inside `publish()`\n * so the caller can use it before persistence completes. */\n id: string;\n /** Plugin namespace (e.g. `\"encore\"`, `\"debug__system\"`). The\n * engine never inspects it — used only for `listFor()` filtering\n * and as a UI grouping key. */\n pluginPkg: string;\n severity: NotifierSeverity;\n lifecycle?: NotifierLifecycle;\n title: string;\n body?: string;\n /** Optional in-app deep-link target (relative URL). The bell popup\n * routes here on row click, with `&notificationId=<id>` appended\n * so the landing page can identify which entry to clear. The\n * engine doesn't read this — it's a UI hint stored on the entry. */\n navigateTarget?: string;\n /** Opaque to the engine. Round-trips through JSON unchanged; only\n * the originating plugin's UI knows the shape. */\n pluginData?: TPluginData;\n /** ISO-8601 timestamp set at `publish()` time. */\n createdAt: string;\n}\n\n/** A history entry — a `NotifierEntry` after it has been cleared or\n * cancelled, with the terminal type and timestamp recorded. The\n * bell popup's \"History\" section renders these read-only. */\nexport interface NotifierHistoryEntry<TPluginData = unknown> extends NotifierEntry<TPluginData> {\n terminalType: \"cleared\" | \"cancelled\";\n terminalAt: string;\n}\n\n/** Caller-supplied input for `publish()`. The engine fills in `id`\n * and `createdAt`; everything else flows through verbatim.\n *\n * Two publish-time rules apply to `action` lifecycle (see\n * `NotifierLifecycle`):\n *\n * - `navigateTarget` MUST be a non-empty string.\n * - `severity` MUST NOT be `\"info\"`.\n *\n * Violations cause `publish()` to throw. Currently expressed as\n * runtime validation rather than a discriminated-union type, so the\n * fields below are all individually optional / loose at the\n * type-level. */\nexport interface PublishInput<TPluginData = unknown> {\n pluginPkg: string;\n severity: NotifierSeverity;\n title: string;\n body?: string;\n lifecycle?: NotifierLifecycle;\n navigateTarget?: string;\n pluginData?: TPluginData;\n}\n\n/** On-disk shape of `~/mulmoclaude/data/notifier/active.json`. Holds\n * only entries that haven't been cleared or cancelled — the file is\n * a snapshot, not an event log. */\nexport interface NotifierFile {\n entries: Record<string, NotifierEntry>;\n}\n\n/** On-disk shape of `~/mulmoclaude/data/notifier/history.json`. Array\n * of terminated entries newest-first, capped at `HISTORY_CAP` with\n * FIFO eviction (push at index 0, slice from the tail). */\nexport interface NotifierHistoryFile {\n entries: NotifierHistoryEntry[];\n}\n\n/** History size cap. The bell popup's History section renders this\n * many entries; older ones fall off when new terminations land. */\nexport const HISTORY_CAP = 50;\n\n/** Pub-sub event published on the host's notifier channel after every\n * successful state change. Discriminated union — subscribers switch\n * on `type` to keep TypeScript narrowing the rest of the payload.\n *\n * `updated` carries the post-mutation entry — the receiver swaps\n * the matching `id` in their local active set. Reserved for in-\n * place edits via `updateForPlugin`; no history record is written\n * because the entry is still active, just with refreshed content. */\nexport type NotifierEvent =\n { type: \"published\"; entry: NotifierEntry } | { type: \"cleared\"; id: string } | { type: \"cancelled\"; id: string } | { type: \"updated\"; entry: NotifierEntry };\n","// Low-level file I/O for the notifier. Reads use node:fs directly;\n// writes go through an injected atomic-JSON writer (the host owns the\n// rename-based atomic write so it stays single-sourced with its other\n// writers). Kept separate from `engine.ts` so the path can be\n// overridden in tests without monkey-patching.\n\nimport { promises as fsPromises } from \"node:fs\";\nimport type { NotifierFile, NotifierHistoryFile } from \"./types.js\";\n\n/** Injected atomic JSON writer — the host's `writeJsonAtomic`. */\nexport type WriteJson = (filePath: string, data: unknown) => Promise<void>;\n\nfunction isNotFoundError(err: unknown): boolean {\n return typeof err === \"object\" && err !== null && (err as { code?: unknown }).code === \"ENOENT\";\n}\n\n/** Read the active-entries file. Returns an empty store when the file\n * doesn't exist yet (first ever call on a fresh workspace). Any other\n * read or parse failure throws — the caller has to decide whether to\n * surface or recover, since silently treating \"malformed file\" as\n * \"no entries\" would lose data. */\nexport async function loadActive(filePath: string): Promise<NotifierFile> {\n let text: string;\n try {\n text = await fsPromises.readFile(filePath, \"utf-8\");\n } catch (err) {\n if (isNotFoundError(err)) return { entries: {} };\n throw err;\n }\n const parsed: unknown = JSON.parse(text);\n // `typeof null === \"object\"` and `Array.isArray([])` is also true,\n // so a naive `typeof entries !== \"object\"` check would let\n // `{ entries: null }` and `{ entries: [] }` through, which then\n // crash downstream `engine.get` / `list*` mutations. Reject both\n // shapes here at load time so the failure surfaces as a clear\n // \"malformed file\" error.\n if (typeof parsed !== \"object\" || parsed === null || !(\"entries\" in parsed)) {\n throw new Error(`notifier: malformed active.json at ${filePath}`);\n }\n const { entries } = parsed as { entries: unknown };\n if (typeof entries !== \"object\" || entries === null || Array.isArray(entries)) {\n throw new Error(`notifier: malformed active.json at ${filePath}`);\n }\n return parsed as NotifierFile;\n}\n\n/** Write the active-entries file via the injected atomic writer so a\n * half-written file is never visible to readers. The caller serialises\n * writes (engine.ts queues mutations) — this function makes no\n * concurrency guarantees of its own. */\nexport async function saveActive(writeJson: WriteJson, filePath: string, state: NotifierFile): Promise<void> {\n await writeJson(filePath, state);\n}\n\n/** Read the history file. Empty array on first run. Same parse-error\n * policy as `loadActive`. */\nexport async function loadHistory(filePath: string): Promise<NotifierHistoryFile> {\n let text: string;\n try {\n text = await fsPromises.readFile(filePath, \"utf-8\");\n } catch (err) {\n if (isNotFoundError(err)) return { entries: [] };\n throw err;\n }\n const parsed: unknown = JSON.parse(text);\n if (typeof parsed !== \"object\" || parsed === null || !(\"entries\" in parsed) || !Array.isArray((parsed as { entries: unknown }).entries)) {\n throw new Error(`notifier: malformed history.json at ${filePath}`);\n }\n return parsed as NotifierHistoryFile;\n}\n\nexport async function saveHistory(writeJson: WriteJson, filePath: string, state: NotifierHistoryFile): Promise<void> {\n await writeJson(filePath, state);\n}\n","// Publish-input validation — pure, dependency-free. Shared by\n// `engine.publish` (throws on error) and the host's HTTP route\n// (returns 400 on error). Single source of truth so plugin-runtime\n// callers and HTTP callers can't drift.\n\nimport type { PublishInput } from \"./types.js\";\n\n/** Hard caps on publish-input fields. The engine reads each entry on\n * every list/get call (no in-memory cache), so unbounded fields hurt\n * every reader. Caps chosen to be generous for legitimate UX copy\n * while bounding active.json growth: a notification fundamentally is\n * a short blurb, not a document. */\nexport const NOTIFIER_LIMITS = {\n titleMax: 200,\n bodyMax: 4000,\n navigateTargetMax: 1000,\n pluginDataMaxBytes: 16 * 1024,\n} as const;\n\nfunction validateTitle(title: string): string | null {\n if (typeof title !== \"string\" || title.length === 0) return \"title must be a non-empty string\";\n if (title.length > NOTIFIER_LIMITS.titleMax) return `title exceeds max length of ${NOTIFIER_LIMITS.titleMax} chars`;\n return null;\n}\n\nfunction validateBody(body: string | undefined): string | null {\n if (body === undefined) return null;\n if (body.length > NOTIFIER_LIMITS.bodyMax) return `body exceeds max length of ${NOTIFIER_LIMITS.bodyMax} chars`;\n return null;\n}\n\nfunction validateNavigateTarget(target: string | undefined): string | null {\n if (target === undefined) return null;\n if (target.length === 0) return \"navigateTarget must be a non-empty relative path when set\";\n if (target.length > NOTIFIER_LIMITS.navigateTargetMax) {\n return `navigateTarget exceeds max length of ${NOTIFIER_LIMITS.navigateTargetMax} chars`;\n }\n // Must be a same-origin relative path. Reject schemes\n // (`javascript:`, `https://...`) and scheme-relative URLs\n // (`//evil.com/...`, which an `<a href>` would resolve to the\n // attacker's origin). One leading \"/\" only.\n if (!target.startsWith(\"/\") || target.startsWith(\"//\")) {\n return \"navigateTarget must be a relative path beginning with a single '/' (no scheme, no '//')\";\n }\n return null;\n}\n\nfunction validatePluginData(pluginData: unknown): string | null {\n if (pluginData === undefined) return null;\n let serialized: string | undefined;\n try {\n serialized = JSON.stringify(pluginData);\n } catch (err) {\n return `pluginData is not JSON-serialisable: ${String(err)}`;\n }\n // `JSON.stringify` returns `undefined` for non-serialisable roots\n // (e.g. a bare function or symbol). Treat that as a serialisation\n // failure so it doesn't slip through as an empty-string size.\n if (typeof serialized !== \"string\") return \"pluginData is not JSON-serialisable\";\n if (serialized.length > NOTIFIER_LIMITS.pluginDataMaxBytes) {\n return `pluginData JSON exceeds ${NOTIFIER_LIMITS.pluginDataMaxBytes} bytes`;\n }\n return null;\n}\n\nfunction validateActionCoherence(input: PublishInput): string | null {\n if (input.lifecycle !== \"action\") return null;\n if (input.severity === \"info\") {\n return \"action lifecycle is incompatible with info severity (use fyi for low-priority pings)\";\n }\n if (typeof input.navigateTarget !== \"string\" || input.navigateTarget.length === 0) {\n return \"action lifecycle requires a non-empty navigateTarget\";\n }\n return null;\n}\n\n/** Validate a `PublishInput`. Returns `null` if OK, or a\n * human-readable error string. Order matters — shape/size errors are\n * reported before lifecycle/severity coherence errors so the message\n * the caller sees points at the most fundamental problem first. */\nexport function validatePublishInput(input: PublishInput): string | null {\n return (\n validateTitle(input.title) ??\n validateBody(input.body) ??\n validateNavigateTarget(input.navigateTarget) ??\n validatePluginData(input.pluginData) ??\n validateActionCoherence(input)\n );\n}\n","// Notifier engine — single-process, two-file (active + history),\n// single-channel. Host-agnostic: file paths, the atomic JSON writer,\n// the pub-sub event sink, and the logger are all injected via\n// `configureNotifier` + `setNotifierFilePaths` so MulmoClaude and\n// MulmoTerminal share one notification engine over their own\n// workspaces and pub-sub fabrics.\n//\n// API surface: publish / clear / cancel / get / listFor / listAll /\n// listHistory (+ plugin-scoped variants). Mutations queue through a\n// writing-flag + waiter-queue coordinator so concurrent callers can't\n// race on the atomic write's rename. Reads bypass the queue (rename\n// atomicity makes half-reads impossible) and trade strict\n// linearisability for simpler code: the contract is \"after\n// `await publish(x)` resolves, subsequent reads see x\" — which holds\n// because `publish` awaits the persist before returning.\n//\n// `clear` / `cancel` push to history *before* removing from active.\n// History persistence is best-effort: if it fails, the active write\n// still wins and the failure is logged. Active is the source of\n// truth; history is an audit aid.\n\nimport { randomUUID } from \"node:crypto\";\nimport { loadActive, loadHistory, saveActive, saveHistory, type WriteJson } from \"./store.js\";\nimport { validatePublishInput } from \"./validate.js\";\nimport {\n HISTORY_CAP,\n type NotifierEntry,\n type NotifierEvent,\n type NotifierFile,\n type NotifierHistoryEntry,\n type NotifierSeverity,\n type PublishInput,\n} from \"./types.js\";\n\nexport { NOTIFIER_LIMITS, validatePublishInput } from \"./validate.js\";\n\n// ── Dependency injection ──────────────────────────────────────────\n\n/** Minimal logger the engine needs. The host passes its structured\n * logger; absent one, failures are swallowed (the engine never throws\n * on a fan-out/persist-best-effort path). */\nexport interface NotifierLogger {\n warn: (message: string, data?: Record<string, unknown>) => void;\n error: (message: string, data?: Record<string, unknown>) => void;\n}\n\nexport interface NotifierConfig {\n /** Atomic JSON writer (the host's `writeJsonAtomic`). */\n writeJson: WriteJson;\n /** Fan-out sink — the host binds this to `pubsub.publish(channel, event)`. */\n publishEvent: (event: NotifierEvent) => void;\n /** Optional logger. */\n log?: NotifierLogger;\n}\n\nconst NOOP_LOG: NotifierLogger = { warn: () => {}, error: () => {} };\n\nlet config: NotifierConfig | null = null;\nlet activeFilePath = \"\";\nlet historyFilePath = \"\";\n\nfunction logger(): NotifierLogger {\n return config?.log ?? NOOP_LOG;\n}\n\n/** Wire the engine's I/O deps. Call once at startup, before the first\n * mutation. Does NOT set file paths — those are set independently via\n * `setNotifierFilePaths` so a host can bind production paths at module\n * load and a test can override them without re-supplying the deps. */\nexport function configureNotifier(injected: NotifierConfig): void {\n config = injected;\n}\n\n// ── In-process event listeners ────────────────────────────────────\n//\n// Separate from the socket.io pubsub so server-side adapters (macOS\n// push, future Encore) can react to state changes without going\n// through a websocket round-trip. The host's pubsub is fan-out-only\n// with no server-side subscribe, so this listener registry is the\n// in-process equivalent. Listeners run synchronously inside `emit`,\n// before the pubsub fan-out.\n\ntype NotifierEventListener = (event: NotifierEvent) => void;\nconst listeners: NotifierEventListener[] = [];\n\n/** Register an in-process listener for engine events. Returns an\n * unsubscribe function the caller can use during teardown. */\nexport function onEvent(listener: NotifierEventListener): () => void {\n listeners.push(listener);\n return () => {\n const idx = listeners.indexOf(listener);\n if (idx >= 0) listeners.splice(idx, 1);\n };\n}\n\nfunction emit(event: NotifierEvent): void {\n // In-process fan-out first. Each listener is wrapped: a throwing\n // listener must not poison the rest, and must not propagate out of\n // `processBatch` and strand the still-unsettled waiters (their\n // resolve/reject is called *after* this emit loop). Fan-out is\n // best-effort by contract — losing one subscriber must not lose\n // the write that already committed.\n for (const listener of listeners) {\n try {\n listener(event);\n } catch (err) {\n logger().error(\"in-process listener failed\", { type: event.type, error: String(err) });\n }\n }\n if (!config) {\n logger().warn(\"emit before init\", { type: event.type });\n return;\n }\n try {\n config.publishEvent(event);\n } catch (err) {\n logger().error(\"emit failed\", { type: event.type, error: String(err) });\n }\n}\n\n// ── Write coordinator ─────────────────────────────────────────────\n\n/** A mutation function applied to the in-memory state object during\n * drain. Returns either:\n *\n * - `null` — no state change (e.g., `clear` on an unknown id).\n * The drainer skips the disk write and the emit if every\n * mutation in a batch returned `null`.\n * - `{ event, historyEntry? }` — state changed. The drainer emits\n * the event after the active write succeeds, and prepends\n * `historyEntry` to history (best-effort) when present.\n *\n * Mutations MUST NOT modify state when returning `null`. Violating\n * this invariant produces a write skip with stale on-disk state. */\ntype MutationOutcome = { event: NotifierEvent; historyEntry?: NotifierHistoryEntry } | null;\ntype Mutation = (state: NotifierFile) => MutationOutcome;\n\ninterface Waiter {\n mutate: Mutation;\n resolve: () => void;\n reject: (err: unknown) => void;\n}\n\ntype MutationResult = { ok: true; outcome: MutationOutcome } | { ok: false; error: unknown };\n\nlet writing = false;\nlet waiters: Waiter[] = [];\n\n/** Point the engine at its active/history files. Resets the write\n * queue, so callers must not have in-flight mutations. The host calls\n * this once with the workspace paths; tests call it per-case with temp\n * files. */\nexport function setNotifierFilePaths(paths: { active: string; history: string }): void {\n activeFilePath = paths.active;\n historyFilePath = paths.history;\n writing = false;\n waiters = [];\n}\n\n/** Test-only: clear config + queue so each suite starts clean. */\nexport function resetNotifier(): void {\n config = null;\n activeFilePath = \"\";\n historyFilePath = \"\";\n writing = false;\n waiters = [];\n listeners.length = 0;\n}\n\nfunction requireWriteJson(): WriteJson {\n if (!config) throw new Error(\"notifier: configureNotifier() not called\");\n return config.writeJson;\n}\n\nfunction applyBatchMutations(batch: Waiter[], state: NotifierFile): MutationResult[] {\n return batch.map((waiter) => {\n try {\n return { ok: true, outcome: waiter.mutate(state) };\n } catch (err) {\n return { ok: false, error: err };\n }\n });\n}\n\nfunction collectEvents(results: MutationResult[]): NotifierEvent[] {\n const events: NotifierEvent[] = [];\n for (const result of results) {\n if (result.ok && result.outcome !== null) events.push(result.outcome.event);\n }\n return events;\n}\n\nfunction collectHistoryEntries(results: MutationResult[]): NotifierHistoryEntry[] {\n const entries: NotifierHistoryEntry[] = [];\n for (const result of results) {\n if (result.ok && result.outcome !== null && result.outcome.historyEntry) {\n entries.push(result.outcome.historyEntry);\n }\n }\n return entries;\n}\n\nfunction settleBatch(batch: Waiter[], results: MutationResult[]): void {\n // Resolves come AFTER any emits so subscribers see the event\n // before the caller's `await` returns.\n for (let index = 0; index < batch.length; index += 1) {\n const result = results[index];\n if (result.ok) batch[index].resolve();\n else batch[index].reject(result.error);\n }\n}\n\nfunction rejectBatch(batch: Waiter[], err: unknown): void {\n for (const waiter of batch) waiter.reject(err);\n}\n\nasync function persistHistory(newEntries: NotifierHistoryEntry[]): Promise<void> {\n const existing = await loadHistory(historyFilePath);\n // Newest-first ordering: a batch contains terminations in arrival\n // order; we want the last one to land at index 0 of history.\n const merged = [...newEntries.slice().reverse(), ...existing.entries].slice(0, HISTORY_CAP);\n await saveHistory(requireWriteJson(), historyFilePath, { entries: merged });\n}\n\nasync function processBatch(batch: Waiter[]): Promise<void> {\n let state: NotifierFile;\n try {\n state = await loadActive(activeFilePath);\n } catch (err) {\n logger().error(\"load failed\", { error: String(err) });\n rejectBatch(batch, err);\n return;\n }\n const results = applyBatchMutations(batch, state);\n const events = collectEvents(results);\n const historyEntries = collectHistoryEntries(results);\n\n if (events.length > 0) {\n try {\n await saveActive(requireWriteJson(), activeFilePath, state);\n } catch (err) {\n logger().error(\"active write failed\", { error: String(err) });\n rejectBatch(batch, err);\n return;\n }\n if (historyEntries.length > 0) {\n // Best-effort: active is the source of truth, history is an\n // audit aid. A failed history write is logged but doesn't\n // unwind the active commit.\n try {\n await persistHistory(historyEntries);\n } catch (err) {\n logger().error(\"history write failed\", { error: String(err) });\n }\n }\n for (const event of events) emit(event);\n }\n settleBatch(batch, results);\n}\n\nasync function drain(): Promise<void> {\n writing = true;\n try {\n while (waiters.length > 0) {\n const batch = waiters;\n waiters = [];\n await processBatch(batch);\n }\n } finally {\n writing = false;\n }\n}\n\nfunction enqueue(mutate: Mutation): Promise<void> {\n return new Promise<void>((resolve, reject) => {\n waiters.push({ mutate, resolve, reject });\n if (!writing) void drain();\n });\n}\n\nfunction removeEntry(state: NotifierFile, entryId: string): NotifierFile[\"entries\"] {\n // Object-rest excludes the key without invoking `delete`.\n const { [entryId]: __removed, ...remaining } = state.entries;\n return remaining;\n}\n\nfunction buildHistoryEntry(entry: NotifierEntry, terminalType: \"cleared\" | \"cancelled\"): NotifierHistoryEntry {\n return { ...entry, terminalType, terminalAt: new Date().toISOString() };\n}\n\n// ── Public API ────────────────────────────────────────────────────\n\nexport async function publish<TPluginData = unknown>(input: PublishInput<TPluginData>): Promise<{ id: string }> {\n // Validate at the engine boundary so plugin-runtime callers and\n // HTTP callers hit the same wall.\n const validationError = validatePublishInput(input as PublishInput);\n if (validationError) {\n throw new Error(`notifier.publish: ${validationError}`);\n }\n const entryId = randomUUID();\n const entry: NotifierEntry<TPluginData> = {\n id: entryId,\n pluginPkg: input.pluginPkg,\n severity: input.severity,\n lifecycle: input.lifecycle,\n title: input.title,\n body: input.body,\n navigateTarget: input.navigateTarget,\n pluginData: input.pluginData,\n createdAt: new Date().toISOString(),\n };\n await enqueue((state) => {\n state.entries[entryId] = entry as NotifierEntry;\n return { event: { type: \"published\", entry: entry as NotifierEntry } };\n });\n return { id: entryId };\n}\n\nexport async function clear(entryId: string): Promise<void> {\n await enqueue((state) => {\n const entry = state.entries[entryId];\n if (!entry) return null;\n state.entries = removeEntry(state, entryId);\n return {\n event: { type: \"cleared\", id: entryId },\n historyEntry: buildHistoryEntry(entry, \"cleared\"),\n };\n });\n}\n\nexport async function cancel(entryId: string): Promise<void> {\n await enqueue((state) => {\n const entry = state.entries[entryId];\n if (!entry) return null;\n state.entries = removeEntry(state, entryId);\n return {\n event: { type: \"cancelled\", id: entryId },\n historyEntry: buildHistoryEntry(entry, \"cancelled\"),\n };\n });\n}\n\n/** In-place update for an active entry. Only the fields present on\n * `patch` are rewritten; `id`, `pluginPkg`, `lifecycle`, and\n * `createdAt` stay fixed. Emits a single `\"updated\"` event with the\n * post-mutation entry — no history record is written because the\n * entry is still active, just with refreshed content.\n *\n * No-ops (no throw) when the id is unknown, the entry belongs to a\n * different plugin, or the merged shape would violate\n * `validatePublishInput`. The silent skip matches `clearForPlugin`'s\n * isolation semantics; validation failures are logged for diagnosis. */\nexport async function updateForPlugin<TPluginData = unknown>(\n pluginPkg: string,\n entryId: string,\n patch: {\n severity?: NotifierSeverity;\n title?: string;\n body?: string;\n navigateTarget?: string;\n pluginData?: TPluginData;\n },\n): Promise<void> {\n await enqueue((state) => {\n const entry = state.entries[entryId];\n if (!entry) return null;\n if (entry.pluginPkg !== pluginPkg) return null;\n const next: NotifierEntry = {\n ...entry,\n ...(patch.severity !== undefined ? { severity: patch.severity } : {}),\n ...(patch.title !== undefined ? { title: patch.title } : {}),\n ...(patch.body !== undefined ? { body: patch.body } : {}),\n ...(patch.navigateTarget !== undefined ? { navigateTarget: patch.navigateTarget } : {}),\n ...(patch.pluginData !== undefined ? { pluginData: patch.pluginData } : {}),\n };\n // Re-validate the merged shape so an update can't degrade the\n // entry below publish-time invariants.\n const validationError = validatePublishInput({\n pluginPkg: next.pluginPkg,\n severity: next.severity,\n title: next.title,\n body: next.body,\n lifecycle: next.lifecycle,\n navigateTarget: next.navigateTarget,\n pluginData: next.pluginData,\n });\n if (validationError) {\n logger().warn(\"update rejected by validation\", { entryId, pluginPkg, error: validationError });\n return null;\n }\n state.entries[entryId] = next;\n return { event: { type: \"updated\", entry: next } };\n });\n}\n\n/** Plugin-scoped point lookup. Returns the entry by id, but only if it\n * belongs to the caller's plugin; otherwise undefined. Cross-plugin\n * reads return undefined for isolation — same property as\n * `clearForPlugin` / `updateForPlugin`. */\nexport async function getForPlugin(pluginPkg: string, entryId: string): Promise<NotifierEntry | undefined> {\n const state = await loadActive(activeFilePath);\n const entry = state.entries[entryId];\n if (!entry) return undefined;\n if (entry.pluginPkg !== pluginPkg) return undefined;\n return entry;\n}\n\n/** Plugin-scoped clear. Same as `clear` but no-ops if the entry's\n * `pluginPkg` doesn't match the caller's, so a plugin can't dismiss\n * another plugin's notification by guessing or scraping its id. */\nexport async function clearForPlugin(pluginPkg: string, entryId: string): Promise<void> {\n await enqueue((state) => {\n const entry = state.entries[entryId];\n if (!entry) return null;\n if (entry.pluginPkg !== pluginPkg) return null;\n state.entries = removeEntry(state, entryId);\n return {\n event: { type: \"cleared\", id: entryId },\n historyEntry: buildHistoryEntry(entry, \"cleared\"),\n };\n });\n}\n\nexport async function get(entryId: string): Promise<NotifierEntry | undefined> {\n const state = await loadActive(activeFilePath);\n return state.entries[entryId];\n}\n\nexport async function listFor(pluginPkg: string): Promise<NotifierEntry[]> {\n const state = await loadActive(activeFilePath);\n return Object.values(state.entries).filter((entry) => entry.pluginPkg === pluginPkg);\n}\n\nexport async function listAll(): Promise<NotifierEntry[]> {\n const state = await loadActive(activeFilePath);\n return Object.values(state.entries);\n}\n\nexport async function listHistory(): Promise<NotifierHistoryEntry[]> {\n const state = await loadHistory(historyFilePath);\n return state.entries;\n}\n"],"mappings":"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"}