@mulmoclaude/core 1.0.0 → 1.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (177) hide show
  1. package/assets/helps/collection-skills.md +28 -0
  2. package/assets/helps/custom-view.md +5 -0
  3. package/dist/artifacts/paths.cjs +68 -0
  4. package/dist/artifacts/paths.cjs.map +1 -0
  5. package/dist/artifacts/paths.d.ts +45 -0
  6. package/dist/artifacts/paths.js +62 -0
  7. package/dist/artifacts/paths.js.map +1 -0
  8. package/dist/atomic-C_7YpMiM.cjs +102 -0
  9. package/dist/atomic-C_7YpMiM.cjs.map +1 -0
  10. package/dist/atomic-DPpdrJzO.js +89 -0
  11. package/dist/atomic-DPpdrJzO.js.map +1 -0
  12. package/dist/collection/core/completion.d.ts +21 -0
  13. package/dist/collection/core/ids.d.ts +5 -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 +14 -0
  17. package/dist/collection/core/schemaRules.d.ts +167 -0
  18. package/dist/collection/core/schemaZ.d.ts +490 -10
  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 +109 -29
  22. package/dist/collection/index.cjs.map +1 -1
  23. package/dist/collection/index.d.ts +4 -0
  24. package/dist/collection/index.js +98 -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-BbsJwVEq.js → discovery-DYlEOa9a.js} +427 -393
  43. package/dist/discovery-DYlEOa9a.js.map +1 -0
  44. package/dist/{discovery-Bklck7Ck.cjs → discovery-UxkvBxV-.cjs} +445 -435
  45. package/dist/discovery-UxkvBxV-.cjs.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/host/hostSlot.d.ts +27 -0
  76. package/dist/{ids-DWmHjm17.cjs → ids-BJ7rdrDN.cjs} +17 -1
  77. package/dist/{ids-DWmHjm17.cjs.map → ids-BJ7rdrDN.cjs.map} +1 -1
  78. package/dist/{ids-D1M1T6KJ.js → ids-Bv6AOW9Z.js} +12 -2
  79. package/dist/{ids-D1M1T6KJ.js.map → ids-Bv6AOW9Z.js.map} +1 -1
  80. package/dist/{ingestTypes-DbuQNuK6.cjs → ingestTypes-CkbM-zrO.cjs} +2 -2
  81. package/dist/{ingestTypes-DbuQNuK6.cjs.map → ingestTypes-CkbM-zrO.cjs.map} +1 -1
  82. package/dist/{ingestTypes-Ci4eOgv-.js → ingestTypes-DuCiZqye.js} +2 -2
  83. package/dist/{ingestTypes-Ci4eOgv-.js.map → ingestTypes-DuCiZqye.js.map} +1 -1
  84. package/dist/notifier/engine.d.ts +2 -4
  85. package/dist/notifier/index.cjs +1 -1
  86. package/dist/notifier/index.js +1 -1
  87. package/dist/{notifier-ChpY0XrY.js → notifier-BdA5qzhe.js} +14 -16
  88. package/dist/notifier-BdA5qzhe.js.map +1 -0
  89. package/dist/{notifier-bS8IEeLA.cjs → notifier-tMsAXyXp.cjs} +14 -16
  90. package/dist/notifier-tMsAXyXp.cjs.map +1 -0
  91. package/dist/plugin-vue/fileWatch.d.ts +5 -0
  92. package/dist/plugin-vue/i18n.cjs +45 -0
  93. package/dist/plugin-vue/i18n.cjs.map +1 -0
  94. package/dist/plugin-vue/i18n.js +44 -0
  95. package/dist/plugin-vue/i18n.js.map +1 -0
  96. package/dist/plugin-vue/index.cjs +98 -0
  97. package/dist/plugin-vue/index.cjs.map +1 -0
  98. package/dist/plugin-vue/index.d.ts +5 -0
  99. package/dist/plugin-vue/index.js +91 -0
  100. package/dist/plugin-vue/index.js.map +1 -0
  101. package/dist/plugin-vue/markdownDoc.d.ts +10 -0
  102. package/dist/plugin-vue/pluginI18n.d.ts +22 -0
  103. package/dist/plugin-vue/useClipboardCopy.d.ts +6 -0
  104. package/dist/plugin-vue/useFileWatch.d.ts +8 -0
  105. package/dist/plugin-vue/useMarkdownDoc.d.ts +3 -0
  106. package/dist/{promptSafety-Bugq2kqL.js → promptSafety-BNelBKhh.js} +79 -7
  107. package/dist/promptSafety-BNelBKhh.js.map +1 -0
  108. package/dist/{promptSafety-DbE6eZmP.cjs → promptSafety-QAtADXUF.cjs} +108 -6
  109. package/dist/promptSafety-QAtADXUF.cjs.map +1 -0
  110. package/dist/remote-host/index.cjs.map +1 -1
  111. package/dist/remote-host/index.js.map +1 -1
  112. package/dist/remote-host/server/hostRunner.d.ts +2 -1
  113. package/dist/remote-host/server/index.cjs +6 -5
  114. package/dist/remote-host/server/index.cjs.map +1 -1
  115. package/dist/remote-host/server/index.js +3 -2
  116. package/dist/remote-host/server/index.js.map +1 -1
  117. package/dist/remote-view/index.cjs +1 -1
  118. package/dist/remote-view/index.cjs.map +1 -1
  119. package/dist/remote-view/index.d.ts +1 -1
  120. package/dist/remote-view/index.js +1 -1
  121. package/dist/remote-view/index.js.map +1 -1
  122. package/dist/scheduler/index.cjs +3 -3
  123. package/dist/scheduler/index.cjs.map +1 -1
  124. package/dist/scheduler/index.js +2 -2
  125. package/dist/scheduler/index.js.map +1 -1
  126. package/dist/scheduler/task-manager.d.ts +2 -5
  127. package/dist/{server-8EZggEg7.js → server-CnwgECW-.js} +209 -41
  128. package/dist/server-CnwgECW-.js.map +1 -0
  129. package/dist/{server-BOiz_HDi.cjs → server-DFo3zFYB.cjs} +250 -46
  130. package/dist/server-DFo3zFYB.cjs.map +1 -0
  131. package/dist/skill-bridge/index.cjs +1 -1
  132. package/dist/skill-bridge/index.js +1 -1
  133. package/dist/translation/client.cjs +16 -0
  134. package/dist/translation/client.cjs.map +1 -1
  135. package/dist/translation/client.d.ts +6 -0
  136. package/dist/translation/client.js +16 -1
  137. package/dist/translation/client.js.map +1 -1
  138. package/dist/utils/errors.d.ts +2 -1
  139. package/dist/utils/fetch.cjs +36 -0
  140. package/dist/utils/fetch.cjs.map +1 -0
  141. package/dist/{collection/registry/server → utils}/fetch.d.ts +5 -3
  142. package/dist/utils/fetch.js +34 -0
  143. package/dist/utils/fetch.js.map +1 -0
  144. package/dist/utils/index.cjs +11 -3
  145. package/dist/utils/index.cjs.map +1 -0
  146. package/dist/utils/index.js +9 -1
  147. package/dist/utils/index.js.map +1 -0
  148. package/dist/whisper/index.cjs +9 -9
  149. package/dist/whisper/index.cjs.map +1 -1
  150. package/dist/whisper/index.js +3 -3
  151. package/dist/wiki/index.cjs +3 -16
  152. package/dist/wiki/index.cjs.map +1 -1
  153. package/dist/wiki/index.js +1 -14
  154. package/dist/wiki/index.js.map +1 -1
  155. package/dist/wiki/render.d.ts +2 -4
  156. package/dist/wiki/server/frontmatter.d.ts +2 -6
  157. package/dist/wiki/server/index.cjs +4 -40
  158. package/dist/wiki/server/index.cjs.map +1 -1
  159. package/dist/wiki/server/index.js +2 -38
  160. package/dist/wiki/server/index.js.map +1 -1
  161. package/dist/workspace-setup/index.js +22 -6
  162. package/dist/workspace-setup/index.js.map +1 -1
  163. package/package.json +50 -8
  164. package/dist/collection/server/atomic.d.ts +0 -1
  165. package/dist/discovery-BbsJwVEq.js.map +0 -1
  166. package/dist/discovery-Bklck7Ck.cjs.map +0 -1
  167. package/dist/errors-7P5eMOSX.cjs +0 -30
  168. package/dist/errors-7P5eMOSX.cjs.map +0 -1
  169. package/dist/errors-eid6Mes3.js +0 -19
  170. package/dist/errors-eid6Mes3.js.map +0 -1
  171. package/dist/google/fetch.d.ts +0 -8
  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-8EZggEg7.js.map +0 -1
  177. package/dist/server-BOiz_HDi.cjs.map +0 -1
@@ -1 +1 @@
1
- {"version":3,"file":"client.js","names":[],"sources":["../../src/translation/client.ts"],"sourcesContent":["// @mulmoclaude/core/translation/client — framework-neutral runtime-translation\n// cache. Both hosts (MulmoClaude, MulmoTerminal) translate UI strings (role\n// query chips, collection starters, …) into the user's locale at runtime by\n// POSTing batches to their own `/api/translation` route; the LLM step and the\n// transport/auth differ per host, so BOTH are injected. This module owns only\n// the host-agnostic parts: the request/response contract, in-flight de-dup,\n// length validation, and an in-memory memo keyed by (namespace, locale,\n// sentence-set). No framework dependency — the host wraps this in its own\n// reactivity (Vue refs, …), mirroring `@mulmoclaude/core/whisper/client`.\n//\n// `en` and empty inputs are NOT special-cased here — that's a UI fallback\n// decision the caller makes (it already holds the English source).\n\nexport interface TranslateRequest {\n /** Cache + server-cache partition, e.g. \"role-queries\", \"collection-starters\". */\n namespace: string;\n /** BCP-47 target language, e.g. \"ja\", \"pt-BR\". */\n targetLanguage: string;\n /** English source strings to translate, in order. */\n sentences: readonly string[];\n}\n\nexport interface TranslateResponse {\n /** Translations in the same order and length as `sentences`. */\n translations: string[];\n}\n\n/** Host-injected transport. Performs the POST however the host likes (auth,\n * URL, error shape are the host's concern) and resolves to the response, or\n * `null` on any network / HTTP failure — the cache treats `null` as a miss and\n * the caller falls back to the English source. */\nexport type TranslateTransport = (req: TranslateRequest) => Promise<TranslateResponse | null>;\n\nexport interface TranslationCache {\n /** Memoized translations for this exact request, or `null` if not resolved\n * yet (never fetched, in flight, or the last attempt failed). Synchronous —\n * a reactive caller reads this after being nudged by `fetch`'s resolution. */\n peek: (req: TranslateRequest) => readonly string[] | null;\n /** Ensure a translation is fetched (de-duped across concurrent callers and\n * memoized). Resolves to the translations, or `null` on transport failure or\n * a length mismatch. Repeat calls with the same key return the memoized\n * value without re-hitting the transport. */\n fetch: (req: TranslateRequest) => Promise<readonly string[] | null>;\n /** Drop all memoized + in-flight state. Primarily for tests that share a\n * worker; production callers never need it. */\n clear: () => void;\n}\n\nfunction cacheKey(req: TranslateRequest): string {\n // Join the parts with a NUL separator (written as an escaped NUL so the\n // SOURCE stays plain text — a raw NUL byte here made Git classify the file as\n // binary and blocked diff review). NUL can't occur in any part, so two\n // different (namespace, locale, sentence-set) tuples can never collide. The\n // sentence set is part of the key (not just namespace+locale) so callers\n // needn't supply a stable id — identical inputs share a slot, which is correct.\n return [req.namespace, req.targetLanguage, ...req.sentences].join(\"\\u0000\");\n}\n\nexport function createTranslationCache(transport: TranslateTransport): TranslationCache {\n const memo = new Map<string, readonly string[]>();\n const inflight = new Map<string, Promise<readonly string[] | null>>();\n\n async function run(key: string, req: TranslateRequest): Promise<readonly string[] | null> {\n const response = await transport(req);\n const translations = response?.translations;\n if (!Array.isArray(translations) || translations.length !== req.sentences.length) {\n return null;\n }\n const frozen = Object.freeze([...translations]);\n memo.set(key, frozen);\n return frozen;\n }\n\n return {\n peek(req) {\n return memo.get(cacheKey(req)) ?? null;\n },\n fetch(req) {\n const key = cacheKey(req);\n const hit = memo.get(key);\n if (hit) return Promise.resolve(hit);\n const pending = inflight.get(key);\n if (pending) return pending;\n const started = run(key, req).finally(() => {\n inflight.delete(key);\n });\n inflight.set(key, started);\n return started;\n },\n clear() {\n memo.clear();\n inflight.clear();\n },\n };\n}\n"],"mappings":";AAgDA,SAAS,SAAS,KAA+B;CAO/C,OAAO;EAAC,IAAI;EAAW,IAAI;EAAgB,GAAG,IAAI;CAAS,CAAC,CAAC,KAAK,IAAQ;AAC5E;AAEA,SAAgB,uBAAuB,WAAiD;CACtF,MAAM,uBAAO,IAAI,IAA+B;CAChD,MAAM,2BAAW,IAAI,IAA+C;CAEpE,eAAe,IAAI,KAAa,KAA0D;EAExF,MAAM,gBAAe,MADE,UAAU,GAAG,EAAA,EACL;EAC/B,IAAI,CAAC,MAAM,QAAQ,YAAY,KAAK,aAAa,WAAW,IAAI,UAAU,QACxE,OAAO;EAET,MAAM,SAAS,OAAO,OAAO,CAAC,GAAG,YAAY,CAAC;EAC9C,KAAK,IAAI,KAAK,MAAM;EACpB,OAAO;CACT;CAEA,OAAO;EACL,KAAK,KAAK;GACR,OAAO,KAAK,IAAI,SAAS,GAAG,CAAC,KAAK;EACpC;EACA,MAAM,KAAK;GACT,MAAM,MAAM,SAAS,GAAG;GACxB,MAAM,MAAM,KAAK,IAAI,GAAG;GACxB,IAAI,KAAK,OAAO,QAAQ,QAAQ,GAAG;GACnC,MAAM,UAAU,SAAS,IAAI,GAAG;GAChC,IAAI,SAAS,OAAO;GACpB,MAAM,UAAU,IAAI,KAAK,GAAG,CAAC,CAAC,cAAc;IAC1C,SAAS,OAAO,GAAG;GACrB,CAAC;GACD,SAAS,IAAI,KAAK,OAAO;GACzB,OAAO;EACT;EACA,QAAQ;GACN,KAAK,MAAM;GACX,SAAS,MAAM;EACjB;CACF;AACF"}
1
+ {"version":3,"file":"client.js","names":[],"sources":["../../src/translation/client.ts"],"sourcesContent":["// @mulmoclaude/core/translation/client — framework-neutral runtime-translation\n// cache. Both hosts (MulmoClaude, MulmoTerminal) translate UI strings (role\n// query chips, collection starters, …) into the user's locale at runtime by\n// POSTing batches to their own `/api/translation` route; the LLM step and the\n// transport/auth differ per host, so BOTH are injected. This module owns only\n// the host-agnostic parts: the request/response contract, in-flight de-dup,\n// length validation, and an in-memory memo keyed by (namespace, locale,\n// sentence-set). No framework dependency — the host wraps this in its own\n// reactivity (Vue refs, …), mirroring `@mulmoclaude/core/whisper/client`.\n//\n// `en` and empty inputs are NOT special-cased here — that's a UI fallback\n// decision the caller makes (it already holds the English source).\n\nexport interface TranslateRequest {\n /** Cache + server-cache partition, e.g. \"role-queries\", \"collection-starters\". */\n namespace: string;\n /** BCP-47 target language, e.g. \"ja\", \"pt-BR\". */\n targetLanguage: string;\n /** English source strings to translate, in order. */\n sentences: readonly string[];\n}\n\nexport interface TranslateResponse {\n /** Translations in the same order and length as `sentences`. */\n translations: string[];\n}\n\n/** Host-injected transport. Performs the POST however the host likes (auth,\n * URL, error shape are the host's concern) and resolves to the response, or\n * `null` on any network / HTTP failure — the cache treats `null` as a miss and\n * the caller falls back to the English source. */\nexport type TranslateTransport = (req: TranslateRequest) => Promise<TranslateResponse | null>;\n\nexport interface TranslationCache {\n /** Memoized translations for this exact request, or `null` if not resolved\n * yet (never fetched, in flight, or the last attempt failed). Synchronous —\n * a reactive caller reads this after being nudged by `fetch`'s resolution. */\n peek: (req: TranslateRequest) => readonly string[] | null;\n /** Ensure a translation is fetched (de-duped across concurrent callers and\n * memoized). Resolves to the translations, or `null` on transport failure or\n * a length mismatch. Repeat calls with the same key return the memoized\n * value without re-hitting the transport. */\n fetch: (req: TranslateRequest) => Promise<readonly string[] | null>;\n /** Drop all memoized + in-flight state. Primarily for tests that share a\n * worker; production callers never need it. */\n clear: () => void;\n}\n\nfunction cacheKey(req: TranslateRequest): string {\n // Join the parts with a NUL separator (written as an escaped NUL so the\n // SOURCE stays plain text — a raw NUL byte here made Git classify the file as\n // binary and blocked diff review). NUL can't occur in any part, so two\n // different (namespace, locale, sentence-set) tuples can never collide. The\n // sentence set is part of the key (not just namespace+locale) so callers\n // needn't supply a stable id — identical inputs share a slot, which is correct.\n return [req.namespace, req.targetLanguage, ...req.sentences].join(\"\\u0000\");\n}\n\n/** Resolve `req` through `cache` (peek-then-fetch) and hand the result to\n * `apply`, but only while `isCurrent()` still holds — so a slow response can't\n * overwrite a newer locale/input's translation. A synchronous peek hit skips\n * the guard (the caller just built `req`, so it is current by construction).\n * A rejected fetch is swallowed: the caller keeps its English fallback. */\nexport function loadTranslated(cache: TranslationCache, req: TranslateRequest, isCurrent: () => boolean, apply: (value: readonly string[]) => void): void {\n const hit = cache.peek(req);\n if (hit !== null) {\n apply(hit);\n return;\n }\n cache\n .fetch(req)\n .then((result) => {\n if (result !== null && isCurrent()) apply(result);\n })\n .catch(() => {\n /* transport rejected — keep the English fallback */\n });\n}\n\nexport function createTranslationCache(transport: TranslateTransport): TranslationCache {\n const memo = new Map<string, readonly string[]>();\n const inflight = new Map<string, Promise<readonly string[] | null>>();\n\n async function run(key: string, req: TranslateRequest): Promise<readonly string[] | null> {\n const response = await transport(req);\n const translations = response?.translations;\n if (!Array.isArray(translations) || translations.length !== req.sentences.length) {\n return null;\n }\n const frozen = Object.freeze([...translations]);\n memo.set(key, frozen);\n return frozen;\n }\n\n return {\n peek(req) {\n return memo.get(cacheKey(req)) ?? null;\n },\n fetch(req) {\n const key = cacheKey(req);\n const hit = memo.get(key);\n if (hit) return Promise.resolve(hit);\n const pending = inflight.get(key);\n if (pending) return pending;\n const started = run(key, req).finally(() => {\n inflight.delete(key);\n });\n inflight.set(key, started);\n return started;\n },\n clear() {\n memo.clear();\n inflight.clear();\n },\n };\n}\n"],"mappings":";AAgDA,SAAS,SAAS,KAA+B;CAO/C,OAAO;EAAC,IAAI;EAAW,IAAI;EAAgB,GAAG,IAAI;CAAS,CAAC,CAAC,KAAK,IAAQ;AAC5E;;;;;;AAOA,SAAgB,eAAe,OAAyB,KAAuB,WAA0B,OAAiD;CACxJ,MAAM,MAAM,MAAM,KAAK,GAAG;CAC1B,IAAI,QAAQ,MAAM;EAChB,MAAM,GAAG;EACT;CACF;CACA,MACG,MAAM,GAAG,CAAC,CACV,MAAM,WAAW;EAChB,IAAI,WAAW,QAAQ,UAAU,GAAG,MAAM,MAAM;CAClD,CAAC,CAAC,CACD,YAAY,CAEb,CAAC;AACL;AAEA,SAAgB,uBAAuB,WAAiD;CACtF,MAAM,uBAAO,IAAI,IAA+B;CAChD,MAAM,2BAAW,IAAI,IAA+C;CAEpE,eAAe,IAAI,KAAa,KAA0D;EAExF,MAAM,gBAAe,MADE,UAAU,GAAG,EAAA,EACL;EAC/B,IAAI,CAAC,MAAM,QAAQ,YAAY,KAAK,aAAa,WAAW,IAAI,UAAU,QACxE,OAAO;EAET,MAAM,SAAS,OAAO,OAAO,CAAC,GAAG,YAAY,CAAC;EAC9C,KAAK,IAAI,KAAK,MAAM;EACpB,OAAO;CACT;CAEA,OAAO;EACL,KAAK,KAAK;GACR,OAAO,KAAK,IAAI,SAAS,GAAG,CAAC,KAAK;EACpC;EACA,MAAM,KAAK;GACT,MAAM,MAAM,SAAS,GAAG;GACxB,MAAM,MAAM,KAAK,IAAI,GAAG;GACxB,IAAI,KAAK,OAAO,QAAQ,QAAQ,GAAG;GACnC,MAAM,UAAU,SAAS,IAAI,GAAG;GAChC,IAAI,SAAS,OAAO;GACpB,MAAM,UAAU,IAAI,KAAK,GAAG,CAAC,CAAC,cAAc;IAC1C,SAAS,OAAO,GAAG;GACrB,CAAC;GACD,SAAS,IAAI,KAAK,OAAO;GACzB,OAAO;EACT;EACA,QAAQ;GACN,KAAK,MAAM;GACX,SAAS,MAAM;EACjB;CACF;AACF"}
@@ -1,2 +1,3 @@
1
- export declare function errorMessage(err: unknown, fallback?: string): string;
1
+ import { errorMessage } from '@mulmoclaude/common';
2
+ export { errorMessage };
2
3
  export declare function toError(err: unknown, fallback?: string): Error;
@@ -0,0 +1,36 @@
1
+ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
+ var DEFAULT_FETCH_TIMEOUT_MS = 10 * 1e3;
3
+ /**
4
+ * `fetch` with a finite timeout. Rejects with a `TimeoutError` once `timeoutMs`
5
+ * elapses. Composes with a caller-supplied `signal` so external cancellation
6
+ * still works (#2221 — do not overwrite the caller's signal).
7
+ */
8
+ async function fetchWithTimeout(url, init = {}) {
9
+ const { timeoutMs = DEFAULT_FETCH_TIMEOUT_MS, signal: callerSignal, ...rest } = init;
10
+ if (callerSignal?.aborted) throw callerSignal.reason ?? new DOMException("Aborted", "AbortError");
11
+ const controller = new AbortController();
12
+ const timer = setTimeout(() => {
13
+ controller.abort(new DOMException(`fetch timed out after ${timeoutMs}ms`, "TimeoutError"));
14
+ }, timeoutMs);
15
+ const unsubscribeCaller = bridgeExternalSignal(callerSignal, controller);
16
+ try {
17
+ return await fetch(url, {
18
+ ...rest,
19
+ signal: controller.signal
20
+ });
21
+ } finally {
22
+ clearTimeout(timer);
23
+ unsubscribeCaller?.();
24
+ }
25
+ }
26
+ function bridgeExternalSignal(external, controller) {
27
+ if (!external) return null;
28
+ const onAbort = () => controller.abort(external.reason);
29
+ external.addEventListener("abort", onAbort, { once: true });
30
+ return () => external.removeEventListener("abort", onAbort);
31
+ }
32
+ //#endregion
33
+ exports.DEFAULT_FETCH_TIMEOUT_MS = DEFAULT_FETCH_TIMEOUT_MS;
34
+ exports.fetchWithTimeout = fetchWithTimeout;
35
+
36
+ //# sourceMappingURL=fetch.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"fetch.cjs","names":[],"sources":["../../src/utils/fetch.ts"],"sourcesContent":["// Canonical `fetch` with a finite timeout for the whole monorepo. The host\n// (`server/utils/fetch.ts`), the collection registry engine and the Google\n// engine all re-export / import this one copy — before #2398 the same helper\n// existed three times, so #2221's caller-signal-composition fix could only land\n// in one of them. Server-only surface (`@mulmoclaude/core/fetch`).\n\nconst ONE_SECOND_MS = 1_000;\n\n// 10 s is long enough for a healthy round-trip (localhost or a healthy upstream)\n// but short enough that a stuck peer returns an error well before any client-side\n// tool-call timeout fires. Callers talking to slow external APIs override via\n// `timeoutMs`.\nexport const DEFAULT_FETCH_TIMEOUT_MS = 10 * ONE_SECOND_MS;\n\n// `Parameters<typeof fetch>[1]` avoids referencing the ambient `RequestInit`\n// type, which ESLint's `no-undef` rule trips over in the server config. The\n// resulting type is identical to `RequestInit & { timeoutMs?: number }`.\nexport type FetchWithTimeoutInit = Parameters<typeof fetch>[1] & { timeoutMs?: number };\n\n/**\n * `fetch` with a finite timeout. Rejects with a `TimeoutError` once `timeoutMs`\n * elapses. Composes with a caller-supplied `signal` so external cancellation\n * still works (#2221 — do not overwrite the caller's signal).\n */\nexport async function fetchWithTimeout(url: string | URL, init: FetchWithTimeoutInit = {}): Promise<Response> {\n const { timeoutMs = DEFAULT_FETCH_TIMEOUT_MS, signal: callerSignal, ...rest } = init;\n\n // Surface an already-aborted caller signal before we touch the network at all,\n // so callers get a deterministic rejection and no fetch side-effects.\n if (callerSignal?.aborted) {\n throw callerSignal.reason ?? new DOMException(\"Aborted\", \"AbortError\");\n }\n\n const controller = new AbortController();\n const timer = setTimeout(() => {\n controller.abort(new DOMException(`fetch timed out after ${timeoutMs}ms`, \"TimeoutError\"));\n }, timeoutMs);\n\n const unsubscribeCaller = bridgeExternalSignal(callerSignal, controller);\n\n try {\n return await fetch(url, { ...rest, signal: controller.signal });\n } finally {\n clearTimeout(timer);\n unsubscribeCaller?.();\n }\n}\n\n// Propagate aborts from a caller-supplied signal into our internal controller.\n// Returns a disposer so the listener is removed on normal completion — otherwise\n// a long-lived caller signal would leak listeners across many fetches.\nfunction bridgeExternalSignal(external: AbortSignal | null | undefined, controller: AbortController): (() => void) | null {\n if (!external) return null;\n const onAbort = () => controller.abort(external.reason);\n external.addEventListener(\"abort\", onAbort, { once: true });\n return () => external.removeEventListener(\"abort\", onAbort);\n}\n"],"mappings":";AAYA,IAAa,2BAA2B,KAAK;;;;;;AAY7C,eAAsB,iBAAiB,KAAmB,OAA6B,CAAC,GAAsB;CAC5G,MAAM,EAAE,YAAY,0BAA0B,QAAQ,cAAc,GAAG,SAAS;CAIhF,IAAI,cAAc,SAChB,MAAM,aAAa,UAAU,IAAI,aAAa,WAAW,YAAY;CAGvE,MAAM,aAAa,IAAI,gBAAgB;CACvC,MAAM,QAAQ,iBAAiB;EAC7B,WAAW,MAAM,IAAI,aAAa,yBAAyB,UAAU,KAAK,cAAc,CAAC;CAC3F,GAAG,SAAS;CAEZ,MAAM,oBAAoB,qBAAqB,cAAc,UAAU;CAEvE,IAAI;EACF,OAAO,MAAM,MAAM,KAAK;GAAE,GAAG;GAAM,QAAQ,WAAW;EAAO,CAAC;CAChE,UAAU;EACR,aAAa,KAAK;EAClB,oBAAoB;CACtB;AACF;AAKA,SAAS,qBAAqB,UAA0C,YAAkD;CACxH,IAAI,CAAC,UAAU,OAAO;CACtB,MAAM,gBAAgB,WAAW,MAAM,SAAS,MAAM;CACtD,SAAS,iBAAiB,SAAS,SAAS,EAAE,MAAM,KAAK,CAAC;CAC1D,aAAa,SAAS,oBAAoB,SAAS,OAAO;AAC5D"}
@@ -2,7 +2,9 @@ export declare const DEFAULT_FETCH_TIMEOUT_MS: number;
2
2
  export type FetchWithTimeoutInit = Parameters<typeof fetch>[1] & {
3
3
  timeoutMs?: number;
4
4
  };
5
- /** `fetch` with a finite timeout. Rejects with a `TimeoutError` once `timeoutMs`
6
- * elapses. Composes with a caller-supplied `signal` so external cancellation
7
- * still works. */
5
+ /**
6
+ * `fetch` with a finite timeout. Rejects with a `TimeoutError` once `timeoutMs`
7
+ * elapses. Composes with a caller-supplied `signal` so external cancellation
8
+ * still works (#2221 — do not overwrite the caller's signal).
9
+ */
8
10
  export declare function fetchWithTimeout(url: string | URL, init?: FetchWithTimeoutInit): Promise<Response>;
@@ -0,0 +1,34 @@
1
+ var DEFAULT_FETCH_TIMEOUT_MS = 10 * 1e3;
2
+ /**
3
+ * `fetch` with a finite timeout. Rejects with a `TimeoutError` once `timeoutMs`
4
+ * elapses. Composes with a caller-supplied `signal` so external cancellation
5
+ * still works (#2221 — do not overwrite the caller's signal).
6
+ */
7
+ async function fetchWithTimeout(url, init = {}) {
8
+ const { timeoutMs = DEFAULT_FETCH_TIMEOUT_MS, signal: callerSignal, ...rest } = init;
9
+ if (callerSignal?.aborted) throw callerSignal.reason ?? new DOMException("Aborted", "AbortError");
10
+ const controller = new AbortController();
11
+ const timer = setTimeout(() => {
12
+ controller.abort(new DOMException(`fetch timed out after ${timeoutMs}ms`, "TimeoutError"));
13
+ }, timeoutMs);
14
+ const unsubscribeCaller = bridgeExternalSignal(callerSignal, controller);
15
+ try {
16
+ return await fetch(url, {
17
+ ...rest,
18
+ signal: controller.signal
19
+ });
20
+ } finally {
21
+ clearTimeout(timer);
22
+ unsubscribeCaller?.();
23
+ }
24
+ }
25
+ function bridgeExternalSignal(external, controller) {
26
+ if (!external) return null;
27
+ const onAbort = () => controller.abort(external.reason);
28
+ external.addEventListener("abort", onAbort, { once: true });
29
+ return () => external.removeEventListener("abort", onAbort);
30
+ }
31
+ //#endregion
32
+ export { DEFAULT_FETCH_TIMEOUT_MS, fetchWithTimeout };
33
+
34
+ //# sourceMappingURL=fetch.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"fetch.js","names":[],"sources":["../../src/utils/fetch.ts"],"sourcesContent":["// Canonical `fetch` with a finite timeout for the whole monorepo. The host\n// (`server/utils/fetch.ts`), the collection registry engine and the Google\n// engine all re-export / import this one copy — before #2398 the same helper\n// existed three times, so #2221's caller-signal-composition fix could only land\n// in one of them. Server-only surface (`@mulmoclaude/core/fetch`).\n\nconst ONE_SECOND_MS = 1_000;\n\n// 10 s is long enough for a healthy round-trip (localhost or a healthy upstream)\n// but short enough that a stuck peer returns an error well before any client-side\n// tool-call timeout fires. Callers talking to slow external APIs override via\n// `timeoutMs`.\nexport const DEFAULT_FETCH_TIMEOUT_MS = 10 * ONE_SECOND_MS;\n\n// `Parameters<typeof fetch>[1]` avoids referencing the ambient `RequestInit`\n// type, which ESLint's `no-undef` rule trips over in the server config. The\n// resulting type is identical to `RequestInit & { timeoutMs?: number }`.\nexport type FetchWithTimeoutInit = Parameters<typeof fetch>[1] & { timeoutMs?: number };\n\n/**\n * `fetch` with a finite timeout. Rejects with a `TimeoutError` once `timeoutMs`\n * elapses. Composes with a caller-supplied `signal` so external cancellation\n * still works (#2221 — do not overwrite the caller's signal).\n */\nexport async function fetchWithTimeout(url: string | URL, init: FetchWithTimeoutInit = {}): Promise<Response> {\n const { timeoutMs = DEFAULT_FETCH_TIMEOUT_MS, signal: callerSignal, ...rest } = init;\n\n // Surface an already-aborted caller signal before we touch the network at all,\n // so callers get a deterministic rejection and no fetch side-effects.\n if (callerSignal?.aborted) {\n throw callerSignal.reason ?? new DOMException(\"Aborted\", \"AbortError\");\n }\n\n const controller = new AbortController();\n const timer = setTimeout(() => {\n controller.abort(new DOMException(`fetch timed out after ${timeoutMs}ms`, \"TimeoutError\"));\n }, timeoutMs);\n\n const unsubscribeCaller = bridgeExternalSignal(callerSignal, controller);\n\n try {\n return await fetch(url, { ...rest, signal: controller.signal });\n } finally {\n clearTimeout(timer);\n unsubscribeCaller?.();\n }\n}\n\n// Propagate aborts from a caller-supplied signal into our internal controller.\n// Returns a disposer so the listener is removed on normal completion — otherwise\n// a long-lived caller signal would leak listeners across many fetches.\nfunction bridgeExternalSignal(external: AbortSignal | null | undefined, controller: AbortController): (() => void) | null {\n if (!external) return null;\n const onAbort = () => controller.abort(external.reason);\n external.addEventListener(\"abort\", onAbort, { once: true });\n return () => external.removeEventListener(\"abort\", onAbort);\n}\n"],"mappings":"AAYA,IAAa,2BAA2B,KAAK;;;;;;AAY7C,eAAsB,iBAAiB,KAAmB,OAA6B,CAAC,GAAsB;CAC5G,MAAM,EAAE,YAAY,0BAA0B,QAAQ,cAAc,GAAG,SAAS;CAIhF,IAAI,cAAc,SAChB,MAAM,aAAa,UAAU,IAAI,aAAa,WAAW,YAAY;CAGvE,MAAM,aAAa,IAAI,gBAAgB;CACvC,MAAM,QAAQ,iBAAiB;EAC7B,WAAW,MAAM,IAAI,aAAa,yBAAyB,UAAU,KAAK,cAAc,CAAC;CAC3F,GAAG,SAAS;CAEZ,MAAM,oBAAoB,qBAAqB,cAAc,UAAU;CAEvE,IAAI;EACF,OAAO,MAAM,MAAM,KAAK;GAAE,GAAG;GAAM,QAAQ,WAAW;EAAO,CAAC;CAChE,UAAU;EACR,aAAa,KAAK;EAClB,oBAAoB;CACtB;AACF;AAKA,SAAS,qBAAqB,UAA0C,YAAkD;CACxH,IAAI,CAAC,UAAU,OAAO;CACtB,MAAM,gBAAgB,WAAW,MAAM,SAAS,MAAM;CACtD,SAAS,iBAAiB,SAAS,SAAS,EAAE,MAAM,KAAK,CAAC;CAC1D,aAAa,SAAS,oBAAoB,SAAS,OAAO;AAC5D"}
@@ -1,6 +1,14 @@
1
1
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
- const require_errors = require("../errors-7P5eMOSX.cjs");
2
+ const require_dist = require("../dist-pWpC-b04.cjs");
3
3
  const require_text = require("../text-CaAh9ZSO.cjs");
4
- exports.errorMessage = require_errors.errorMessage;
5
- exports.toError = require_errors.toError;
4
+ //#region src/utils/errors.ts
5
+ function toError(err, fallback) {
6
+ if (err instanceof Error) return err;
7
+ return new Error(fallback ?? require_dist.errorMessage(err));
8
+ }
9
+ //#endregion
10
+ exports.errorMessage = require_dist.errorMessage;
11
+ exports.toError = toError;
6
12
  exports.truncate = require_text.truncate;
13
+
14
+ //# sourceMappingURL=index.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.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.\n//\n// `errorMessage` itself now lives in `@mulmoclaude/common` (browser-safe leaf,\n// zero deps) so the Vue plugin surfaces can share the one implementation\n// instead of hand-copying it — `@mulmoclaude/core/utils` is server-only and\n// unreachable from browser code. This module keeps the same import surface so\n// no core/host consumer of `errorMessage` breaks.\nimport { errorMessage } from \"@mulmoclaude/common\";\n\nexport { errorMessage };\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":";;;;AAuBA,SAAgB,QAAQ,KAAc,UAA0B;CAC9D,IAAI,eAAe,OAAO,OAAO;CACjC,OAAO,IAAI,MAAM,YAAY,aAAA,aAAa,GAAG,CAAC;AAChD"}
@@ -1,3 +1,11 @@
1
- import { n as toError, t as errorMessage } from "../errors-eid6Mes3.js";
1
+ import { t as errorMessage } from "../dist-Cwk0e12G.js";
2
2
  import { t as truncate } from "../text-CjDbP2O1.js";
3
+ //#region src/utils/errors.ts
4
+ function toError(err, fallback) {
5
+ if (err instanceof Error) return err;
6
+ return new Error(fallback ?? errorMessage(err));
7
+ }
8
+ //#endregion
3
9
  export { errorMessage, toError, truncate };
10
+
11
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.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.\n//\n// `errorMessage` itself now lives in `@mulmoclaude/common` (browser-safe leaf,\n// zero deps) so the Vue plugin surfaces can share the one implementation\n// instead of hand-copying it — `@mulmoclaude/core/utils` is server-only and\n// unreachable from browser code. This module keeps the same import surface so\n// no core/host consumer of `errorMessage` breaks.\nimport { errorMessage } from \"@mulmoclaude/common\";\n\nexport { errorMessage };\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":";;;AAuBA,SAAgB,QAAQ,KAAc,UAA0B;CAC9D,IAAI,eAAe,OAAO,OAAO;CACjC,OAAO,IAAI,MAAM,YAAY,aAAa,GAAG,CAAC;AAChD"}
@@ -1,11 +1,11 @@
1
1
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
2
  const require_rolldown_runtime = require("../rolldown-runtime-D6vf50IK.cjs");
3
- const require_errors = require("../errors-7P5eMOSX.cjs");
3
+ const require_dist = require("../dist-pWpC-b04.cjs");
4
+ let node_fs = require("node:fs");
4
5
  let node_path = require("node:path");
5
6
  node_path = require_rolldown_runtime.__toESM(node_path, 1);
6
- let node_fs = require("node:fs");
7
- let node_fs_promises = require("node:fs/promises");
8
7
  let node_crypto = require("node:crypto");
8
+ let node_fs_promises = require("node:fs/promises");
9
9
  let node_net = require("node:net");
10
10
  let node_child_process = require("node:child_process");
11
11
  let node_util = require("node:util");
@@ -176,7 +176,7 @@ function createModelDownloader(modelsDir, logger = NOOP_LOGGER) {
176
176
  downloadStatus.set(name, { state: "ready" });
177
177
  logger.info("model download: ok", { model: name });
178
178
  } catch (err) {
179
- const error = require_errors.errorMessage(err);
179
+ const error = require_dist.errorMessage(err);
180
180
  downloadStatus.set(name, {
181
181
  state: "error",
182
182
  error
@@ -266,7 +266,7 @@ function waitForReadyOrFailure(proc, port) {
266
266
  };
267
267
  onError = (err) => {
268
268
  cleanup();
269
- reject(/* @__PURE__ */ new Error(`spawn failed: ${require_errors.errorMessage(err)}`));
269
+ reject(/* @__PURE__ */ new Error(`spawn failed: ${require_dist.errorMessage(err)}`));
270
270
  };
271
271
  onExit = (code) => {
272
272
  cleanup();
@@ -297,7 +297,7 @@ async function postInference(port, wavPath, language) {
297
297
  signal: AbortSignal.timeout(INFERENCE_TIMEOUT_MS)
298
298
  });
299
299
  } catch (err) {
300
- throw new Error(`whisper-server request failed: ${require_errors.errorMessage(err)}`);
300
+ throw new Error(`whisper-server request failed: ${require_dist.errorMessage(err)}`);
301
301
  }
302
302
  if (!res.ok) throw new Error(`whisper-server returned HTTP ${res.status}`);
303
303
  return parseInferenceText(await res.json());
@@ -321,7 +321,7 @@ function instrumentChildProcess(proc, model, logger, onExit) {
321
321
  drainStderr(proc, stderrTail);
322
322
  proc.on("error", (err) => logger.warn("sidecar: process error", {
323
323
  model,
324
- error: require_errors.errorMessage(err)
324
+ error: require_dist.errorMessage(err)
325
325
  }));
326
326
  proc.on("exit", (code) => {
327
327
  logger.warn("sidecar: exited", {
@@ -338,7 +338,7 @@ async function awaitReadyOrThrow(proc, port, stderrTail, waitReady) {
338
338
  await waitReady(proc, port);
339
339
  } catch (err) {
340
340
  proc.kill();
341
- throw new Error(`whisper-server failed to start: ${require_errors.errorMessage(err)} — stderr: ${stderrTail.text.slice(-500)}`);
341
+ throw new Error(`whisper-server failed to start: ${require_dist.errorMessage(err)} — stderr: ${stderrTail.text.slice(-500)}`);
342
342
  }
343
343
  }
344
344
  function throwIfStartCancelled(startedToken, currentToken, proc) {
@@ -421,7 +421,7 @@ function createSidecar(modelsDir, serverBinary = "whisper-server", logger = NOOP
421
421
  } catch (err) {
422
422
  logger.warn("sidecar: warmup failed", {
423
423
  model,
424
- error: require_errors.errorMessage(err)
424
+ error: require_dist.errorMessage(err)
425
425
  });
426
426
  }
427
427
  }
@@ -1 +1 @@
1
- {"version":3,"file":"index.cjs","names":[],"sources":["../../src/whisper/internal.ts","../../src/whisper/ffmpeg.ts","../../src/whisper/models.ts","../../src/whisper/sidecar-helpers.ts","../../src/whisper/sidecar.ts","../../src/whisper/whisper.ts"],"sourcesContent":["// Small self-contained utilities so the package has no host dependencies.\n\nexport const ONE_SECOND_MS = 1_000;\nexport const ONE_MINUTE_MS = 60_000;\n\nexport { errorMessage } from \"../utils/errors.js\";\n\n/** Minimal logger the host can inject; defaults to no-op so the package\n * is silent unless wired up. */\nexport interface WhisperLogger {\n info: (message: string, data?: unknown) => void;\n warn: (message: string, data?: unknown) => void;\n error: (message: string, data?: unknown) => void;\n}\n\nconst NOOP = (): void => undefined;\nexport const NOOP_LOGGER: WhisperLogger = { info: NOOP, warn: NOOP, error: NOOP };\n","// Thin wrapper around the system `ffmpeg` binary (provided by the host) for\n// converting a browser webm/opus clip to the 16 kHz mono 16-bit WAV whisper.cpp\n// requires.\n\nimport { execFile } from \"node:child_process\";\nimport { promisify } from \"node:util\";\nimport { ONE_MINUTE_MS } from \"./internal.ts\";\n\nconst execFileAsync = promisify(execFile);\n\n/** ffmpeg args to decode any input to 16 kHz mono signed-16-bit WAV. Pure +\n * exported for unit tests. */\nexport function buildWav16kArgs(inputPath: string, outputPath: string): string[] {\n return [\"-y\", \"-loglevel\", \"error\", \"-i\", inputPath, \"-ar\", \"16000\", \"-ac\", \"1\", \"-c:a\", \"pcm_s16le\", outputPath];\n}\n\n/** Convert `inputPath` to a 16 kHz mono WAV at `outputPath`. Throws on ffmpeg\n * failure or timeout. */\nexport async function convertToWav16k(inputPath: string, outputPath: string, ffmpegBinary = \"ffmpeg\"): Promise<void> {\n await execFileAsync(ffmpegBinary, buildWav16kArgs(inputPath, outputPath), {\n timeout: ONE_MINUTE_MS,\n });\n}\n","// Whisper GGML model registry + on-disk management. The host injects the models\n// directory (e.g. `{workspace}/models`); nothing here reads a host module.\n\nimport { createWriteStream, mkdirSync, renameSync, statSync, unlinkSync } from \"node:fs\";\nimport { once } from \"node:events\";\nimport path from \"node:path\";\nimport { errorMessage, NOOP_LOGGER, ONE_MINUTE_MS, type WhisperLogger } from \"./internal.ts\";\n\nexport interface WhisperModelSpec {\n /** GGML filename — identical on disk and in the Hugging Face repo. */\n readonly file: string;\n /** Download URL (Hugging Face whisper.cpp model repo). */\n readonly url: string;\n /** Conservative lower bound on the finished file size, in bytes — guards\n * against a truncated transfer or an HTML error page saved as the model\n * without pinning an exact checksum. */\n readonly minBytes: number;\n}\n\nconst HF_BASE = \"https://huggingface.co/ggerganov/whisper.cpp/resolve/main\";\n\n// large-v3-turbo: strong accuracy, near-real-time on Apple Silicon with Metal.\n// small/base are lighter fallbacks for low-RAM machines.\nexport const WHISPER_MODELS = {\n \"large-v3-turbo\": { file: \"ggml-large-v3-turbo.bin\", url: `${HF_BASE}/ggml-large-v3-turbo.bin`, minBytes: 1_000_000_000 },\n small: { file: \"ggml-small.bin\", url: `${HF_BASE}/ggml-small.bin`, minBytes: 300_000_000 },\n base: { file: \"ggml-base.bin\", url: `${HF_BASE}/ggml-base.bin`, minBytes: 100_000_000 },\n} as const satisfies Record<string, WhisperModelSpec>;\n\nexport type WhisperModelName = keyof typeof WHISPER_MODELS;\nexport const DEFAULT_WHISPER_MODEL: WhisperModelName = \"large-v3-turbo\";\n\nexport function isWhisperModelName(value: unknown): value is WhisperModelName {\n // Own-property check — `in` would accept inherited keys like \"toString\",\n // which then crash the `WHISPER_MODELS[name]` lookups instead of falling\n // back to the default.\n return typeof value === \"string\" && Object.prototype.hasOwnProperty.call(WHISPER_MODELS, value);\n}\n\n/** Resolve a possibly-unset / unknown model name to a valid one. */\nexport function resolveModelName(name: string | undefined): WhisperModelName {\n return isWhisperModelName(name) ? name : DEFAULT_WHISPER_MODEL;\n}\n\nexport function modelFilePath(modelsDir: string, name: WhisperModelName): string {\n return path.join(modelsDir, WHISPER_MODELS[name].file);\n}\n\n/** A model is \"ready\" when its file exists and meets the size floor. */\nexport function isModelReady(modelsDir: string, name: WhisperModelName): boolean {\n try {\n return statSync(modelFilePath(modelsDir, name)).size >= WHISPER_MODELS[name].minBytes;\n } catch {\n return false;\n }\n}\n\nexport type ModelDownloadState = \"idle\" | \"downloading\" | \"ready\" | \"error\";\n\nexport interface ModelStatus {\n state: ModelDownloadState;\n /** 0..1 — present only while downloading and Content-Length is known. */\n progress?: number;\n /** Present only in the \"error\" state. */\n error?: string;\n}\n\n/** Report status precedence: an in-flight download wins, then an on-disk ready\n * file, then the last transient state (idle if none). Pure so the precedence\n * is unit-testable without touching the filesystem. */\nexport function pickModelStatus(live: ModelStatus | undefined, isReady: boolean): ModelStatus {\n if (live?.state === \"downloading\") return live;\n if (isReady) return { state: \"ready\" };\n return live ?? { state: \"idle\" };\n}\n\n/** Parse a `Content-Length` header to a byte count; unknown / malformed → 0. */\nexport function parseContentLength(header: string | null): number {\n return Number(header) || 0;\n}\n\n// Abort a download if no bytes arrive for this long (stalled connection).\nconst DOWNLOAD_STALL_TIMEOUT_MS = ONE_MINUTE_MS;\n\nexport interface ModelDownloader {\n getStatus: (name: WhisperModelName) => ModelStatus;\n ensure: (name: WhisperModelName) => Promise<void>;\n}\n\ninterface StreamToFileDeps {\n downloadStatus: Map<WhisperModelName, ModelStatus>;\n onProgress: () => void;\n}\n\n/** Drain the response stream into the `.partial` file, resetting the stall\n * timer on every chunk and publishing progress once the total size is known. */\nasync function streamToFile(\n body: ReadableStream<Uint8Array>,\n partialPath: string,\n total: number,\n name: WhisperModelName,\n deps: StreamToFileDeps,\n): Promise<void> {\n const reader = body.getReader();\n const fileStream = createWriteStream(partialPath);\n let received = 0;\n try {\n for (;;) {\n const { done, value } = await reader.read();\n if (done) break;\n deps.onProgress();\n received += value.byteLength;\n if (!fileStream.write(value)) await once(fileStream, \"drain\");\n if (total > 0) deps.downloadStatus.set(name, { state: \"downloading\", progress: received / total });\n }\n fileStream.end();\n await once(fileStream, \"finish\");\n } catch (err) {\n // `createWriteStream` opens lazily. Destroying it while that open is still\n // in flight lets the open's own failure land later as an `error` event with\n // no listener — an uncaughtException that kills the process. `err` already\n // carries the real cause, so absorb the late one.\n fileStream.on(\"error\", () => {});\n fileStream.destroy();\n throw err;\n }\n}\n\n/** Fetch a model into a `.partial` file, reject a truncated transfer via the\n * size floor, then atomically rename it into place. */\nasync function downloadModel(name: WhisperModelName, modelsDir: string, downloadStatus: Map<WhisperModelName, ModelStatus>): Promise<void> {\n const spec = WHISPER_MODELS[name];\n mkdirSync(modelsDir, { recursive: true });\n const dest = modelFilePath(modelsDir, name);\n const partial = `${dest}.partial`;\n const controller = new AbortController();\n let stallTimer: ReturnType<typeof setTimeout> | null = null;\n const resetStall = () => {\n if (stallTimer) clearTimeout(stallTimer);\n stallTimer = setTimeout(() => controller.abort(), DOWNLOAD_STALL_TIMEOUT_MS);\n };\n try {\n const response = await fetch(spec.url, { signal: controller.signal });\n if (!response.ok || !response.body) {\n throw new Error(`download failed: HTTP ${response.status}`);\n }\n const total = parseContentLength(response.headers.get(\"content-length\"));\n resetStall();\n await streamToFile(response.body, partial, total, name, { downloadStatus, onProgress: resetStall });\n if (statSync(partial).size < spec.minBytes) {\n unlinkSync(partial);\n throw new Error(\"downloaded file is smaller than expected — likely truncated\");\n }\n renameSync(partial, dest);\n } finally {\n if (stallTimer) clearTimeout(stallTimer);\n }\n}\n\nexport function createModelDownloader(modelsDir: string, logger: WhisperLogger = NOOP_LOGGER): ModelDownloader {\n // A finished file on disk is the source of truth for \"ready\"; this map tracks\n // transient progress + the last error.\n const downloadStatus = new Map<WhisperModelName, ModelStatus>();\n\n function getStatus(name: WhisperModelName): ModelStatus {\n return pickModelStatus(downloadStatus.get(name), isModelReady(modelsDir, name));\n }\n\n // Fire-and-forget friendly: errors land in the status map, never thrown.\n // Idempotent — a second call while a download is in flight does nothing.\n async function ensure(name: WhisperModelName): Promise<void> {\n if (isModelReady(modelsDir, name)) {\n downloadStatus.set(name, { state: \"ready\" });\n return;\n }\n if (downloadStatus.get(name)?.state === \"downloading\") return;\n downloadStatus.set(name, { state: \"downloading\", progress: 0 });\n logger.info(\"model download: start\", { model: name });\n try {\n await downloadModel(name, modelsDir, downloadStatus);\n downloadStatus.set(name, { state: \"ready\" });\n logger.info(\"model download: ok\", { model: name });\n } catch (err) {\n const error = errorMessage(err);\n downloadStatus.set(name, { state: \"error\", error });\n logger.error(\"model download: failed\", { model: name, error });\n }\n }\n\n return { getStatus, ensure };\n}\n","// Pure helpers for the whisper-server sidecar. Kept separate from `sidecar.ts`\n// so the argv/response/stderr logic can be unit-tested without spawning a\n// process or opening a socket.\n\n/** whisper-server CLI args to preload `modelPath` and bind to `host:port`. */\nexport function buildServerArgs(modelPath: string, host: string, port: number): string[] {\n return [\"--model\", modelPath, \"--host\", host, \"--port\", String(port)];\n}\n\n/** Append `chunk` to a bounded stderr tail, keeping only the last `maxChars`. */\nexport function appendStderrTail(previous: string, chunk: string, maxChars: number): string {\n return (previous + chunk).slice(-maxChars);\n}\n\n/** Extract the transcript from a whisper-server `/inference` JSON response.\n * Returns \"\" for any shape that lacks a string `text` field. */\nexport function parseInferenceText(data: unknown): string {\n if (typeof data === \"object\" && data !== null && \"text\" in data) {\n const { text } = data as { text: unknown };\n if (typeof text === \"string\") return text;\n }\n return \"\";\n}\n","// whisper.cpp warm-model sidecar. Spawns `whisper-server` once with the model\n// preloaded and reuses it across transcriptions over its local HTTP API, so the\n// weights stay resident (no per-request reload). State is encapsulated per\n// instance via the factory closure.\n\nimport { spawn, type ChildProcess } from \"node:child_process\";\nimport { createServer } from \"node:net\";\nimport { readFile } from \"node:fs/promises\";\nimport { setTimeout as delay } from \"node:timers/promises\";\nimport { errorMessage, NOOP_LOGGER, ONE_MINUTE_MS, ONE_SECOND_MS, type WhisperLogger } from \"./internal.ts\";\nimport { modelFilePath, type WhisperModelName } from \"./models.ts\";\nimport { appendStderrTail, buildServerArgs, parseInferenceText } from \"./sidecar-helpers.ts\";\n\nconst HOST = \"127.0.0.1\";\nconst READY_TIMEOUT_MS = 60 * ONE_SECOND_MS;\nconst READY_POLL_INTERVAL_MS = 500;\nconst INFERENCE_TIMEOUT_MS = 2 * ONE_MINUTE_MS;\nconst STDERR_TAIL_MAX_CHARS = 4_000;\n\nexport interface ActiveSidecar {\n readonly port: number;\n readonly proc: ChildProcess;\n readonly model: WhisperModelName;\n}\n\nexport interface Sidecar {\n transcribeWav: (wavPath: string, language: string, model: WhisperModelName) => Promise<string>;\n warmup: (model: WhisperModelName) => Promise<void>;\n shutdown: () => void;\n}\n\nasync function findFreePort(): Promise<number> {\n return new Promise((resolve, reject) => {\n const srv = createServer();\n srv.on(\"error\", reject);\n srv.listen(0, HOST, () => {\n const addr = srv.address();\n if (addr && typeof addr === \"object\") {\n const { port } = addr;\n srv.close(() => resolve(port));\n } else {\n srv.close(() => reject(new Error(\"could not determine a free port\")));\n }\n });\n });\n}\n\n/** Resolve once the server answers any HTTP request (any status = listener up),\n * or throw after the ready timeout. */\nasync function waitUntilReady(port: number): Promise<void> {\n const deadline = Date.now() + READY_TIMEOUT_MS;\n while (Date.now() < deadline) {\n try {\n await fetch(`http://${HOST}:${port}/`, { signal: AbortSignal.timeout(ONE_SECOND_MS) });\n return;\n } catch {\n await delay(READY_POLL_INTERVAL_MS);\n }\n }\n throw new Error(\"whisper-server did not become ready in time\");\n}\n\n// whisper-server logs verbosely to stderr; left unread the OS pipe buffer fills\n// and the child blocks on its next write. Drain into a small tail buffer.\nfunction drainStderr(proc: ChildProcess, tail: { text: string }): void {\n proc.stderr?.setEncoding(\"utf8\");\n proc.stderr?.on(\"data\", (chunk: string) => {\n tail.text = appendStderrTail(tail.text, chunk, STDERR_TAIL_MAX_CHARS);\n });\n}\n\n// Resolve when the server answers, or reject on spawn failure (e.g. ENOENT) /\n// early exit. Listeners are one-shot and removed once the race settles.\nfunction waitForReadyOrFailure(proc: ChildProcess, port: number): Promise<void> {\n return new Promise<void>((resolve, reject) => {\n let onError: (err: Error) => void = () => undefined;\n let onExit: (code: number | null) => void = () => undefined;\n const cleanup = () => {\n proc.removeListener(\"error\", onError);\n proc.removeListener(\"exit\", onExit);\n };\n onError = (err: Error) => {\n cleanup();\n reject(new Error(`spawn failed: ${errorMessage(err)}`));\n };\n onExit = (code: number | null) => {\n cleanup();\n reject(new Error(`exited early (code ${code})`));\n };\n proc.once(\"error\", onError);\n proc.once(\"exit\", onExit);\n waitUntilReady(port)\n .then(() => {\n cleanup();\n resolve();\n })\n .catch((err: unknown) => {\n cleanup();\n reject(err instanceof Error ? err : new Error(String(err)));\n });\n });\n}\n\n// POST the wav to whisper-server's `/inference` and return the transcript.\n// Isolated from the lifecycle so the HTTP contract is unit-testable without a\n// live child (stub `fetch`, hand it a real wav path).\nexport async function postInference(port: number, wavPath: string, language: string): Promise<string> {\n const buf = await readFile(wavPath);\n const form = new FormData();\n form.append(\"file\", new Blob([buf], { type: \"audio/wav\" }), \"audio.wav\");\n form.append(\"response_format\", \"json\");\n form.append(\"language\", language || \"auto\");\n let res: Response;\n try {\n res = await fetch(`http://${HOST}:${port}/inference`, { method: \"POST\", body: form, signal: AbortSignal.timeout(INFERENCE_TIMEOUT_MS) });\n } catch (err) {\n throw new Error(`whisper-server request failed: ${errorMessage(err)}`);\n }\n if (!res.ok) throw new Error(`whisper-server returned HTTP ${res.status}`);\n return parseInferenceText(await res.json());\n}\n\n// The impure primitives the start lifecycle depends on, injected so the\n// cancellation / single-flight / model-switch logic can be driven with fakes.\nexport interface StartLifecycleDeps {\n /** Reserve a free TCP port for the next child — the sole async step of a start. */\n allocatePort: () => Promise<number>;\n /** Spawn the whisper-server child on `port`. MUST be synchronous so the spawn\n * and the `startingProc` handoff stay in one tick: `shutdown()` has to be able\n * to kill an in-flight child immediately, with no await gap after it exists. */\n spawnServer: (model: WhisperModelName, port: number) => ChildProcess;\n /** Resolve once the child answers HTTP, or reject on spawn failure / early exit. */\n waitReady: (proc: ChildProcess, port: number) => Promise<void>;\n logger: WhisperLogger;\n}\n\nexport interface StartLifecycle {\n ensureSidecar: (model: WhisperModelName) => Promise<ActiveSidecar>;\n shutdown: () => void;\n}\n\nexport function defaultSpawnServer(modelsDir: string, serverBinary: string, logger: WhisperLogger): StartLifecycleDeps[\"spawnServer\"] {\n return (model: WhisperModelName, port: number): ChildProcess => {\n const args = buildServerArgs(modelFilePath(modelsDir, model), HOST, port);\n logger.info(\"sidecar: spawning\", { model, port });\n return spawn(serverBinary, args, { stdio: [\"ignore\", \"ignore\", \"pipe\"] });\n };\n}\n\n// Wires the three listeners every whisper-server child needs: stderr drain,\n// permanent `error` handler (an unhandled 'error' from ENOENT etc. would crash\n// the host), and `exit` handler that both logs and lets the caller decide\n// whether this proc is still the active one (identity check via `onExit` — a\n// stale process must not evict a newer live sidecar).\nfunction instrumentChildProcess(proc: ChildProcess, model: WhisperModelName, logger: WhisperLogger, onExit: (exited: ChildProcess) => void): { text: string } {\n const stderrTail = { text: \"\" };\n drainStderr(proc, stderrTail);\n proc.on(\"error\", (err) => logger.warn(\"sidecar: process error\", { model, error: errorMessage(err) }));\n proc.on(\"exit\", (code) => {\n logger.warn(\"sidecar: exited\", { model, code, stderrTail: stderrTail.text.slice(-500) });\n onExit(proc);\n });\n return stderrTail;\n}\n\n// Await server readiness; on failure, kill the child and surface both the\n// underlying error and the tail of stderr (whisper-server usually explains its\n// crash there). Kept at module scope so `startSidecar` stays under the\n// per-function line cap.\nasync function awaitReadyOrThrow(proc: ChildProcess, port: number, stderrTail: { text: string }, waitReady: StartLifecycleDeps[\"waitReady\"]): Promise<void> {\n try {\n await waitReady(proc, port);\n } catch (err) {\n proc.kill();\n throw new Error(`whisper-server failed to start: ${errorMessage(err)} — stderr: ${stderrTail.text.slice(-500)}`);\n }\n}\n\n// Abort a start that was raced by `shutdown()` (or a newer start). Compares\n// the token captured at start entry against the current token; if they differ,\n// kill the freshly-booted child rather than publish it after shutdown returned.\nfunction throwIfStartCancelled(startedToken: number, currentToken: number, proc: ChildProcess): void {\n if (startedToken !== currentToken) {\n proc.kill();\n throw new Error(\"whisper-server start cancelled\");\n }\n}\n\n// Owns the single warm child's lifecycle: which model is live, the in-flight\n// start, and the cancellation token. Kept as a factory closure so the mutable\n// state is encapsulated rather than threaded through parameters.\nexport function createStartLifecycle(deps: StartLifecycleDeps): StartLifecycle {\n const { allocatePort, spawnServer, waitReady, logger } = deps;\n let sidecar: ActiveSidecar | null = null;\n let starting: { model: WhisperModelName; promise: Promise<ActiveSidecar> } | null = null;\n // The child of an in-flight start (before it's published as `sidecar`), plus a\n // token that `shutdown()` bumps to cancel a start that's still booting — so\n // shutdown can't return with a child that then publishes itself afterwards.\n let startingProc: ChildProcess | null = null;\n let startToken = 0;\n\n function shutdown(): void {\n startToken += 1;\n startingProc?.kill();\n startingProc = null;\n sidecar?.proc.kill();\n sidecar = null;\n }\n\n async function startSidecar(model: WhisperModelName): Promise<ActiveSidecar> {\n const token = ++startToken;\n const port = await allocatePort();\n const proc = spawnServer(model, port);\n startingProc = proc;\n // onExit fires when THIS proc exits: only null out `sidecar` if it's still the live one.\n const stderrTail = instrumentChildProcess(proc, model, logger, (exited) => sidecar?.proc === exited && (sidecar = null));\n try {\n await awaitReadyOrThrow(proc, port, stderrTail, waitReady);\n } finally {\n if (startingProc === proc) startingProc = null;\n }\n throwIfStartCancelled(token, startToken, proc);\n sidecar = { port, proc, model };\n logger.info(\"sidecar: ready\", { model, port });\n return sidecar;\n }\n\n // Own function so it isn't a loop closure (no-loop-func). Only ever one start\n // is in flight at a time, so clearing `starting` on settle is safe.\n function beginStart(model: WhisperModelName): Promise<ActiveSidecar> {\n const promise = startSidecar(model).finally(() => (starting = null));\n starting = { model, promise };\n return promise;\n }\n\n async function ensureSidecar(model: WhisperModelName): Promise<ActiveSidecar> {\n // Loop so that after awaiting an in-flight start for a DIFFERENT model we\n // re-evaluate; the decision-to-spawn path (beginStart) has no await, so the\n // first waiter sets `starting` synchronously and siblings then reuse it.\n for (;;) {\n if (sidecar && sidecar.model === model && !sidecar.proc.killed) return sidecar;\n if (starting && starting.model === model) return starting.promise;\n if (starting) {\n await starting.promise.catch(() => undefined);\n continue;\n }\n if (sidecar && sidecar.model !== model) shutdown();\n return beginStart(model);\n }\n }\n\n return { ensureSidecar, shutdown };\n}\n\nexport function createSidecar(modelsDir: string, serverBinary = \"whisper-server\", logger: WhisperLogger = NOOP_LOGGER): Sidecar {\n const lifecycle = createStartLifecycle({\n allocatePort: findFreePort,\n spawnServer: defaultSpawnServer(modelsDir, serverBinary, logger),\n waitReady: waitForReadyOrFailure,\n logger,\n });\n\n async function warmup(model: WhisperModelName): Promise<void> {\n try {\n await lifecycle.ensureSidecar(model);\n } catch (err) {\n logger.warn(\"sidecar: warmup failed\", { model, error: errorMessage(err) });\n }\n }\n\n async function transcribeWav(wavPath: string, language: string, model: WhisperModelName): Promise<string> {\n const active = await lifecycle.ensureSidecar(model);\n return postInference(active.port, wavPath, language);\n }\n\n return { transcribeWav, warmup, shutdown: lifecycle.shutdown };\n}\n","// Public server-side façade: wires the model downloader, the warm sidecar, and\n// ffmpeg conversion into one host-agnostic service. The host injects the models\n// directory + a logger and gates capability itself (platform / binary presence);\n// this package assumes the binaries exist when called.\n\nimport { mkdirSync } from \"node:fs\";\nimport { rm, writeFile } from \"node:fs/promises\";\nimport { randomUUID } from \"node:crypto\";\nimport path from \"node:path\";\nimport { NOOP_LOGGER, type WhisperLogger } from \"./internal.ts\";\nimport { convertToWav16k } from \"./ffmpeg.ts\";\nimport { createModelDownloader } from \"./models.ts\";\nimport { createSidecar } from \"./sidecar.ts\";\nimport { isModelReady, type ModelStatus, type WhisperModelName } from \"./models.ts\";\n\nexport interface WhisperOptions {\n /** Directory that holds the GGML model files (e.g. `{workspace}/models`). */\n modelsDir: string;\n logger?: WhisperLogger;\n /** Defaults to \"whisper-server\" / \"ffmpeg\" on PATH. */\n serverBinary?: string;\n ffmpegBinary?: string;\n}\n\nexport interface TranscribeRequest {\n base64: string;\n mimeType: string;\n language: string;\n model: WhisperModelName;\n}\n\nexport interface Whisper {\n isModelReady: (model: WhisperModelName) => boolean;\n getModelStatus: (model: WhisperModelName) => ModelStatus;\n /** Fire-and-forget friendly; never throws (errors land in the status). */\n ensureModelDownloaded: (model: WhisperModelName) => Promise<void>;\n warmup: (model: WhisperModelName) => Promise<void>;\n transcribe: (req: TranscribeRequest) => Promise<{ text: string }>;\n shutdown: () => void;\n}\n\n// whisper.cpp returns these sentinels for non-speech windows; treat them as\n// empty so the UI shows \"didn't catch that\" rather than a literal marker.\nconst BLANK_MARKERS = new Set([\"[blank_audio]\", \"[silence]\", \"(silence)\", \"[ inaudible ]\"]);\n\nfunction normalizeTranscript(raw: string): string {\n const trimmed = raw.trim().replace(/\\s+/g, \" \");\n return BLANK_MARKERS.has(trimmed.toLowerCase()) ? \"\" : trimmed;\n}\n\nexport function createWhisper(opts: WhisperOptions): Whisper {\n const { modelsDir } = opts;\n const logger = opts.logger ?? NOOP_LOGGER;\n const ffmpegBinary = opts.ffmpegBinary ?? \"ffmpeg\";\n const downloader = createModelDownloader(modelsDir, logger);\n const sidecar = createSidecar(modelsDir, opts.serverBinary ?? \"whisper-server\", logger);\n\n // Scratch dir for transient audio — a hidden subdir of the models dir so it\n // shares the (non-git) models tree. Files are deleted after each transcription.\n const scratchDir = path.join(modelsDir, \".scratch\");\n\n async function transcribe(req: TranscribeRequest): Promise<{ text: string }> {\n mkdirSync(scratchDir, { recursive: true });\n const clipId = randomUUID();\n const inputPath = path.join(scratchDir, `utterance-${clipId}.webm`);\n const wavPath = path.join(scratchDir, `utterance-${clipId}.wav`);\n try {\n await writeFile(inputPath, Buffer.from(req.base64, \"base64\"));\n await convertToWav16k(inputPath, wavPath, ffmpegBinary);\n const text = await sidecar.transcribeWav(wavPath, req.language, req.model);\n return { text: normalizeTranscript(text) };\n } finally {\n await rm(inputPath, { force: true });\n await rm(wavPath, { force: true });\n }\n }\n\n return {\n isModelReady: (model) => isModelReady(modelsDir, model),\n getModelStatus: (model) => downloader.getStatus(model),\n ensureModelDownloaded: (model) => downloader.ensure(model),\n warmup: (model) => sidecar.warmup(model),\n transcribe,\n shutdown: () => sidecar.shutdown(),\n };\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"index.cjs","names":[],"sources":["../../src/whisper/internal.ts","../../src/whisper/ffmpeg.ts","../../src/whisper/models.ts","../../src/whisper/sidecar-helpers.ts","../../src/whisper/sidecar.ts","../../src/whisper/whisper.ts"],"sourcesContent":["// Small self-contained utilities so the package has no host dependencies.\n\nexport const ONE_SECOND_MS = 1_000;\nexport const ONE_MINUTE_MS = 60_000;\n\nexport { errorMessage } from \"../utils/errors.js\";\n\n/** Minimal logger the host can inject; defaults to no-op so the package\n * is silent unless wired up. */\nexport interface WhisperLogger {\n info: (message: string, data?: unknown) => void;\n warn: (message: string, data?: unknown) => void;\n error: (message: string, data?: unknown) => void;\n}\n\nconst NOOP = (): void => undefined;\nexport const NOOP_LOGGER: WhisperLogger = { info: NOOP, warn: NOOP, error: NOOP };\n","// Thin wrapper around the system `ffmpeg` binary (provided by the host) for\n// converting a browser webm/opus clip to the 16 kHz mono 16-bit WAV whisper.cpp\n// requires.\n\nimport { execFile } from \"node:child_process\";\nimport { promisify } from \"node:util\";\nimport { ONE_MINUTE_MS } from \"./internal.ts\";\n\nconst execFileAsync = promisify(execFile);\n\n/** ffmpeg args to decode any input to 16 kHz mono signed-16-bit WAV. Pure +\n * exported for unit tests. */\nexport function buildWav16kArgs(inputPath: string, outputPath: string): string[] {\n return [\"-y\", \"-loglevel\", \"error\", \"-i\", inputPath, \"-ar\", \"16000\", \"-ac\", \"1\", \"-c:a\", \"pcm_s16le\", outputPath];\n}\n\n/** Convert `inputPath` to a 16 kHz mono WAV at `outputPath`. Throws on ffmpeg\n * failure or timeout. */\nexport async function convertToWav16k(inputPath: string, outputPath: string, ffmpegBinary = \"ffmpeg\"): Promise<void> {\n await execFileAsync(ffmpegBinary, buildWav16kArgs(inputPath, outputPath), {\n timeout: ONE_MINUTE_MS,\n });\n}\n","// Whisper GGML model registry + on-disk management. The host injects the models\n// directory (e.g. `{workspace}/models`); nothing here reads a host module.\n\nimport { createWriteStream, mkdirSync, renameSync, statSync, unlinkSync } from \"node:fs\";\nimport { once } from \"node:events\";\nimport path from \"node:path\";\nimport { errorMessage, NOOP_LOGGER, ONE_MINUTE_MS, type WhisperLogger } from \"./internal.ts\";\n\nexport interface WhisperModelSpec {\n /** GGML filename — identical on disk and in the Hugging Face repo. */\n readonly file: string;\n /** Download URL (Hugging Face whisper.cpp model repo). */\n readonly url: string;\n /** Conservative lower bound on the finished file size, in bytes — guards\n * against a truncated transfer or an HTML error page saved as the model\n * without pinning an exact checksum. */\n readonly minBytes: number;\n}\n\nconst HF_BASE = \"https://huggingface.co/ggerganov/whisper.cpp/resolve/main\";\n\n// large-v3-turbo: strong accuracy, near-real-time on Apple Silicon with Metal.\n// small/base are lighter fallbacks for low-RAM machines.\nexport const WHISPER_MODELS = {\n \"large-v3-turbo\": { file: \"ggml-large-v3-turbo.bin\", url: `${HF_BASE}/ggml-large-v3-turbo.bin`, minBytes: 1_000_000_000 },\n small: { file: \"ggml-small.bin\", url: `${HF_BASE}/ggml-small.bin`, minBytes: 300_000_000 },\n base: { file: \"ggml-base.bin\", url: `${HF_BASE}/ggml-base.bin`, minBytes: 100_000_000 },\n} as const satisfies Record<string, WhisperModelSpec>;\n\nexport type WhisperModelName = keyof typeof WHISPER_MODELS;\nexport const DEFAULT_WHISPER_MODEL: WhisperModelName = \"large-v3-turbo\";\n\nexport function isWhisperModelName(value: unknown): value is WhisperModelName {\n // Own-property check — `in` would accept inherited keys like \"toString\",\n // which then crash the `WHISPER_MODELS[name]` lookups instead of falling\n // back to the default.\n return typeof value === \"string\" && Object.prototype.hasOwnProperty.call(WHISPER_MODELS, value);\n}\n\n/** Resolve a possibly-unset / unknown model name to a valid one. */\nexport function resolveModelName(name: string | undefined): WhisperModelName {\n return isWhisperModelName(name) ? name : DEFAULT_WHISPER_MODEL;\n}\n\nexport function modelFilePath(modelsDir: string, name: WhisperModelName): string {\n return path.join(modelsDir, WHISPER_MODELS[name].file);\n}\n\n/** A model is \"ready\" when its file exists and meets the size floor. */\nexport function isModelReady(modelsDir: string, name: WhisperModelName): boolean {\n try {\n return statSync(modelFilePath(modelsDir, name)).size >= WHISPER_MODELS[name].minBytes;\n } catch {\n return false;\n }\n}\n\nexport type ModelDownloadState = \"idle\" | \"downloading\" | \"ready\" | \"error\";\n\nexport interface ModelStatus {\n state: ModelDownloadState;\n /** 0..1 — present only while downloading and Content-Length is known. */\n progress?: number;\n /** Present only in the \"error\" state. */\n error?: string;\n}\n\n/** Report status precedence: an in-flight download wins, then an on-disk ready\n * file, then the last transient state (idle if none). Pure so the precedence\n * is unit-testable without touching the filesystem. */\nexport function pickModelStatus(live: ModelStatus | undefined, isReady: boolean): ModelStatus {\n if (live?.state === \"downloading\") return live;\n if (isReady) return { state: \"ready\" };\n return live ?? { state: \"idle\" };\n}\n\n/** Parse a `Content-Length` header to a byte count; unknown / malformed → 0. */\nexport function parseContentLength(header: string | null): number {\n return Number(header) || 0;\n}\n\n// Abort a download if no bytes arrive for this long (stalled connection).\nconst DOWNLOAD_STALL_TIMEOUT_MS = ONE_MINUTE_MS;\n\nexport interface ModelDownloader {\n getStatus: (name: WhisperModelName) => ModelStatus;\n ensure: (name: WhisperModelName) => Promise<void>;\n}\n\ninterface StreamToFileDeps {\n downloadStatus: Map<WhisperModelName, ModelStatus>;\n onProgress: () => void;\n}\n\n/** Drain the response stream into the `.partial` file, resetting the stall\n * timer on every chunk and publishing progress once the total size is known. */\nasync function streamToFile(\n body: ReadableStream<Uint8Array>,\n partialPath: string,\n total: number,\n name: WhisperModelName,\n deps: StreamToFileDeps,\n): Promise<void> {\n const reader = body.getReader();\n const fileStream = createWriteStream(partialPath);\n let received = 0;\n try {\n for (;;) {\n const { done, value } = await reader.read();\n if (done) break;\n deps.onProgress();\n received += value.byteLength;\n if (!fileStream.write(value)) await once(fileStream, \"drain\");\n if (total > 0) deps.downloadStatus.set(name, { state: \"downloading\", progress: received / total });\n }\n fileStream.end();\n await once(fileStream, \"finish\");\n } catch (err) {\n // `createWriteStream` opens lazily. Destroying it while that open is still\n // in flight lets the open's own failure land later as an `error` event with\n // no listener — an uncaughtException that kills the process. `err` already\n // carries the real cause, so absorb the late one.\n fileStream.on(\"error\", () => {});\n fileStream.destroy();\n throw err;\n }\n}\n\n/** Fetch a model into a `.partial` file, reject a truncated transfer via the\n * size floor, then atomically rename it into place. */\nasync function downloadModel(name: WhisperModelName, modelsDir: string, downloadStatus: Map<WhisperModelName, ModelStatus>): Promise<void> {\n const spec = WHISPER_MODELS[name];\n mkdirSync(modelsDir, { recursive: true });\n const dest = modelFilePath(modelsDir, name);\n const partial = `${dest}.partial`;\n const controller = new AbortController();\n let stallTimer: ReturnType<typeof setTimeout> | null = null;\n const resetStall = () => {\n if (stallTimer) clearTimeout(stallTimer);\n stallTimer = setTimeout(() => controller.abort(), DOWNLOAD_STALL_TIMEOUT_MS);\n };\n try {\n const response = await fetch(spec.url, { signal: controller.signal });\n if (!response.ok || !response.body) {\n throw new Error(`download failed: HTTP ${response.status}`);\n }\n const total = parseContentLength(response.headers.get(\"content-length\"));\n resetStall();\n await streamToFile(response.body, partial, total, name, { downloadStatus, onProgress: resetStall });\n if (statSync(partial).size < spec.minBytes) {\n unlinkSync(partial);\n throw new Error(\"downloaded file is smaller than expected — likely truncated\");\n }\n renameSync(partial, dest);\n } finally {\n if (stallTimer) clearTimeout(stallTimer);\n }\n}\n\nexport function createModelDownloader(modelsDir: string, logger: WhisperLogger = NOOP_LOGGER): ModelDownloader {\n // A finished file on disk is the source of truth for \"ready\"; this map tracks\n // transient progress + the last error.\n const downloadStatus = new Map<WhisperModelName, ModelStatus>();\n\n function getStatus(name: WhisperModelName): ModelStatus {\n return pickModelStatus(downloadStatus.get(name), isModelReady(modelsDir, name));\n }\n\n // Fire-and-forget friendly: errors land in the status map, never thrown.\n // Idempotent — a second call while a download is in flight does nothing.\n async function ensure(name: WhisperModelName): Promise<void> {\n if (isModelReady(modelsDir, name)) {\n downloadStatus.set(name, { state: \"ready\" });\n return;\n }\n if (downloadStatus.get(name)?.state === \"downloading\") return;\n downloadStatus.set(name, { state: \"downloading\", progress: 0 });\n logger.info(\"model download: start\", { model: name });\n try {\n await downloadModel(name, modelsDir, downloadStatus);\n downloadStatus.set(name, { state: \"ready\" });\n logger.info(\"model download: ok\", { model: name });\n } catch (err) {\n const error = errorMessage(err);\n downloadStatus.set(name, { state: \"error\", error });\n logger.error(\"model download: failed\", { model: name, error });\n }\n }\n\n return { getStatus, ensure };\n}\n","// Pure helpers for the whisper-server sidecar. Kept separate from `sidecar.ts`\n// so the argv/response/stderr logic can be unit-tested without spawning a\n// process or opening a socket.\n\n/** whisper-server CLI args to preload `modelPath` and bind to `host:port`. */\nexport function buildServerArgs(modelPath: string, host: string, port: number): string[] {\n return [\"--model\", modelPath, \"--host\", host, \"--port\", String(port)];\n}\n\n/** Append `chunk` to a bounded stderr tail, keeping only the last `maxChars`. */\nexport function appendStderrTail(previous: string, chunk: string, maxChars: number): string {\n return (previous + chunk).slice(-maxChars);\n}\n\n/** Extract the transcript from a whisper-server `/inference` JSON response.\n * Returns \"\" for any shape that lacks a string `text` field. */\nexport function parseInferenceText(data: unknown): string {\n if (typeof data === \"object\" && data !== null && \"text\" in data) {\n const { text } = data as { text: unknown };\n if (typeof text === \"string\") return text;\n }\n return \"\";\n}\n","// whisper.cpp warm-model sidecar. Spawns `whisper-server` once with the model\n// preloaded and reuses it across transcriptions over its local HTTP API, so the\n// weights stay resident (no per-request reload). State is encapsulated per\n// instance via the factory closure.\n\nimport { spawn, type ChildProcess } from \"node:child_process\";\nimport { createServer } from \"node:net\";\nimport { readFile } from \"node:fs/promises\";\nimport { setTimeout as delay } from \"node:timers/promises\";\nimport { errorMessage, NOOP_LOGGER, ONE_MINUTE_MS, ONE_SECOND_MS, type WhisperLogger } from \"./internal.ts\";\nimport { modelFilePath, type WhisperModelName } from \"./models.ts\";\nimport { appendStderrTail, buildServerArgs, parseInferenceText } from \"./sidecar-helpers.ts\";\n\nconst HOST = \"127.0.0.1\";\nconst READY_TIMEOUT_MS = 60 * ONE_SECOND_MS;\nconst READY_POLL_INTERVAL_MS = 500;\nconst INFERENCE_TIMEOUT_MS = 2 * ONE_MINUTE_MS;\nconst STDERR_TAIL_MAX_CHARS = 4_000;\n\nexport interface ActiveSidecar {\n readonly port: number;\n readonly proc: ChildProcess;\n readonly model: WhisperModelName;\n}\n\nexport interface Sidecar {\n transcribeWav: (wavPath: string, language: string, model: WhisperModelName) => Promise<string>;\n warmup: (model: WhisperModelName) => Promise<void>;\n shutdown: () => void;\n}\n\nasync function findFreePort(): Promise<number> {\n return new Promise((resolve, reject) => {\n const srv = createServer();\n srv.on(\"error\", reject);\n srv.listen(0, HOST, () => {\n const addr = srv.address();\n if (addr && typeof addr === \"object\") {\n const { port } = addr;\n srv.close(() => resolve(port));\n } else {\n srv.close(() => reject(new Error(\"could not determine a free port\")));\n }\n });\n });\n}\n\n/** Resolve once the server answers any HTTP request (any status = listener up),\n * or throw after the ready timeout. */\nasync function waitUntilReady(port: number): Promise<void> {\n const deadline = Date.now() + READY_TIMEOUT_MS;\n while (Date.now() < deadline) {\n try {\n await fetch(`http://${HOST}:${port}/`, { signal: AbortSignal.timeout(ONE_SECOND_MS) });\n return;\n } catch {\n await delay(READY_POLL_INTERVAL_MS);\n }\n }\n throw new Error(\"whisper-server did not become ready in time\");\n}\n\n// whisper-server logs verbosely to stderr; left unread the OS pipe buffer fills\n// and the child blocks on its next write. Drain into a small tail buffer.\nfunction drainStderr(proc: ChildProcess, tail: { text: string }): void {\n proc.stderr?.setEncoding(\"utf8\");\n proc.stderr?.on(\"data\", (chunk: string) => {\n tail.text = appendStderrTail(tail.text, chunk, STDERR_TAIL_MAX_CHARS);\n });\n}\n\n// Resolve when the server answers, or reject on spawn failure (e.g. ENOENT) /\n// early exit. Listeners are one-shot and removed once the race settles.\nfunction waitForReadyOrFailure(proc: ChildProcess, port: number): Promise<void> {\n return new Promise<void>((resolve, reject) => {\n let onError: (err: Error) => void = () => undefined;\n let onExit: (code: number | null) => void = () => undefined;\n const cleanup = () => {\n proc.removeListener(\"error\", onError);\n proc.removeListener(\"exit\", onExit);\n };\n onError = (err: Error) => {\n cleanup();\n reject(new Error(`spawn failed: ${errorMessage(err)}`));\n };\n onExit = (code: number | null) => {\n cleanup();\n reject(new Error(`exited early (code ${code})`));\n };\n proc.once(\"error\", onError);\n proc.once(\"exit\", onExit);\n waitUntilReady(port)\n .then(() => {\n cleanup();\n resolve();\n })\n .catch((err: unknown) => {\n cleanup();\n reject(err instanceof Error ? err : new Error(String(err)));\n });\n });\n}\n\n// POST the wav to whisper-server's `/inference` and return the transcript.\n// Isolated from the lifecycle so the HTTP contract is unit-testable without a\n// live child (stub `fetch`, hand it a real wav path).\nexport async function postInference(port: number, wavPath: string, language: string): Promise<string> {\n const buf = await readFile(wavPath);\n const form = new FormData();\n form.append(\"file\", new Blob([buf], { type: \"audio/wav\" }), \"audio.wav\");\n form.append(\"response_format\", \"json\");\n form.append(\"language\", language || \"auto\");\n let res: Response;\n try {\n res = await fetch(`http://${HOST}:${port}/inference`, { method: \"POST\", body: form, signal: AbortSignal.timeout(INFERENCE_TIMEOUT_MS) });\n } catch (err) {\n throw new Error(`whisper-server request failed: ${errorMessage(err)}`);\n }\n if (!res.ok) throw new Error(`whisper-server returned HTTP ${res.status}`);\n return parseInferenceText(await res.json());\n}\n\n// The impure primitives the start lifecycle depends on, injected so the\n// cancellation / single-flight / model-switch logic can be driven with fakes.\nexport interface StartLifecycleDeps {\n /** Reserve a free TCP port for the next child — the sole async step of a start. */\n allocatePort: () => Promise<number>;\n /** Spawn the whisper-server child on `port`. MUST be synchronous so the spawn\n * and the `startingProc` handoff stay in one tick: `shutdown()` has to be able\n * to kill an in-flight child immediately, with no await gap after it exists. */\n spawnServer: (model: WhisperModelName, port: number) => ChildProcess;\n /** Resolve once the child answers HTTP, or reject on spawn failure / early exit. */\n waitReady: (proc: ChildProcess, port: number) => Promise<void>;\n logger: WhisperLogger;\n}\n\nexport interface StartLifecycle {\n ensureSidecar: (model: WhisperModelName) => Promise<ActiveSidecar>;\n shutdown: () => void;\n}\n\nexport function defaultSpawnServer(modelsDir: string, serverBinary: string, logger: WhisperLogger): StartLifecycleDeps[\"spawnServer\"] {\n return (model: WhisperModelName, port: number): ChildProcess => {\n const args = buildServerArgs(modelFilePath(modelsDir, model), HOST, port);\n logger.info(\"sidecar: spawning\", { model, port });\n return spawn(serverBinary, args, { stdio: [\"ignore\", \"ignore\", \"pipe\"] });\n };\n}\n\n// Wires the three listeners every whisper-server child needs: stderr drain,\n// permanent `error` handler (an unhandled 'error' from ENOENT etc. would crash\n// the host), and `exit` handler that both logs and lets the caller decide\n// whether this proc is still the active one (identity check via `onExit` — a\n// stale process must not evict a newer live sidecar).\nfunction instrumentChildProcess(proc: ChildProcess, model: WhisperModelName, logger: WhisperLogger, onExit: (exited: ChildProcess) => void): { text: string } {\n const stderrTail = { text: \"\" };\n drainStderr(proc, stderrTail);\n proc.on(\"error\", (err) => logger.warn(\"sidecar: process error\", { model, error: errorMessage(err) }));\n proc.on(\"exit\", (code) => {\n logger.warn(\"sidecar: exited\", { model, code, stderrTail: stderrTail.text.slice(-500) });\n onExit(proc);\n });\n return stderrTail;\n}\n\n// Await server readiness; on failure, kill the child and surface both the\n// underlying error and the tail of stderr (whisper-server usually explains its\n// crash there). Kept at module scope so `startSidecar` stays under the\n// per-function line cap.\nasync function awaitReadyOrThrow(proc: ChildProcess, port: number, stderrTail: { text: string }, waitReady: StartLifecycleDeps[\"waitReady\"]): Promise<void> {\n try {\n await waitReady(proc, port);\n } catch (err) {\n proc.kill();\n throw new Error(`whisper-server failed to start: ${errorMessage(err)} — stderr: ${stderrTail.text.slice(-500)}`);\n }\n}\n\n// Abort a start that was raced by `shutdown()` (or a newer start). Compares\n// the token captured at start entry against the current token; if they differ,\n// kill the freshly-booted child rather than publish it after shutdown returned.\nfunction throwIfStartCancelled(startedToken: number, currentToken: number, proc: ChildProcess): void {\n if (startedToken !== currentToken) {\n proc.kill();\n throw new Error(\"whisper-server start cancelled\");\n }\n}\n\n// Owns the single warm child's lifecycle: which model is live, the in-flight\n// start, and the cancellation token. Kept as a factory closure so the mutable\n// state is encapsulated rather than threaded through parameters.\nexport function createStartLifecycle(deps: StartLifecycleDeps): StartLifecycle {\n const { allocatePort, spawnServer, waitReady, logger } = deps;\n let sidecar: ActiveSidecar | null = null;\n let starting: { model: WhisperModelName; promise: Promise<ActiveSidecar> } | null = null;\n // The child of an in-flight start (before it's published as `sidecar`), plus a\n // token that `shutdown()` bumps to cancel a start that's still booting — so\n // shutdown can't return with a child that then publishes itself afterwards.\n let startingProc: ChildProcess | null = null;\n let startToken = 0;\n\n function shutdown(): void {\n startToken += 1;\n startingProc?.kill();\n startingProc = null;\n sidecar?.proc.kill();\n sidecar = null;\n }\n\n async function startSidecar(model: WhisperModelName): Promise<ActiveSidecar> {\n const token = ++startToken;\n const port = await allocatePort();\n const proc = spawnServer(model, port);\n startingProc = proc;\n // onExit fires when THIS proc exits: only null out `sidecar` if it's still the live one.\n const stderrTail = instrumentChildProcess(proc, model, logger, (exited) => sidecar?.proc === exited && (sidecar = null));\n try {\n await awaitReadyOrThrow(proc, port, stderrTail, waitReady);\n } finally {\n if (startingProc === proc) startingProc = null;\n }\n throwIfStartCancelled(token, startToken, proc);\n sidecar = { port, proc, model };\n logger.info(\"sidecar: ready\", { model, port });\n return sidecar;\n }\n\n // Own function so it isn't a loop closure (no-loop-func). Only ever one start\n // is in flight at a time, so clearing `starting` on settle is safe.\n function beginStart(model: WhisperModelName): Promise<ActiveSidecar> {\n const promise = startSidecar(model).finally(() => (starting = null));\n starting = { model, promise };\n return promise;\n }\n\n async function ensureSidecar(model: WhisperModelName): Promise<ActiveSidecar> {\n // Loop so that after awaiting an in-flight start for a DIFFERENT model we\n // re-evaluate; the decision-to-spawn path (beginStart) has no await, so the\n // first waiter sets `starting` synchronously and siblings then reuse it.\n for (;;) {\n if (sidecar && sidecar.model === model && !sidecar.proc.killed) return sidecar;\n if (starting && starting.model === model) return starting.promise;\n if (starting) {\n await starting.promise.catch(() => undefined);\n continue;\n }\n if (sidecar && sidecar.model !== model) shutdown();\n return beginStart(model);\n }\n }\n\n return { ensureSidecar, shutdown };\n}\n\nexport function createSidecar(modelsDir: string, serverBinary = \"whisper-server\", logger: WhisperLogger = NOOP_LOGGER): Sidecar {\n const lifecycle = createStartLifecycle({\n allocatePort: findFreePort,\n spawnServer: defaultSpawnServer(modelsDir, serverBinary, logger),\n waitReady: waitForReadyOrFailure,\n logger,\n });\n\n async function warmup(model: WhisperModelName): Promise<void> {\n try {\n await lifecycle.ensureSidecar(model);\n } catch (err) {\n logger.warn(\"sidecar: warmup failed\", { model, error: errorMessage(err) });\n }\n }\n\n async function transcribeWav(wavPath: string, language: string, model: WhisperModelName): Promise<string> {\n const active = await lifecycle.ensureSidecar(model);\n return postInference(active.port, wavPath, language);\n }\n\n return { transcribeWav, warmup, shutdown: lifecycle.shutdown };\n}\n","// Public server-side façade: wires the model downloader, the warm sidecar, and\n// ffmpeg conversion into one host-agnostic service. The host injects the models\n// directory + a logger and gates capability itself (platform / binary presence);\n// this package assumes the binaries exist when called.\n\nimport { mkdirSync } from \"node:fs\";\nimport { rm, writeFile } from \"node:fs/promises\";\nimport { randomUUID } from \"node:crypto\";\nimport path from \"node:path\";\nimport { NOOP_LOGGER, type WhisperLogger } from \"./internal.ts\";\nimport { convertToWav16k } from \"./ffmpeg.ts\";\nimport { createModelDownloader } from \"./models.ts\";\nimport { createSidecar } from \"./sidecar.ts\";\nimport { isModelReady, type ModelStatus, type WhisperModelName } from \"./models.ts\";\n\nexport interface WhisperOptions {\n /** Directory that holds the GGML model files (e.g. `{workspace}/models`). */\n modelsDir: string;\n logger?: WhisperLogger;\n /** Defaults to \"whisper-server\" / \"ffmpeg\" on PATH. */\n serverBinary?: string;\n ffmpegBinary?: string;\n}\n\nexport interface TranscribeRequest {\n base64: string;\n mimeType: string;\n language: string;\n model: WhisperModelName;\n}\n\nexport interface Whisper {\n isModelReady: (model: WhisperModelName) => boolean;\n getModelStatus: (model: WhisperModelName) => ModelStatus;\n /** Fire-and-forget friendly; never throws (errors land in the status). */\n ensureModelDownloaded: (model: WhisperModelName) => Promise<void>;\n warmup: (model: WhisperModelName) => Promise<void>;\n transcribe: (req: TranscribeRequest) => Promise<{ text: string }>;\n shutdown: () => void;\n}\n\n// whisper.cpp returns these sentinels for non-speech windows; treat them as\n// empty so the UI shows \"didn't catch that\" rather than a literal marker.\nconst BLANK_MARKERS = new Set([\"[blank_audio]\", \"[silence]\", \"(silence)\", \"[ inaudible ]\"]);\n\nfunction normalizeTranscript(raw: string): string {\n const trimmed = raw.trim().replace(/\\s+/g, \" \");\n return BLANK_MARKERS.has(trimmed.toLowerCase()) ? \"\" : trimmed;\n}\n\nexport function createWhisper(opts: WhisperOptions): Whisper {\n const { modelsDir } = opts;\n const logger = opts.logger ?? NOOP_LOGGER;\n const ffmpegBinary = opts.ffmpegBinary ?? \"ffmpeg\";\n const downloader = createModelDownloader(modelsDir, logger);\n const sidecar = createSidecar(modelsDir, opts.serverBinary ?? \"whisper-server\", logger);\n\n // Scratch dir for transient audio — a hidden subdir of the models dir so it\n // shares the (non-git) models tree. Files are deleted after each transcription.\n const scratchDir = path.join(modelsDir, \".scratch\");\n\n async function transcribe(req: TranscribeRequest): Promise<{ text: string }> {\n mkdirSync(scratchDir, { recursive: true });\n const clipId = randomUUID();\n const inputPath = path.join(scratchDir, `utterance-${clipId}.webm`);\n const wavPath = path.join(scratchDir, `utterance-${clipId}.wav`);\n try {\n await writeFile(inputPath, Buffer.from(req.base64, \"base64\"));\n await convertToWav16k(inputPath, wavPath, ffmpegBinary);\n const text = await sidecar.transcribeWav(wavPath, req.language, req.model);\n return { text: normalizeTranscript(text) };\n } finally {\n await rm(inputPath, { force: true });\n await rm(wavPath, { force: true });\n }\n }\n\n return {\n isModelReady: (model) => isModelReady(modelsDir, model),\n getModelStatus: (model) => downloader.getStatus(model),\n ensureModelDownloaded: (model) => downloader.ensure(model),\n warmup: (model) => sidecar.warmup(model),\n transcribe,\n shutdown: () => sidecar.shutdown(),\n };\n}\n"],"mappings":"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"}
@@ -1,8 +1,8 @@
1
- import { t as errorMessage } from "../errors-eid6Mes3.js";
2
- import path from "node:path";
1
+ import { t as errorMessage } from "../dist-Cwk0e12G.js";
3
2
  import { createWriteStream, mkdirSync, renameSync, statSync, unlinkSync } from "node:fs";
4
- import { readFile, rm, writeFile } from "node:fs/promises";
3
+ import path from "node:path";
5
4
  import { randomUUID } from "node:crypto";
5
+ import { readFile, rm, writeFile } from "node:fs/promises";
6
6
  import { createServer } from "node:net";
7
7
  import { execFile, spawn } from "node:child_process";
8
8
  import { promisify } from "node:util";
@@ -1,4 +1,5 @@
1
1
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
+ const require_dist = require("../dist-pWpC-b04.cjs");
2
3
  const require_graph = require("../graph-2R4HbQAg.cjs");
3
4
  const require_slug = require("../slug-DbwORN9z.cjs");
4
5
  //#region src/wiki/route.ts
@@ -72,20 +73,6 @@ function wikiActionFor(target) {
72
73
  }
73
74
  //#endregion
74
75
  //#region src/wiki/render.ts
75
- /** HTML-escape attribute / text content. Self-contained on purpose:
76
- * this package can't reach the host's escaper, and the rule is a
77
- * fixed five-char map. */
78
- function escapeHtml(value) {
79
- return value.replace(/[&<>"']/g, (char) => {
80
- switch (char) {
81
- case "&": return "&amp;";
82
- case "<": return "&lt;";
83
- case ">": return "&gt;";
84
- case "\"": return "&quot;";
85
- default: return "&#39;";
86
- }
87
- });
88
- }
89
76
  /**
90
77
  * Replace every `[[page name]]` occurrence in `content` with a
91
78
  * `<span class="wiki-link" data-page="…">…</span>` element. The
@@ -108,7 +95,7 @@ function renderWikiLinks(content) {
108
95
  const closeStart = findNextCloseBrackets(content, i + 2);
109
96
  if (closeStart !== -1) {
110
97
  const { target, display } = require_graph.parseWikiLink(content.slice(i + 2, closeStart));
111
- out.push(`<span class="wiki-link" data-page="${escapeHtml(target)}">${escapeHtml(display)}</span>`);
98
+ out.push(`<span class="wiki-link" data-page="${require_dist.escapeHtml(target)}">${require_dist.escapeHtml(display)}</span>`);
112
99
  i = closeStart + 2;
113
100
  continue;
114
101
  }
@@ -148,7 +135,7 @@ exports.WIKI_ROUTE_SECTION = WIKI_ROUTE_SECTION;
148
135
  exports.buildTableColumnMap = require_graph.buildTableColumnMap;
149
136
  exports.buildWikiGraph = require_graph.buildWikiGraph;
150
137
  exports.buildWikiRouteParams = buildWikiRouteParams;
151
- exports.escapeHtml = escapeHtml;
138
+ exports.escapeHtml = require_dist.escapeHtml;
152
139
  exports.extractHashTags = require_graph.extractHashTags;
153
140
  exports.extractSlugFromBulletHref = require_graph.extractSlugFromBulletHref;
154
141
  exports.findBrokenLinksInPage = require_graph.findBrokenLinksInPage;
@@ -1 +1 @@
1
- {"version":3,"file":"index.cjs","names":[],"sources":["../../src/wiki/route.ts","../../src/wiki/render.ts"],"sourcesContent":["// Pure helpers for reading, building, and validating wiki route\n// params. Kept free of Vue / vue-router imports so it can be shared\n// across hosts (MulmoClaude + MulmoTerminal) from\n// `@mulmoclaude/core/wiki`:\n//\n// - the host router guard (synchronous validation at navigation time)\n// - the wiki View (watcher + push helpers)\n// - the workspace-link click handler\n// - unit tests\n//\n// Mirrors the \"one file owns the URL ↔ domain mapping\" pattern so the\n// literals don't drift across the router definition, guards, views,\n// and tests.\n\nimport { isSafeSlug } from \"./slug.js\";\n\n// URL segment used in `/wiki/:section/...`. Closed enum — the router\n// regex `(pages|log|lint-report)` rejects anything else.\nexport const WIKI_ROUTE_SECTION = {\n pages: \"pages\",\n log: \"log\",\n lintReport: \"lint-report\",\n graph: \"graph\",\n} as const;\n\nexport type WikiRouteSection = (typeof WIKI_ROUTE_SECTION)[keyof typeof WIKI_ROUTE_SECTION];\n\n// Internal action name sent to the server / shown in the View. Diverges\n// from the URL segment in one place only: `lint-report` (URL) vs\n// `lint_report` (action), because the server API still speaks the\n// underscore form.\nexport const WIKI_ACTION = {\n index: \"index\",\n page: \"page\",\n log: \"log\",\n lintReport: \"lint_report\",\n graph: \"graph\",\n save: \"save\",\n} as const;\n\nexport type WikiAction = (typeof WIKI_ACTION)[keyof typeof WIKI_ACTION];\n\n// Route-level representation. `pushWiki(target)` and\n// `readWikiRouteTarget(params)` both speak this so the watcher, the\n// button handlers, and the router guard agree on the same shape.\nexport type WikiTarget = { kind: \"index\" } | { kind: \"page\"; slug: string } | { kind: \"log\" } | { kind: \"lint_report\" } | { kind: \"graph\" };\n\n// Reject anything that could escape `data/wiki/pages/` or collide\n// with a different page. Vue Router decodes `%2F` back to `/` in\n// `route.params.slug`, so `/wiki/pages/..%2Fsecrets` lands here as\n// `slug === \"../secrets\"` — this check is the last line of defence\n// before the slug is passed to the server's page resolver. Non-\n// ASCII characters (e.g. Japanese page titles) are allowed; only\n// separators and the literal `.` / `..` are blocked.\n//\n// Delegates the actual rule to `isSafeSlug` in\n// `src/lib/wiki-page/slug.ts` (imported at the top of this file)\n// so server and frontend share one implementation (#1297). The\n// wrapper adds the type-guard / non-string-input handling the\n// router needs.\nexport function isSafeWikiSlug(value: unknown): value is string {\n return typeof value === \"string\" && isSafeSlug(value);\n}\n\n// Read `route.params` from the wiki route and normalise to a\n// `WikiTarget`. Returns `null` when the params describe an invalid\n// state (unknown section, missing slug for a page view, unsafe slug)\n// so the caller can decide what to do — the router guard redirects to\n// `/wiki`, the view watcher treats it as \"render the index\".\nexport function readWikiRouteTarget(params: unknown): WikiTarget | null {\n if (!params || typeof params !== \"object\") return null;\n const { section, slug } = params as { section?: unknown; slug?: unknown };\n\n if (section === undefined || section === \"\") return { kind: \"index\" };\n\n if (section === WIKI_ROUTE_SECTION.pages) {\n if (!isSafeWikiSlug(slug)) return null;\n return { kind: \"page\", slug };\n }\n if (section === WIKI_ROUTE_SECTION.log) return { kind: \"log\" };\n if (section === WIKI_ROUTE_SECTION.lintReport) return { kind: \"lint_report\" };\n if (section === WIKI_ROUTE_SECTION.graph) return { kind: \"graph\" };\n\n return null;\n}\n\n// Inverse of `readWikiRouteTarget`: given a target, produce the\n// `{ section, slug }` params object that `router.push({ name: \"wiki\",\n// params })` needs.\n//\n// Optional route params are NOT cleared by named-route navigation\n// unless explicitly set — returning `{}` for `kind: \"index\"` would\n// leak the previous `section`/`slug` into the URL when navigating\n// from `/wiki/pages/foo` back to the index, leaving the user on\n// `/wiki/pages/foo` with the View believing it's the index. Pass\n// empty strings so the router writes out `/wiki` cleanly. The\n// readWikiRouteTarget() branch for `section === \"\"` already\n// normalises those back to `{ kind: \"index\" }`.\nexport function buildWikiRouteParams(target: WikiTarget): Record<string, string> {\n switch (target.kind) {\n case \"index\":\n return { section: \"\", slug: \"\" };\n case \"page\":\n return { section: WIKI_ROUTE_SECTION.pages, slug: target.slug };\n case \"log\":\n return { section: WIKI_ROUTE_SECTION.log, slug: \"\" };\n case \"lint_report\":\n return { section: WIKI_ROUTE_SECTION.lintReport, slug: \"\" };\n case \"graph\":\n return { section: WIKI_ROUTE_SECTION.graph, slug: \"\" };\n default: {\n const exhaustive: never = target;\n throw new Error(`unreachable WikiTarget kind: ${JSON.stringify(exhaustive)}`);\n }\n }\n}\n\n// Resolve a target to the action name the server expects. Centralises\n// the one place URL-shape and action-shape diverge (`lint-report` ↔\n// `lint_report`).\nexport function wikiActionFor(target: WikiTarget): WikiAction {\n switch (target.kind) {\n case \"index\":\n return WIKI_ACTION.index;\n case \"page\":\n return WIKI_ACTION.page;\n case \"log\":\n return WIKI_ACTION.log;\n case \"lint_report\":\n return WIKI_ACTION.lintReport;\n case \"graph\":\n return WIKI_ACTION.graph;\n default: {\n const exhaustive: never = target;\n throw new Error(`unreachable WikiTarget kind: ${JSON.stringify(exhaustive)}`);\n }\n }\n}\n","// Pure `[[wiki-link]]` → HTML renderer, shared across hosts via\n// `@mulmoclaude/core/wiki`. Extracted from the MulmoClaude host's\n// `src/plugins/wiki/helpers.ts` so MulmoTerminal can render the same\n// internal-link markup without forking the walker.\n//\n// String-only, no `marked` / DOM / Node deps — the host pipeline\n// (image-ref rewrite, marked.parse, task-interactive) wraps this.\n\nimport { parseWikiLink } from \"./link.js\";\n\n/** HTML-escape attribute / text content. Self-contained on purpose:\n * this package can't reach the host's escaper, and the rule is a\n * fixed five-char map. */\nexport function escapeHtml(value: string): string {\n return value.replace(/[&<>\"']/g, (char) => {\n switch (char) {\n case \"&\":\n return \"&amp;\";\n case \"<\":\n return \"&lt;\";\n case \">\":\n return \"&gt;\";\n case '\"':\n return \"&quot;\";\n default:\n return \"&#39;\";\n }\n });\n}\n\n/**\n * Replace every `[[page name]]` occurrence in `content` with a\n * `<span class=\"wiki-link\" data-page=\"…\">…</span>` element. The\n * page name may not contain `]`; an opening `[[` that is not\n * followed later by `]]` (with no bare `]` in between) is left\n * untouched so malformed text renders as-is — matching the\n * previous regex's non-match behaviour.\n *\n * `[[target|display]]` is split via the shared `parseWikiLink`\n * helper so `data-page` carries only the target slug while the\n * visible text shows the display half (#1297). Both halves are\n * HTML-escaped before interpolation — `parseWikiLink` runs BEFORE\n * the host's `marked.parse`, so escaping has to happen here.\n */\nexport function renderWikiLinks(content: string): string {\n const out: string[] = [];\n let i = 0;\n while (i < content.length) {\n if (content[i] === \"[\" && content[i + 1] === \"[\") {\n const closeStart = findNextCloseBrackets(content, i + 2);\n if (closeStart !== -1) {\n const inner = content.slice(i + 2, closeStart);\n const { target, display } = parseWikiLink(inner);\n out.push(`<span class=\"wiki-link\" data-page=\"${escapeHtml(target)}\">${escapeHtml(display)}</span>`);\n i = closeStart + 2;\n continue;\n }\n }\n out.push(content[i]);\n i++;\n }\n return out.join(\"\");\n}\n\n/**\n * Starting at `from`, scan forward for a `]]` sequence. Returns\n * the index of the first `]` of that pair, or -1 if a bare `]`\n * (one not immediately followed by a second `]`) is encountered\n * first — mirroring the old regex's `[^\\]]+` constraint that the\n * page name must contain no `]` characters. Also returns -1 if\n * nothing matched before the end of input, or if the pair sits\n * immediately after `from` (zero-length page name, which the old\n * regex rejected via the `+` quantifier).\n */\nfunction findNextCloseBrackets(str: string, from: number): number {\n let j = from;\n while (j < str.length) {\n if (str[j] === \"]\") {\n if (str[j + 1] === \"]\" && j > from) return j;\n // Bare `]` inside the page-name span — old regex would not\n // match here, so we bail and let the caller emit the `[[`\n // as literal text.\n return -1;\n }\n j++;\n }\n return -1;\n}\n"],"mappings":";;;;AAkBA,IAAa,qBAAqB;CAChC,OAAO;CACP,KAAK;CACL,YAAY;CACZ,OAAO;AACT;AAQA,IAAa,cAAc;CACzB,OAAO;CACP,MAAM;CACN,KAAK;CACL,YAAY;CACZ,OAAO;CACP,MAAM;AACR;AAsBA,SAAgB,eAAe,OAAiC;CAC9D,OAAO,OAAO,UAAU,YAAY,aAAA,WAAW,KAAK;AACtD;AAOA,SAAgB,oBAAoB,QAAoC;CACtE,IAAI,CAAC,UAAU,OAAO,WAAW,UAAU,OAAO;CAClD,MAAM,EAAE,SAAS,SAAS;CAE1B,IAAI,YAAY,KAAA,KAAa,YAAY,IAAI,OAAO,EAAE,MAAM,QAAQ;CAEpE,IAAI,YAAY,mBAAmB,OAAO;EACxC,IAAI,CAAC,eAAe,IAAI,GAAG,OAAO;EAClC,OAAO;GAAE,MAAM;GAAQ;EAAK;CAC9B;CACA,IAAI,YAAY,mBAAmB,KAAK,OAAO,EAAE,MAAM,MAAM;CAC7D,IAAI,YAAY,mBAAmB,YAAY,OAAO,EAAE,MAAM,cAAc;CAC5E,IAAI,YAAY,mBAAmB,OAAO,OAAO,EAAE,MAAM,QAAQ;CAEjE,OAAO;AACT;AAcA,SAAgB,qBAAqB,QAA4C;CAC/E,QAAQ,OAAO,MAAf;EACE,KAAK,SACH,OAAO;GAAE,SAAS;GAAI,MAAM;EAAG;EACjC,KAAK,QACH,OAAO;GAAE,SAAS,mBAAmB;GAAO,MAAM,OAAO;EAAK;EAChE,KAAK,OACH,OAAO;GAAE,SAAS,mBAAmB;GAAK,MAAM;EAAG;EACrD,KAAK,eACH,OAAO;GAAE,SAAS,mBAAmB;GAAY,MAAM;EAAG;EAC5D,KAAK,SACH,OAAO;GAAE,SAAS,mBAAmB;GAAO,MAAM;EAAG;EACvD,SAEE,MAAM,IAAI,MAAM,gCAAgC,KAAK,UAAU,MAAU,GAAG;CAEhF;AACF;AAKA,SAAgB,cAAc,QAAgC;CAC5D,QAAQ,OAAO,MAAf;EACE,KAAK,SACH,OAAO,YAAY;EACrB,KAAK,QACH,OAAO,YAAY;EACrB,KAAK,OACH,OAAO,YAAY;EACrB,KAAK,eACH,OAAO,YAAY;EACrB,KAAK,SACH,OAAO,YAAY;EACrB,SAEE,MAAM,IAAI,MAAM,gCAAgC,KAAK,UAAU,MAAU,GAAG;CAEhF;AACF;;;;;;AC5HA,SAAgB,WAAW,OAAuB;CAChD,OAAO,MAAM,QAAQ,aAAa,SAAS;EACzC,QAAQ,MAAR;GACE,KAAK,KACH,OAAO;GACT,KAAK,KACH,OAAO;GACT,KAAK,KACH,OAAO;GACT,KAAK,MACH,OAAO;GACT,SACE,OAAO;EACX;CACF,CAAC;AACH;;;;;;;;;;;;;;;AAgBA,SAAgB,gBAAgB,SAAyB;CACvD,MAAM,MAAgB,CAAC;CACvB,IAAI,IAAI;CACR,OAAO,IAAI,QAAQ,QAAQ;EACzB,IAAI,QAAQ,OAAO,OAAO,QAAQ,IAAI,OAAO,KAAK;GAChD,MAAM,aAAa,sBAAsB,SAAS,IAAI,CAAC;GACvD,IAAI,eAAe,IAAI;IAErB,MAAM,EAAE,QAAQ,YAAY,cAAA,cADd,QAAQ,MAAM,IAAI,GAAG,UACO,CAAK;IAC/C,IAAI,KAAK,sCAAsC,WAAW,MAAM,EAAE,IAAI,WAAW,OAAO,EAAE,QAAQ;IAClG,IAAI,aAAa;IACjB;GACF;EACF;EACA,IAAI,KAAK,QAAQ,EAAE;EACnB;CACF;CACA,OAAO,IAAI,KAAK,EAAE;AACpB;;;;;;;;;;;AAYA,SAAS,sBAAsB,KAAa,MAAsB;CAChE,IAAI,IAAI;CACR,OAAO,IAAI,IAAI,QAAQ;EACrB,IAAI,IAAI,OAAO,KAAK;GAClB,IAAI,IAAI,IAAI,OAAO,OAAO,IAAI,MAAM,OAAO;GAI3C,OAAO;EACT;EACA;CACF;CACA,OAAO;AACT"}
1
+ {"version":3,"file":"index.cjs","names":[],"sources":["../../src/wiki/route.ts","../../src/wiki/render.ts"],"sourcesContent":["// Pure helpers for reading, building, and validating wiki route\n// params. Kept free of Vue / vue-router imports so it can be shared\n// across hosts (MulmoClaude + MulmoTerminal) from\n// `@mulmoclaude/core/wiki`:\n//\n// - the host router guard (synchronous validation at navigation time)\n// - the wiki View (watcher + push helpers)\n// - the workspace-link click handler\n// - unit tests\n//\n// Mirrors the \"one file owns the URL ↔ domain mapping\" pattern so the\n// literals don't drift across the router definition, guards, views,\n// and tests.\n\nimport { isSafeSlug } from \"./slug.js\";\n\n// URL segment used in `/wiki/:section/...`. Closed enum — the router\n// regex `(pages|log|lint-report)` rejects anything else.\nexport const WIKI_ROUTE_SECTION = {\n pages: \"pages\",\n log: \"log\",\n lintReport: \"lint-report\",\n graph: \"graph\",\n} as const;\n\nexport type WikiRouteSection = (typeof WIKI_ROUTE_SECTION)[keyof typeof WIKI_ROUTE_SECTION];\n\n// Internal action name sent to the server / shown in the View. Diverges\n// from the URL segment in one place only: `lint-report` (URL) vs\n// `lint_report` (action), because the server API still speaks the\n// underscore form.\nexport const WIKI_ACTION = {\n index: \"index\",\n page: \"page\",\n log: \"log\",\n lintReport: \"lint_report\",\n graph: \"graph\",\n save: \"save\",\n} as const;\n\nexport type WikiAction = (typeof WIKI_ACTION)[keyof typeof WIKI_ACTION];\n\n// Route-level representation. `pushWiki(target)` and\n// `readWikiRouteTarget(params)` both speak this so the watcher, the\n// button handlers, and the router guard agree on the same shape.\nexport type WikiTarget = { kind: \"index\" } | { kind: \"page\"; slug: string } | { kind: \"log\" } | { kind: \"lint_report\" } | { kind: \"graph\" };\n\n// Reject anything that could escape `data/wiki/pages/` or collide\n// with a different page. Vue Router decodes `%2F` back to `/` in\n// `route.params.slug`, so `/wiki/pages/..%2Fsecrets` lands here as\n// `slug === \"../secrets\"` — this check is the last line of defence\n// before the slug is passed to the server's page resolver. Non-\n// ASCII characters (e.g. Japanese page titles) are allowed; only\n// separators and the literal `.` / `..` are blocked.\n//\n// Delegates the actual rule to `isSafeSlug` in\n// `src/lib/wiki-page/slug.ts` (imported at the top of this file)\n// so server and frontend share one implementation (#1297). The\n// wrapper adds the type-guard / non-string-input handling the\n// router needs.\nexport function isSafeWikiSlug(value: unknown): value is string {\n return typeof value === \"string\" && isSafeSlug(value);\n}\n\n// Read `route.params` from the wiki route and normalise to a\n// `WikiTarget`. Returns `null` when the params describe an invalid\n// state (unknown section, missing slug for a page view, unsafe slug)\n// so the caller can decide what to do — the router guard redirects to\n// `/wiki`, the view watcher treats it as \"render the index\".\nexport function readWikiRouteTarget(params: unknown): WikiTarget | null {\n if (!params || typeof params !== \"object\") return null;\n const { section, slug } = params as { section?: unknown; slug?: unknown };\n\n if (section === undefined || section === \"\") return { kind: \"index\" };\n\n if (section === WIKI_ROUTE_SECTION.pages) {\n if (!isSafeWikiSlug(slug)) return null;\n return { kind: \"page\", slug };\n }\n if (section === WIKI_ROUTE_SECTION.log) return { kind: \"log\" };\n if (section === WIKI_ROUTE_SECTION.lintReport) return { kind: \"lint_report\" };\n if (section === WIKI_ROUTE_SECTION.graph) return { kind: \"graph\" };\n\n return null;\n}\n\n// Inverse of `readWikiRouteTarget`: given a target, produce the\n// `{ section, slug }` params object that `router.push({ name: \"wiki\",\n// params })` needs.\n//\n// Optional route params are NOT cleared by named-route navigation\n// unless explicitly set — returning `{}` for `kind: \"index\"` would\n// leak the previous `section`/`slug` into the URL when navigating\n// from `/wiki/pages/foo` back to the index, leaving the user on\n// `/wiki/pages/foo` with the View believing it's the index. Pass\n// empty strings so the router writes out `/wiki` cleanly. The\n// readWikiRouteTarget() branch for `section === \"\"` already\n// normalises those back to `{ kind: \"index\" }`.\nexport function buildWikiRouteParams(target: WikiTarget): Record<string, string> {\n switch (target.kind) {\n case \"index\":\n return { section: \"\", slug: \"\" };\n case \"page\":\n return { section: WIKI_ROUTE_SECTION.pages, slug: target.slug };\n case \"log\":\n return { section: WIKI_ROUTE_SECTION.log, slug: \"\" };\n case \"lint_report\":\n return { section: WIKI_ROUTE_SECTION.lintReport, slug: \"\" };\n case \"graph\":\n return { section: WIKI_ROUTE_SECTION.graph, slug: \"\" };\n default: {\n const exhaustive: never = target;\n throw new Error(`unreachable WikiTarget kind: ${JSON.stringify(exhaustive)}`);\n }\n }\n}\n\n// Resolve a target to the action name the server expects. Centralises\n// the one place URL-shape and action-shape diverge (`lint-report` ↔\n// `lint_report`).\nexport function wikiActionFor(target: WikiTarget): WikiAction {\n switch (target.kind) {\n case \"index\":\n return WIKI_ACTION.index;\n case \"page\":\n return WIKI_ACTION.page;\n case \"log\":\n return WIKI_ACTION.log;\n case \"lint_report\":\n return WIKI_ACTION.lintReport;\n case \"graph\":\n return WIKI_ACTION.graph;\n default: {\n const exhaustive: never = target;\n throw new Error(`unreachable WikiTarget kind: ${JSON.stringify(exhaustive)}`);\n }\n }\n}\n","// Pure `[[wiki-link]]` → HTML renderer, shared across hosts via\n// `@mulmoclaude/core/wiki`. Extracted from the MulmoClaude host's\n// `src/plugins/wiki/helpers.ts` so MulmoTerminal can render the same\n// internal-link markup without forking the walker.\n//\n// String-only, no `marked` / DOM / Node deps — the host pipeline\n// (image-ref rewrite, marked.parse, task-interactive) wraps this.\n\nimport { escapeHtml } from \"@mulmoclaude/common\";\nimport { parseWikiLink } from \"./link.js\";\n\n// `escapeHtml` moved down to the zero-dep leaf `@mulmoclaude/common` (#2483) so\n// `@mulmoclaude/markdown-utils` — a leaf THIS package depends on, hence unable\n// to import back up — can share it. Re-exported here because the wiki\n// consumers (collection-plugin's graph view, the host's wiki embeds and\n// spreadsheet view) already import it from `@mulmoclaude/core/wiki`.\nexport { escapeHtml };\n\n/**\n * Replace every `[[page name]]` occurrence in `content` with a\n * `<span class=\"wiki-link\" data-page=\"…\">…</span>` element. The\n * page name may not contain `]`; an opening `[[` that is not\n * followed later by `]]` (with no bare `]` in between) is left\n * untouched so malformed text renders as-is — matching the\n * previous regex's non-match behaviour.\n *\n * `[[target|display]]` is split via the shared `parseWikiLink`\n * helper so `data-page` carries only the target slug while the\n * visible text shows the display half (#1297). Both halves are\n * HTML-escaped before interpolation — `parseWikiLink` runs BEFORE\n * the host's `marked.parse`, so escaping has to happen here.\n */\nexport function renderWikiLinks(content: string): string {\n const out: string[] = [];\n let i = 0;\n while (i < content.length) {\n if (content[i] === \"[\" && content[i + 1] === \"[\") {\n const closeStart = findNextCloseBrackets(content, i + 2);\n if (closeStart !== -1) {\n const inner = content.slice(i + 2, closeStart);\n const { target, display } = parseWikiLink(inner);\n out.push(`<span class=\"wiki-link\" data-page=\"${escapeHtml(target)}\">${escapeHtml(display)}</span>`);\n i = closeStart + 2;\n continue;\n }\n }\n out.push(content[i]);\n i++;\n }\n return out.join(\"\");\n}\n\n/**\n * Starting at `from`, scan forward for a `]]` sequence. Returns\n * the index of the first `]` of that pair, or -1 if a bare `]`\n * (one not immediately followed by a second `]`) is encountered\n * first — mirroring the old regex's `[^\\]]+` constraint that the\n * page name must contain no `]` characters. Also returns -1 if\n * nothing matched before the end of input, or if the pair sits\n * immediately after `from` (zero-length page name, which the old\n * regex rejected via the `+` quantifier).\n */\nfunction findNextCloseBrackets(str: string, from: number): number {\n let j = from;\n while (j < str.length) {\n if (str[j] === \"]\") {\n if (str[j + 1] === \"]\" && j > from) return j;\n // Bare `]` inside the page-name span — old regex would not\n // match here, so we bail and let the caller emit the `[[`\n // as literal text.\n return -1;\n }\n j++;\n }\n return -1;\n}\n"],"mappings":";;;;;AAkBA,IAAa,qBAAqB;CAChC,OAAO;CACP,KAAK;CACL,YAAY;CACZ,OAAO;AACT;AAQA,IAAa,cAAc;CACzB,OAAO;CACP,MAAM;CACN,KAAK;CACL,YAAY;CACZ,OAAO;CACP,MAAM;AACR;AAsBA,SAAgB,eAAe,OAAiC;CAC9D,OAAO,OAAO,UAAU,YAAY,aAAA,WAAW,KAAK;AACtD;AAOA,SAAgB,oBAAoB,QAAoC;CACtE,IAAI,CAAC,UAAU,OAAO,WAAW,UAAU,OAAO;CAClD,MAAM,EAAE,SAAS,SAAS;CAE1B,IAAI,YAAY,KAAA,KAAa,YAAY,IAAI,OAAO,EAAE,MAAM,QAAQ;CAEpE,IAAI,YAAY,mBAAmB,OAAO;EACxC,IAAI,CAAC,eAAe,IAAI,GAAG,OAAO;EAClC,OAAO;GAAE,MAAM;GAAQ;EAAK;CAC9B;CACA,IAAI,YAAY,mBAAmB,KAAK,OAAO,EAAE,MAAM,MAAM;CAC7D,IAAI,YAAY,mBAAmB,YAAY,OAAO,EAAE,MAAM,cAAc;CAC5E,IAAI,YAAY,mBAAmB,OAAO,OAAO,EAAE,MAAM,QAAQ;CAEjE,OAAO;AACT;AAcA,SAAgB,qBAAqB,QAA4C;CAC/E,QAAQ,OAAO,MAAf;EACE,KAAK,SACH,OAAO;GAAE,SAAS;GAAI,MAAM;EAAG;EACjC,KAAK,QACH,OAAO;GAAE,SAAS,mBAAmB;GAAO,MAAM,OAAO;EAAK;EAChE,KAAK,OACH,OAAO;GAAE,SAAS,mBAAmB;GAAK,MAAM;EAAG;EACrD,KAAK,eACH,OAAO;GAAE,SAAS,mBAAmB;GAAY,MAAM;EAAG;EAC5D,KAAK,SACH,OAAO;GAAE,SAAS,mBAAmB;GAAO,MAAM;EAAG;EACvD,SAEE,MAAM,IAAI,MAAM,gCAAgC,KAAK,UAAU,MAAU,GAAG;CAEhF;AACF;AAKA,SAAgB,cAAc,QAAgC;CAC5D,QAAQ,OAAO,MAAf;EACE,KAAK,SACH,OAAO,YAAY;EACrB,KAAK,QACH,OAAO,YAAY;EACrB,KAAK,OACH,OAAO,YAAY;EACrB,KAAK,eACH,OAAO,YAAY;EACrB,KAAK,SACH,OAAO,YAAY;EACrB,SAEE,MAAM,IAAI,MAAM,gCAAgC,KAAK,UAAU,MAAU,GAAG;CAEhF;AACF;;;;;;;;;;;;;;;;;ACzGA,SAAgB,gBAAgB,SAAyB;CACvD,MAAM,MAAgB,CAAC;CACvB,IAAI,IAAI;CACR,OAAO,IAAI,QAAQ,QAAQ;EACzB,IAAI,QAAQ,OAAO,OAAO,QAAQ,IAAI,OAAO,KAAK;GAChD,MAAM,aAAa,sBAAsB,SAAS,IAAI,CAAC;GACvD,IAAI,eAAe,IAAI;IAErB,MAAM,EAAE,QAAQ,YAAY,cAAA,cADd,QAAQ,MAAM,IAAI,GAAG,UACO,CAAK;IAC/C,IAAI,KAAK,sCAAsC,aAAA,WAAW,MAAM,EAAE,IAAI,aAAA,WAAW,OAAO,EAAE,QAAQ;IAClG,IAAI,aAAa;IACjB;GACF;EACF;EACA,IAAI,KAAK,QAAQ,EAAE;EACnB;CACF;CACA,OAAO,IAAI,KAAK,EAAE;AACpB;;;;;;;;;;;AAYA,SAAS,sBAAsB,KAAa,MAAsB;CAChE,IAAI,IAAI;CACR,OAAO,IAAI,IAAI,QAAQ;EACrB,IAAI,IAAI,OAAO,KAAK;GAClB,IAAI,IAAI,IAAI,OAAO,OAAO,IAAI,MAAM,OAAO;GAI3C,OAAO;EACT;EACA;CACF;CACA,OAAO;AACT"}
@@ -1,3 +1,4 @@
1
+ import { n as escapeHtml } from "../dist-Cwk0e12G.js";
1
2
  import { _ as WIKI_LINK_PATTERN, a as findBrokenLinksInPage, c as findTagDrift, d as BULLET_WIKI_LINK_PATTERN, f as buildTableColumnMap, g as parseTagsCell, h as parseIndexEntries, i as resolveLinkTarget, l as formatLintReport, m as extractSlugFromBulletHref, n as incomingLinks, o as findMissingFiles, p as extractHashTags, r as pageOutgoingSlugs, s as findOrphanPages, t as buildWikiGraph, u as BULLET_LINK_PATTERN, v as parseWikiLink } from "../graph-o5FZZbXf.js";
2
3
  import { n as wikiSlugify, t as isSafeSlug } from "../slug-CdN-pQX1.js";
3
4
  //#region src/wiki/route.ts
@@ -71,20 +72,6 @@ function wikiActionFor(target) {
71
72
  }
72
73
  //#endregion
73
74
  //#region src/wiki/render.ts
74
- /** HTML-escape attribute / text content. Self-contained on purpose:
75
- * this package can't reach the host's escaper, and the rule is a
76
- * fixed five-char map. */
77
- function escapeHtml(value) {
78
- return value.replace(/[&<>"']/g, (char) => {
79
- switch (char) {
80
- case "&": return "&amp;";
81
- case "<": return "&lt;";
82
- case ">": return "&gt;";
83
- case "\"": return "&quot;";
84
- default: return "&#39;";
85
- }
86
- });
87
- }
88
75
  /**
89
76
  * Replace every `[[page name]]` occurrence in `content` with a
90
77
  * `<span class="wiki-link" data-page="…">…</span>` element. The