@mulmoclaude/core 1.0.1 → 1.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/assets/helps/collection-skills.md +28 -0
- package/dist/artifacts/paths.cjs +68 -0
- package/dist/artifacts/paths.cjs.map +1 -0
- package/dist/artifacts/paths.d.ts +45 -0
- package/dist/artifacts/paths.js +62 -0
- package/dist/artifacts/paths.js.map +1 -0
- package/dist/atomic-C_7YpMiM.cjs +102 -0
- package/dist/atomic-C_7YpMiM.cjs.map +1 -0
- package/dist/atomic-DPpdrJzO.js +89 -0
- package/dist/atomic-DPpdrJzO.js.map +1 -0
- package/dist/collection/core/chatSeed.d.ts +4 -0
- package/dist/collection/core/completion.d.ts +53 -0
- package/dist/collection/core/ids.d.ts +10 -0
- package/dist/collection/core/linkTargets.d.ts +14 -0
- package/dist/collection/core/ownProp.d.ts +9 -0
- package/dist/collection/core/recordKeys.d.ts +21 -0
- package/dist/collection/core/schemaRules.d.ts +167 -0
- package/dist/collection/core/schemaZ.d.ts +489 -9
- package/dist/collection/core/sortValueOf.d.ts +21 -0
- package/dist/collection/core/textSearch.d.ts +12 -0
- package/dist/collection/index.cjs +132 -29
- package/dist/collection/index.cjs.map +1 -1
- package/dist/collection/index.d.ts +5 -0
- package/dist/collection/index.js +115 -29
- package/dist/collection/index.js.map +1 -1
- package/dist/collection/registry/server/index.cjs +14 -42
- package/dist/collection/registry/server/index.cjs.map +1 -1
- package/dist/collection/registry/server/index.js +6 -34
- package/dist/collection/registry/server/index.js.map +1 -1
- package/dist/collection/server/discovery.d.ts +7 -0
- package/dist/collection/server/host.d.ts +3 -8
- package/dist/collection/server/index.cjs +10 -9
- package/dist/collection/server/index.d.ts +1 -0
- package/dist/collection/server/index.js +4 -4
- package/dist/collection/server/io.d.ts +0 -88
- package/dist/collection/server/skillAssets.d.ts +90 -0
- package/dist/collection-watchers/config.d.ts +2 -4
- package/dist/collection-watchers/index.cjs +5 -5
- package/dist/collection-watchers/index.cjs.map +1 -1
- package/dist/collection-watchers/index.js +5 -5
- package/dist/collection-watchers/index.js.map +1 -1
- package/dist/{discovery-Dpz6SWwe.cjs → discovery--kRUWed0.cjs} +444 -434
- package/dist/discovery--kRUWed0.cjs.map +1 -0
- package/dist/{discovery-BYIsLqag.js → discovery-CKR4TOX3.js} +426 -392
- package/dist/discovery-CKR4TOX3.js.map +1 -0
- package/dist/dist-Cwk0e12G.js +50 -0
- package/dist/dist-Cwk0e12G.js.map +1 -0
- package/dist/dist-pWpC-b04.cjs +61 -0
- package/dist/dist-pWpC-b04.cjs.map +1 -0
- package/dist/feeds/index.cjs +2 -2
- package/dist/feeds/index.js +2 -2
- package/dist/feeds/server/host.d.ts +5 -8
- package/dist/feeds/server/index.cjs +143 -61
- package/dist/feeds/server/index.cjs.map +1 -1
- package/dist/feeds/server/index.js +141 -59
- package/dist/feeds/server/index.js.map +1 -1
- package/dist/files/atomic.d.ts +34 -0
- package/dist/files/index.cjs +47 -0
- package/dist/files/index.cjs.map +1 -0
- package/dist/files/index.d.ts +3 -0
- package/dist/files/index.js +40 -0
- package/dist/files/index.js.map +1 -0
- package/dist/files/json.d.ts +4 -0
- package/dist/files/safe.d.ts +9 -0
- package/dist/google/auth.d.ts +1 -1
- package/dist/google/collectionDateTime.d.ts +5 -0
- package/dist/google/collectionSync.d.ts +2 -2
- package/dist/google/fsJson.d.ts +3 -2
- package/dist/google/host.d.ts +3 -6
- package/dist/google/index.cjs +64 -91
- package/dist/google/index.cjs.map +1 -1
- package/dist/google/index.d.ts +1 -0
- package/dist/google/index.js +57 -85
- package/dist/google/index.js.map +1 -1
- package/dist/{graph-o5FZZbXf.js → graph-BcwqiSpR.js} +2 -14
- package/dist/graph-BcwqiSpR.js.map +1 -0
- package/dist/{graph-2R4HbQAg.cjs → graph-DjE1Az1C.cjs} +2 -14
- package/dist/graph-DjE1Az1C.cjs.map +1 -0
- package/dist/host/hostSlot.d.ts +27 -0
- package/dist/{ids-DWmHjm17.cjs → ids-BnTh3Hl0.cjs} +29 -1
- package/dist/{ids-DWmHjm17.cjs.map → ids-BnTh3Hl0.cjs.map} +1 -1
- package/dist/{ids-D1M1T6KJ.js → ids-s4GfmE93.js} +18 -2
- package/dist/{ids-D1M1T6KJ.js.map → ids-s4GfmE93.js.map} +1 -1
- package/dist/{ingestTypes-Ci4eOgv-.js → ingestTypes-L59cIGgX.js} +2 -2
- package/dist/{ingestTypes-Ci4eOgv-.js.map → ingestTypes-L59cIGgX.js.map} +1 -1
- package/dist/{ingestTypes-DbuQNuK6.cjs → ingestTypes-xX8VpQqh.cjs} +2 -2
- package/dist/{ingestTypes-DbuQNuK6.cjs.map → ingestTypes-xX8VpQqh.cjs.map} +1 -1
- package/dist/notifier/engine.d.ts +2 -4
- package/dist/notifier/index.cjs +1 -1
- package/dist/notifier/index.js +1 -1
- package/dist/{notifier-ChpY0XrY.js → notifier-BdA5qzhe.js} +14 -16
- package/dist/notifier-BdA5qzhe.js.map +1 -0
- package/dist/{notifier-bS8IEeLA.cjs → notifier-tMsAXyXp.cjs} +14 -16
- package/dist/notifier-tMsAXyXp.cjs.map +1 -0
- package/dist/plugin-vue/fileWatch.d.ts +5 -0
- package/dist/plugin-vue/i18n.cjs +45 -0
- package/dist/plugin-vue/i18n.cjs.map +1 -0
- package/dist/plugin-vue/i18n.js +44 -0
- package/dist/plugin-vue/i18n.js.map +1 -0
- package/dist/plugin-vue/index.cjs +98 -0
- package/dist/plugin-vue/index.cjs.map +1 -0
- package/dist/plugin-vue/index.d.ts +5 -0
- package/dist/plugin-vue/index.js +91 -0
- package/dist/plugin-vue/index.js.map +1 -0
- package/dist/plugin-vue/markdownDoc.d.ts +10 -0
- package/dist/plugin-vue/pluginI18n.d.ts +22 -0
- package/dist/plugin-vue/useClipboardCopy.d.ts +6 -0
- package/dist/plugin-vue/useFileWatch.d.ts +8 -0
- package/dist/plugin-vue/useMarkdownDoc.d.ts +3 -0
- package/dist/{promptSafety-Bugq2kqL.js → promptSafety-CQ5Un4wi.js} +104 -7
- package/dist/promptSafety-CQ5Un4wi.js.map +1 -0
- package/dist/{promptSafety-DbE6eZmP.cjs → promptSafety-DfZYNtjK.cjs} +151 -6
- package/dist/promptSafety-DfZYNtjK.cjs.map +1 -0
- package/dist/remote-host/server/hostRunner.d.ts +5 -1
- package/dist/remote-host/server/index.cjs +75 -21
- package/dist/remote-host/server/index.cjs.map +1 -1
- package/dist/remote-host/server/index.js +72 -18
- package/dist/remote-host/server/index.js.map +1 -1
- package/dist/scheduler/index.cjs +3 -3
- package/dist/scheduler/index.cjs.map +1 -1
- package/dist/scheduler/index.js +2 -2
- package/dist/scheduler/index.js.map +1 -1
- package/dist/scheduler/task-manager.d.ts +2 -5
- package/dist/{server-CtCBLYgZ.js → server-BZJCw2HU.js} +209 -41
- package/dist/server-BZJCw2HU.js.map +1 -0
- package/dist/{server-BjWrcc5T.cjs → server-w5ZZRLju.cjs} +250 -46
- package/dist/server-w5ZZRLju.cjs.map +1 -0
- package/dist/skill-bridge/index.cjs +1 -1
- package/dist/skill-bridge/index.js +1 -1
- package/dist/translation/client.cjs +16 -0
- package/dist/translation/client.cjs.map +1 -1
- package/dist/translation/client.d.ts +6 -0
- package/dist/translation/client.js +16 -1
- package/dist/translation/client.js.map +1 -1
- package/dist/utils/errors.d.ts +2 -1
- package/dist/utils/fetch.cjs +36 -0
- package/dist/utils/fetch.cjs.map +1 -0
- package/dist/{collection/registry/server → utils}/fetch.d.ts +5 -3
- package/dist/utils/fetch.js +34 -0
- package/dist/utils/fetch.js.map +1 -0
- package/dist/utils/index.cjs +11 -3
- package/dist/utils/index.cjs.map +1 -0
- package/dist/utils/index.js +9 -1
- package/dist/utils/index.js.map +1 -0
- package/dist/whisper/index.cjs +9 -9
- package/dist/whisper/index.cjs.map +1 -1
- package/dist/whisper/index.js +3 -3
- package/dist/wiki/index.cjs +23 -28
- package/dist/wiki/index.cjs.map +1 -1
- package/dist/wiki/index.js +21 -26
- package/dist/wiki/index.js.map +1 -1
- package/dist/wiki/link.d.ts +7 -1
- package/dist/wiki/render.d.ts +5 -7
- package/dist/wiki/server/frontmatter.d.ts +2 -6
- package/dist/wiki/server/index.cjs +5 -41
- package/dist/wiki/server/index.cjs.map +1 -1
- package/dist/wiki/server/index.js +3 -39
- package/dist/wiki/server/index.js.map +1 -1
- package/dist/workspace-setup/index.js +22 -6
- package/dist/workspace-setup/index.js.map +1 -1
- package/package.json +54 -11
- package/dist/collection/server/atomic.d.ts +0 -1
- package/dist/discovery-BYIsLqag.js.map +0 -1
- package/dist/discovery-Dpz6SWwe.cjs.map +0 -1
- package/dist/errors-7P5eMOSX.cjs +0 -30
- package/dist/errors-7P5eMOSX.cjs.map +0 -1
- package/dist/errors-eid6Mes3.js +0 -19
- package/dist/errors-eid6Mes3.js.map +0 -1
- package/dist/google/fetch.d.ts +0 -8
- package/dist/graph-2R4HbQAg.cjs.map +0 -1
- package/dist/graph-o5FZZbXf.js.map +0 -1
- package/dist/notifier-ChpY0XrY.js.map +0 -1
- package/dist/notifier-bS8IEeLA.cjs.map +0 -1
- package/dist/promptSafety-Bugq2kqL.js.map +0 -1
- package/dist/promptSafety-DbE6eZmP.cjs.map +0 -1
- package/dist/server-BjWrcc5T.cjs.map +0 -1
- package/dist/server-CtCBLYgZ.js.map +0 -1
package/dist/wiki/index.cjs.map
CHANGED
|
@@ -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 \"&\";\n case \"<\":\n return \"<\";\n case \">\":\n return \">\";\n case '\"':\n return \""\";\n default:\n return \"'\";\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, WIKI_LINK_MAX_LEN } 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 `[` or `]`; an opening `[[` that is\n * not followed later by `]]` (with no bare `[` or `]` in between)\n * is left 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 the span isn't\n * a valid wiki-link body — mirroring `WIKI_LINK_PATTERN` exactly so\n * the renderer can't accept a link the graph / backlinks / lint\n * would reject. The pattern's body class is `[^\\][\\r\\n]`, so it\n * rejects `[` as well as `]`. Bails (-1) on:\n * - a bare `]` (regex `[^\\]]`),\n * - a `[` inside the body (regex `[^\\[]` — a nested `[[` opener\n * never belongs in a page name, and the pattern would not match),\n * - a `\\r` or `\\n` (regex `[^\\r\\n]` — a newline-bearing link must\n * not render clickable, and its slug must not reach the URL),\n * - a body longer than WIKI_LINK_MAX_LEN (regex `{1,200}`),\n * - a zero-length body (regex `{1,}` minimum),\n * - reaching end-of-input with no `]]`.\n */\nfunction findNextCloseBrackets(str: string, from: number): number {\n let j = from;\n while (j < str.length) {\n const char = str[j];\n if (char === \"]\") {\n if (str[j + 1] === \"]\" && j > from) return j;\n // Bare `]` inside the page-name span — regex would not match.\n return -1;\n }\n // A `[`, a newline, or a span past the cap: none match the pattern's\n // `[^\\][\\r\\n]{1,200}` body, so bail and let the caller emit `[[` literally.\n if (char === \"[\" || char === \"\\n\" || char === \"\\r\" || j - from >= WIKI_LINK_MAX_LEN) return -1;\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;;;;;;;;;;;;;;;;;AAkBA,SAAS,sBAAsB,KAAa,MAAsB;CAChE,IAAI,IAAI;CACR,OAAO,IAAI,IAAI,QAAQ;EACrB,MAAM,OAAO,IAAI;EACjB,IAAI,SAAS,KAAK;GAChB,IAAI,IAAI,IAAI,OAAO,OAAO,IAAI,MAAM,OAAO;GAE3C,OAAO;EACT;EAGA,IAAI,SAAS,OAAO,SAAS,QAAQ,SAAS,QAAQ,IAAI,QAAA,KAA2B,OAAO;EAC5F;CACF;CACA,OAAO;AACT"}
|
package/dist/wiki/index.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { n as escapeHtml } from "../dist-Cwk0e12G.js";
|
|
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-BcwqiSpR.js";
|
|
2
3
|
import { n as wikiSlugify, t as isSafeSlug } from "../slug-CdN-pQX1.js";
|
|
3
4
|
//#region src/wiki/route.ts
|
|
4
5
|
var WIKI_ROUTE_SECTION = {
|
|
@@ -71,26 +72,12 @@ 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 "&";
|
|
81
|
-
case "<": return "<";
|
|
82
|
-
case ">": return ">";
|
|
83
|
-
case "\"": return """;
|
|
84
|
-
default: return "'";
|
|
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
|
|
91
|
-
* page name may not contain `]`; an opening `[[` that is
|
|
92
|
-
* followed later by `]]` (with no bare `]` in between)
|
|
93
|
-
* untouched so malformed text renders as-is — matching the
|
|
78
|
+
* page name may not contain `[` or `]`; an opening `[[` that is
|
|
79
|
+
* not followed later by `]]` (with no bare `[` or `]` in between)
|
|
80
|
+
* is left untouched so malformed text renders as-is — matching the
|
|
94
81
|
* previous regex's non-match behaviour.
|
|
95
82
|
*
|
|
96
83
|
* `[[target|display]]` is split via the shared `parseWikiLink`
|
|
@@ -119,21 +106,29 @@ function renderWikiLinks(content) {
|
|
|
119
106
|
}
|
|
120
107
|
/**
|
|
121
108
|
* Starting at `from`, scan forward for a `]]` sequence. Returns
|
|
122
|
-
* the index of the first `]` of that pair, or -1 if
|
|
123
|
-
*
|
|
124
|
-
*
|
|
125
|
-
*
|
|
126
|
-
*
|
|
127
|
-
*
|
|
128
|
-
*
|
|
109
|
+
* the index of the first `]` of that pair, or -1 if the span isn't
|
|
110
|
+
* a valid wiki-link body — mirroring `WIKI_LINK_PATTERN` exactly so
|
|
111
|
+
* the renderer can't accept a link the graph / backlinks / lint
|
|
112
|
+
* would reject. The pattern's body class is `[^\][\r\n]`, so it
|
|
113
|
+
* rejects `[` as well as `]`. Bails (-1) on:
|
|
114
|
+
* - a bare `]` (regex `[^\]]`),
|
|
115
|
+
* - a `[` inside the body (regex `[^\[]` — a nested `[[` opener
|
|
116
|
+
* never belongs in a page name, and the pattern would not match),
|
|
117
|
+
* - a `\r` or `\n` (regex `[^\r\n]` — a newline-bearing link must
|
|
118
|
+
* not render clickable, and its slug must not reach the URL),
|
|
119
|
+
* - a body longer than WIKI_LINK_MAX_LEN (regex `{1,200}`),
|
|
120
|
+
* - a zero-length body (regex `{1,}` minimum),
|
|
121
|
+
* - reaching end-of-input with no `]]`.
|
|
129
122
|
*/
|
|
130
123
|
function findNextCloseBrackets(str, from) {
|
|
131
124
|
let j = from;
|
|
132
125
|
while (j < str.length) {
|
|
133
|
-
|
|
126
|
+
const char = str[j];
|
|
127
|
+
if (char === "]") {
|
|
134
128
|
if (str[j + 1] === "]" && j > from) return j;
|
|
135
129
|
return -1;
|
|
136
130
|
}
|
|
131
|
+
if (char === "[" || char === "\n" || char === "\r" || j - from >= 200) return -1;
|
|
137
132
|
j++;
|
|
138
133
|
}
|
|
139
134
|
return -1;
|
package/dist/wiki/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","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 \"&\";\n case \"<\":\n return \"<\";\n case \">\":\n return \">\";\n case '\"':\n return \""\";\n default:\n return \"'\";\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":"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"}
|
|
1
|
+
{"version":3,"file":"index.js","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, WIKI_LINK_MAX_LEN } 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 `[` or `]`; an opening `[[` that is\n * not followed later by `]]` (with no bare `[` or `]` in between)\n * is left 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 the span isn't\n * a valid wiki-link body — mirroring `WIKI_LINK_PATTERN` exactly so\n * the renderer can't accept a link the graph / backlinks / lint\n * would reject. The pattern's body class is `[^\\][\\r\\n]`, so it\n * rejects `[` as well as `]`. Bails (-1) on:\n * - a bare `]` (regex `[^\\]]`),\n * - a `[` inside the body (regex `[^\\[]` — a nested `[[` opener\n * never belongs in a page name, and the pattern would not match),\n * - a `\\r` or `\\n` (regex `[^\\r\\n]` — a newline-bearing link must\n * not render clickable, and its slug must not reach the URL),\n * - a body longer than WIKI_LINK_MAX_LEN (regex `{1,200}`),\n * - a zero-length body (regex `{1,}` minimum),\n * - reaching end-of-input with no `]]`.\n */\nfunction findNextCloseBrackets(str: string, from: number): number {\n let j = from;\n while (j < str.length) {\n const char = str[j];\n if (char === \"]\") {\n if (str[j + 1] === \"]\" && j > from) return j;\n // Bare `]` inside the page-name span — regex would not match.\n return -1;\n }\n // A `[`, a newline, or a span past the cap: none match the pattern's\n // `[^\\][\\r\\n]{1,200}` body, so bail and let the caller emit `[[` literally.\n if (char === \"[\" || char === \"\\n\" || char === \"\\r\" || j - from >= WIKI_LINK_MAX_LEN) return -1;\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,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,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;;;;;;;;;;;;;;;;;AAkBA,SAAS,sBAAsB,KAAa,MAAsB;CAChE,IAAI,IAAI;CACR,OAAO,IAAI,IAAI,QAAQ;EACrB,MAAM,OAAO,IAAI;EACjB,IAAI,SAAS,KAAK;GAChB,IAAI,IAAI,IAAI,OAAO,OAAO,IAAI,MAAM,OAAO;GAE3C,OAAO;EACT;EAGA,IAAI,SAAS,OAAO,SAAS,QAAQ,SAAS,QAAQ,IAAI,QAAA,KAA2B,OAAO;EAC5F;CACF;CACA,OAAO;AACT"}
|
package/dist/wiki/link.d.ts
CHANGED
|
@@ -8,7 +8,13 @@
|
|
|
8
8
|
* unmatched `[[` could still pin the CPU. 200 is well above the
|
|
9
9
|
* longest legitimate title we've seen and matches the cap the
|
|
10
10
|
* server-side `WIKI_LINK_PATTERN` constant has carried since the
|
|
11
|
-
* original implementation in #951.
|
|
11
|
+
* original implementation in #951.
|
|
12
|
+
*
|
|
13
|
+
* Shared with the renderer's hand-rolled scanner so the two can't
|
|
14
|
+
* drift: the renderer must reject the same newline-bearing and
|
|
15
|
+
* over-length bodies, or a link would render clickable while the
|
|
16
|
+
* graph / backlinks / lint (which use this pattern) never see it. */
|
|
17
|
+
export declare const WIKI_LINK_MAX_LEN = 200;
|
|
12
18
|
export declare const WIKI_LINK_PATTERN: RegExp;
|
|
13
19
|
/** The structural shape of an inline `[[target|display]]` link.
|
|
14
20
|
* `target` is what the resolver / lint compares against page
|
package/dist/wiki/render.d.ts
CHANGED
|
@@ -1,13 +1,11 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
* fixed five-char map. */
|
|
4
|
-
export declare function escapeHtml(value: string): string;
|
|
1
|
+
import { escapeHtml } from '@mulmoclaude/common';
|
|
2
|
+
export { escapeHtml };
|
|
5
3
|
/**
|
|
6
4
|
* Replace every `[[page name]]` occurrence in `content` with a
|
|
7
5
|
* `<span class="wiki-link" data-page="…">…</span>` element. The
|
|
8
|
-
* page name may not contain `]`; an opening `[[` that is
|
|
9
|
-
* followed later by `]]` (with no bare `]` in between)
|
|
10
|
-
* untouched so malformed text renders as-is — matching the
|
|
6
|
+
* page name may not contain `[` or `]`; an opening `[[` that is
|
|
7
|
+
* not followed later by `]]` (with no bare `[` or `]` in between)
|
|
8
|
+
* is left untouched so malformed text renders as-is — matching the
|
|
11
9
|
* previous regex's non-match behaviour.
|
|
12
10
|
*
|
|
13
11
|
* `[[target|display]]` is split via the shared `parseWikiLink`
|
|
@@ -1,9 +1,5 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
export declare function parseFrontmatter(raw: string): {
|
|
4
|
-
meta: Record<string, unknown>;
|
|
5
|
-
hasHeader: boolean;
|
|
6
|
-
};
|
|
1
|
+
import { parseFrontmatter } from '@mulmoclaude/markdown-utils/markdown/frontmatter';
|
|
2
|
+
export { parseFrontmatter };
|
|
7
3
|
/** Narrow `tags:` reader. Handles flow (`tags: [a, b]`) and block-list
|
|
8
4
|
* style; anything unparseable returns `[]` (best-effort, never throws). */
|
|
9
5
|
export declare function parseFrontmatterTags(content: string): string[];
|
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
2
2
|
const require_rolldown_runtime = require("../../rolldown-runtime-D6vf50IK.cjs");
|
|
3
|
-
const require_graph = require("../../graph-
|
|
3
|
+
const require_graph = require("../../graph-DjE1Az1C.cjs");
|
|
4
4
|
const require_slug = require("../../slug-DbwORN9z.cjs");
|
|
5
5
|
const require_wiki_paths = require("../paths.cjs");
|
|
6
|
+
let node_fs = require("node:fs");
|
|
6
7
|
let node_path = require("node:path");
|
|
7
8
|
node_path = require_rolldown_runtime.__toESM(node_path, 1);
|
|
8
|
-
let node_fs = require("node:fs");
|
|
9
9
|
let node_fs_promises = require("node:fs/promises");
|
|
10
|
-
let
|
|
10
|
+
let _mulmoclaude_markdown_utils_markdown_frontmatter = require("@mulmoclaude/markdown-utils/markdown/frontmatter");
|
|
11
11
|
//#region src/wiki/server/fs.ts
|
|
12
12
|
async function readTextSafe(absPath) {
|
|
13
13
|
try {
|
|
@@ -74,49 +74,13 @@ function __resetPageIndexCache() {
|
|
|
74
74
|
}
|
|
75
75
|
//#endregion
|
|
76
76
|
//#region src/wiki/server/frontmatter.ts
|
|
77
|
-
var FRONTMATTER_OPEN = /^---\r?\n/;
|
|
78
|
-
var FRONTMATTER_CLOSE = /(?:^|\r?\n)---\s*(?:\r?\n|$)/;
|
|
79
|
-
/** Parse `---\n…\n---\n` frontmatter. Never throws; malformed YAML in a
|
|
80
|
-
* well-formed envelope yields `{ meta: {}, hasHeader: false }`. */
|
|
81
|
-
function parseFrontmatter(raw) {
|
|
82
|
-
if (!FRONTMATTER_OPEN.test(raw)) return {
|
|
83
|
-
meta: {},
|
|
84
|
-
hasHeader: false
|
|
85
|
-
};
|
|
86
|
-
const afterOpen = raw.replace(FRONTMATTER_OPEN, "");
|
|
87
|
-
const closeMatch = FRONTMATTER_CLOSE.exec(afterOpen);
|
|
88
|
-
if (!closeMatch || closeMatch.index === void 0) return {
|
|
89
|
-
meta: {},
|
|
90
|
-
hasHeader: false
|
|
91
|
-
};
|
|
92
|
-
const meta = safeYamlLoad(afterOpen.slice(0, closeMatch.index));
|
|
93
|
-
if (meta === null) return {
|
|
94
|
-
meta: {},
|
|
95
|
-
hasHeader: false
|
|
96
|
-
};
|
|
97
|
-
return {
|
|
98
|
-
meta,
|
|
99
|
-
hasHeader: true
|
|
100
|
-
};
|
|
101
|
-
}
|
|
102
|
-
function safeYamlLoad(text) {
|
|
103
|
-
if (text.trim() === "") return {};
|
|
104
|
-
try {
|
|
105
|
-
const loaded = (0, js_yaml.load)(text, { schema: js_yaml.FAILSAFE_SCHEMA });
|
|
106
|
-
if (loaded === null || loaded === void 0) return {};
|
|
107
|
-
if (typeof loaded !== "object" || Array.isArray(loaded)) return null;
|
|
108
|
-
return loaded;
|
|
109
|
-
} catch {
|
|
110
|
-
return null;
|
|
111
|
-
}
|
|
112
|
-
}
|
|
113
77
|
function cleanTagToken(token) {
|
|
114
78
|
return token.trim().replace(/^['"]|['"]$/g, "").replace(/^#/, "").toLowerCase();
|
|
115
79
|
}
|
|
116
80
|
/** Narrow `tags:` reader. Handles flow (`tags: [a, b]`) and block-list
|
|
117
81
|
* style; anything unparseable returns `[]` (best-effort, never throws). */
|
|
118
82
|
function parseFrontmatterTags(content) {
|
|
119
|
-
const parsed = parseFrontmatter(content);
|
|
83
|
+
const parsed = (0, _mulmoclaude_markdown_utils_markdown_frontmatter.parseFrontmatter)(content);
|
|
120
84
|
if (!parsed.hasHeader) return [];
|
|
121
85
|
const tagsValue = parsed.meta.tags;
|
|
122
86
|
if (!Array.isArray(tagsValue)) return [];
|
|
@@ -237,7 +201,7 @@ exports.__resetPageIndexCache = __resetPageIndexCache;
|
|
|
237
201
|
exports.collectLintIssues = collectLintIssues;
|
|
238
202
|
exports.getPageIndex = getPageIndex;
|
|
239
203
|
exports.loadWikiGraph = loadWikiGraph;
|
|
240
|
-
exports.parseFrontmatter = parseFrontmatter;
|
|
204
|
+
exports.parseFrontmatter = _mulmoclaude_markdown_utils_markdown_frontmatter.parseFrontmatter;
|
|
241
205
|
exports.parseFrontmatterTags = parseFrontmatterTags;
|
|
242
206
|
exports.pickFuzzyMatch = pickFuzzyMatch;
|
|
243
207
|
exports.readWikiIndex = readWikiIndex;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.cjs","names":[],"sources":["../../../src/wiki/server/fs.ts","../../../src/wiki/server/pageIndex.ts","../../../src/wiki/server/frontmatter.ts","../../../src/wiki/server/engine.ts"],"sourcesContent":["// Minimal ENOENT-swallowing fs wrappers for the wiki read-engine.\n// Self-contained (core can't reach the host's `server/utils/files/safe.ts`);\n// only the handful of helpers the wiki engine needs. Same \"return\n// null / [] on any error\" contract so callers branch on the value\n// instead of try/catch.\n\nimport type { Dirent, Stats } from \"node:fs\";\nimport { readFileSync } from \"node:fs\";\nimport { readFile, readdir, stat } from \"node:fs/promises\";\n\nexport async function readTextSafe(absPath: string): Promise<string | null> {\n try {\n return await readFile(absPath, \"utf-8\");\n } catch {\n return null;\n }\n}\n\nexport function readTextSafeSync(absPath: string): string | null {\n try {\n return readFileSync(absPath, \"utf-8\");\n } catch {\n return null;\n }\n}\n\nexport async function statSafeAsync(absPath: string): Promise<Stats | null> {\n try {\n return await stat(absPath);\n } catch {\n return null;\n }\n}\n\nexport async function readDirSafeAsync(absPath: string): Promise<Dirent[]> {\n try {\n return await readdir(absPath, { withFileTypes: true });\n } catch {\n return [];\n }\n}\n","// In-memory index of `<pagesDir>/*.md` keyed by slug (= filename\n// without the `.md` extension). Kept fresh via `pagesDir` mtime —\n// adding, removing, or renaming a file under `pagesDir` advances the\n// directory's mtime on every major filesystem we target (macOS APFS,\n// Linux ext4, Windows NTFS), so one cheap `stat()` per request is\n// enough to decide whether to rebuild.\n\nimport { readDirSafeAsync, statSafeAsync } from \"./fs.js\";\n\nexport interface PageIndex {\n mtimeMs: number;\n /** slug → filename (e.g. \"sakura-internet\" → \"sakura-internet.md\"). */\n slugs: Map<string, string>;\n}\n\n// Keyed by `pagesDir`: the engine is workspace-injected and may be\n// called with more than one pages directory in a single process. A\n// single global slot would let one workspace's slug map be returned\n// for another whenever their mtimes happen to tie (Codex P2 on #1876).\nconst cache = new Map<string, PageIndex>();\n\n/**\n * Get the page index for `pagesDir`. Returns a cached value as long as\n * THAT directory's mtime hasn't advanced; otherwise rebuilds. Safe to\n * call concurrently — racing builds produce the same result.\n */\nexport async function getPageIndex(pagesDir: string): Promise<PageIndex> {\n const stat = await statSafeAsync(pagesDir);\n if (!stat) {\n // Dir doesn't exist yet (never ingested). Return empty but don't\n // cache a stale-forever value — the next call re-stats.\n return { mtimeMs: 0, slugs: new Map() };\n }\n const cached = cache.get(pagesDir);\n if (cached && cached.mtimeMs >= stat.mtimeMs) return cached;\n const entries = await readDirSafeAsync(pagesDir);\n const slugs = new Map<string, string>();\n for (const entry of entries) {\n if (!entry.isFile()) continue;\n const { name } = entry;\n if (!name.endsWith(\".md\")) continue;\n slugs.set(name.slice(0, -\".md\".length), name);\n }\n const built: PageIndex = { mtimeMs: stat.mtimeMs, slugs };\n cache.set(pagesDir, built);\n return built;\n}\n\n/** Test-only: drop the module-level cache (all directories). */\nexport function __resetPageIndexCache(): void {\n cache.clear();\n}\n","// Read-side YAML frontmatter parsing for the wiki engine. Mirrors the\n// host's `server/utils/markdown/frontmatter.ts` parser (value-preserving\n// FAILSAFE_SCHEMA) so MulmoClaude and MulmoTerminal read `tags:` (and\n// any other field) identically. Only the READ path lives here — the\n// serialize/merge write side stays host-side until a host needs it.\n\nimport { FAILSAFE_SCHEMA, load as yamlLoad } from \"js-yaml\";\n\nconst FRONTMATTER_OPEN = /^---\\r?\\n/;\n// `(?:^|\\r?\\n)` lets the closing fence sit at the very start of\n// `afterOpen` — needed for the empty-envelope case `---\\n---\\n`.\nconst FRONTMATTER_CLOSE = /(?:^|\\r?\\n)---\\s*(?:\\r?\\n|$)/;\n\n/** Parse `---\\n…\\n---\\n` frontmatter. Never throws; malformed YAML in a\n * well-formed envelope yields `{ meta: {}, hasHeader: false }`. */\nexport function parseFrontmatter(raw: string): { meta: Record<string, unknown>; hasHeader: boolean } {\n if (!FRONTMATTER_OPEN.test(raw)) return { meta: {}, hasHeader: false };\n const afterOpen = raw.replace(FRONTMATTER_OPEN, \"\");\n const closeMatch = FRONTMATTER_CLOSE.exec(afterOpen);\n if (!closeMatch || closeMatch.index === undefined) return { meta: {}, hasHeader: false };\n const meta = safeYamlLoad(afterOpen.slice(0, closeMatch.index));\n if (meta === null) return { meta: {}, hasHeader: false };\n return { meta, hasHeader: true };\n}\n\nfunction safeYamlLoad(text: string): Record<string, unknown> | null {\n // js-yaml throws on empty/whitespace-only input; an empty header\n // (`---\\n---\\n`) means \"no metadata\", not \"malformed\".\n if (text.trim() === \"\") return {};\n try {\n // FAILSAFE_SCHEMA keeps every scalar a string (no YAML-1.1 date\n // coercion, no numeric-string truncation) — matches the host.\n const loaded = yamlLoad(text, { schema: FAILSAFE_SCHEMA });\n if (loaded === null || loaded === undefined) return {};\n if (typeof loaded !== \"object\" || Array.isArray(loaded)) return null;\n return loaded as Record<string, unknown>;\n } catch {\n return null;\n }\n}\n\nfunction cleanTagToken(token: string): string {\n return token\n .trim()\n .replace(/^['\"]|['\"]$/g, \"\")\n .replace(/^#/, \"\")\n .toLowerCase();\n}\n\n/** Narrow `tags:` reader. Handles flow (`tags: [a, b]`) and block-list\n * style; anything unparseable returns `[]` (best-effort, never throws). */\nexport function parseFrontmatterTags(content: string): string[] {\n const parsed = parseFrontmatter(content);\n if (!parsed.hasHeader) return [];\n const tagsValue = parsed.meta.tags;\n if (!Array.isArray(tagsValue)) return [];\n return tagsValue\n .filter((item): item is string => typeof item === \"string\")\n .map(cleanTagToken)\n .filter((token) => token.length > 0);\n}\n","// Workspace-injected wiki read-engine — the filesystem layer that feeds\n// the pure helpers in `@mulmoclaude/core/wiki`. Shared by every host so\n// the two apps reading the same `data/wiki/` can't disagree on slug\n// resolution, graph edges, or lint findings. Mirrors the bodies that\n// previously lived in MulmoClaude's `server/api/routes/wiki.ts`; the\n// hosts keep only their HTTP response shaping on top.\n//\n// The read/write WRITE side (writeWikiPage / snapshots) stays host-side\n// until a host needs it shared.\n\nimport path from \"node:path\";\nimport { parseWikiLink } from \"../link.js\";\nimport { wikiSlugify } from \"../slug.js\";\nimport { type WikiPageEntry, parseIndexEntries } from \"../index-parse.js\";\nimport { findBrokenLinksInPage, findMissingFiles, findOrphanPages, findTagDrift } from \"../lint.js\";\nimport { type WikiGraph, buildWikiGraph } from \"../graph.js\";\nimport { readTextSafe, readTextSafeSync } from \"./fs.js\";\nimport { getPageIndex } from \"./pageIndex.js\";\nimport { parseFrontmatterTags } from \"./frontmatter.js\";\nimport { wikiDirs } from \"./paths.js\";\n\n// Below this length the fuzzy `includes` step is skipped — CJK /\n// emoji-only / very-short page names slugify down to a short noise\n// tail that partial-matches almost anything; the index.md title-match\n// fallback still handles the legitimate non-ASCII case (#1194).\nconst MIN_FUZZY_SLUG_LEN = 6;\n\nfunction readFileOrEmpty(absPath: string): string {\n return readTextSafeSync(absPath) ?? \"\";\n}\n\n/** Walk every indexed slug for an `includes`-style match. Returns the\n * single best candidate, or null when the slug is too short OR several\n * candidates tie at the top score (ambiguous → defer to the caller's\n * title-match fallback). Score = min/max length, decoupled from Map\n * iteration order so resolution is deterministic across hosts. */\nexport function pickFuzzyMatch(slug: string, slugs: ReadonlyMap<string, string>): string | null {\n if (slug.length < MIN_FUZZY_SLUG_LEN) return null;\n let bestFile: string | null = null;\n let bestScore = 0;\n let bestIsTied = false;\n for (const [key, file] of slugs) {\n if (!slug.includes(key) && !key.includes(slug)) continue;\n const shorter = Math.min(slug.length, key.length);\n const longer = Math.max(slug.length, key.length);\n const score = shorter / longer;\n if (score > bestScore) {\n bestScore = score;\n bestFile = file;\n bestIsTied = false;\n } else if (score === bestScore) {\n bestIsTied = true;\n }\n }\n return bestIsTied ? null : bestFile;\n}\n\n/** Resolve a page name to an absolute `.md` path: exact slug → fuzzy →\n * index-title fallback (for non-ASCII names that slugify to empty).\n * `pageName` may carry the `[[target|display]]` form; `parseWikiLink`\n * strips the display half so the lookup uses just the target. */\nexport async function resolvePagePath(workspace: string, pageName: string): Promise<string | null> {\n const { pagesDir, indexFile } = wikiDirs(workspace);\n const { slugs } = await getPageIndex(pagesDir);\n if (slugs.size === 0) return null;\n\n const { target } = parseWikiLink(pageName);\n const slug = wikiSlugify(target);\n\n if (slug.length > 0) {\n const exact = slugs.get(slug);\n if (exact) return path.join(pagesDir, exact);\n const fuzzy = pickFuzzyMatch(slug, slugs);\n if (fuzzy) return path.join(pagesDir, fuzzy);\n }\n\n const entries = parseIndexEntries(readFileOrEmpty(indexFile));\n const titleMatch = entries.find((entry) => entry.title === target);\n if (titleMatch) {\n const file = slugs.get(titleMatch.slug);\n if (file) return path.join(pagesDir, file);\n }\n return null;\n}\n\n/** Raw `index.md` content + its parsed entries. */\nexport function readWikiIndex(workspace: string): { content: string; entries: WikiPageEntry[] } {\n const content = readFileOrEmpty(wikiDirs(workspace).indexFile);\n return { content, entries: parseIndexEntries(content) };\n}\n\n/** Raw `log.md` content (empty string if absent). */\nexport function readWikiLog(workspace: string): string {\n return readFileOrEmpty(wikiDirs(workspace).logFile);\n}\n\nexport interface WikiPageRead {\n /** Absolute path of the resolved file, or null when nothing matched. */\n filePath: string | null;\n /** File body (empty when missing OR when the file is an empty placeholder). */\n content: string;\n /** True iff a page file resolved (distinct from empty content). */\n exists: boolean;\n /** Title to display — the resolved filename stem, or the raw pageName. */\n resolvedTitle: string;\n}\n\n/** Resolve + read a page. Distinguishes missing (`exists: false`) from\n * empty-but-present (`exists: true`, `content: \"\"`). */\nexport async function readWikiPage(workspace: string, pageName: string): Promise<WikiPageRead> {\n const filePath = await resolvePagePath(workspace, pageName);\n const content = filePath ? readFileOrEmpty(filePath) : \"\";\n const resolvedTitle = filePath ? path.basename(filePath, \".md\") : pageName;\n return { filePath, content, exists: Boolean(filePath), resolvedTitle };\n}\n\n/** Read every page + the index and build the page→page link graph.\n * No cache: the graph is requested explicitly and a content edit does\n * not advance the pagesDir mtime the page index caches on. */\nexport async function loadWikiGraph(workspace: string): Promise<WikiGraph> {\n const { pagesDir, indexFile } = wikiDirs(workspace);\n const { slugs } = await getPageIndex(pagesDir);\n const fileEntries = [...slugs.entries()];\n const contents = await Promise.all(fileEntries.map(async ([, fileName]) => (await readTextSafe(path.join(pagesDir, fileName))) ?? \"\"));\n const pages = fileEntries.map(([slug], i) => ({ slug, content: contents[i] }));\n const indexEntries = parseIndexEntries(readFileOrEmpty(indexFile));\n return buildWikiGraph(pages, indexEntries);\n}\n\n/** Run every lint rule over the on-disk wiki, returning issue strings. */\nexport async function collectLintIssues(workspace: string): Promise<string[]> {\n const { pagesDir, indexFile } = wikiDirs(workspace);\n const { slugs } = await getPageIndex(pagesDir);\n if (slugs.size === 0) {\n return [\"- Wiki `pages/` directory does not exist yet. Start ingesting sources.\"];\n }\n const pageEntries = parseIndexEntries(readFileOrEmpty(indexFile));\n const indexedSlugs = new Set(pageEntries.map((entry) => entry.slug));\n const pageFiles = [...slugs.values()];\n const fileSlugs = new Set(slugs.keys());\n\n const issues: string[] = [];\n issues.push(...findOrphanPages(fileSlugs, indexedSlugs));\n issues.push(...findMissingFiles(pageEntries, fileSlugs));\n const contents = await Promise.all(pageFiles.map(async (fileName) => (await readTextSafe(path.join(pagesDir, fileName))) ?? \"\"));\n const frontmatterTagsBySlug = new Map<string, string[]>();\n for (let i = 0; i < pageFiles.length; i++) {\n issues.push(...findBrokenLinksInPage(pageFiles[i], contents[i], fileSlugs));\n // Lowercase the key so a `MyPage.md` filename matches an\n // `entry.slug` of `mypage`; `findTagDrift` lowercases the lookup.\n const slug = pageFiles[i].replace(/\\.md$/i, \"\").toLowerCase();\n frontmatterTagsBySlug.set(slug, parseFrontmatterTags(contents[i]));\n }\n issues.push(...findTagDrift(pageEntries, frontmatterTagsBySlug));\n return issues;\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"index.cjs","names":[],"sources":["../../../src/wiki/server/fs.ts","../../../src/wiki/server/pageIndex.ts","../../../src/wiki/server/frontmatter.ts","../../../src/wiki/server/engine.ts"],"sourcesContent":["// Minimal ENOENT-swallowing fs wrappers for the wiki read-engine.\n// Self-contained (core can't reach the host's `server/utils/files/safe.ts`);\n// only the handful of helpers the wiki engine needs. Same \"return\n// null / [] on any error\" contract so callers branch on the value\n// instead of try/catch.\n\nimport type { Dirent, Stats } from \"node:fs\";\nimport { readFileSync } from \"node:fs\";\nimport { readFile, readdir, stat } from \"node:fs/promises\";\n\nexport async function readTextSafe(absPath: string): Promise<string | null> {\n try {\n return await readFile(absPath, \"utf-8\");\n } catch {\n return null;\n }\n}\n\nexport function readTextSafeSync(absPath: string): string | null {\n try {\n return readFileSync(absPath, \"utf-8\");\n } catch {\n return null;\n }\n}\n\nexport async function statSafeAsync(absPath: string): Promise<Stats | null> {\n try {\n return await stat(absPath);\n } catch {\n return null;\n }\n}\n\nexport async function readDirSafeAsync(absPath: string): Promise<Dirent[]> {\n try {\n return await readdir(absPath, { withFileTypes: true });\n } catch {\n return [];\n }\n}\n","// In-memory index of `<pagesDir>/*.md` keyed by slug (= filename\n// without the `.md` extension). Kept fresh via `pagesDir` mtime —\n// adding, removing, or renaming a file under `pagesDir` advances the\n// directory's mtime on every major filesystem we target (macOS APFS,\n// Linux ext4, Windows NTFS), so one cheap `stat()` per request is\n// enough to decide whether to rebuild.\n\nimport { readDirSafeAsync, statSafeAsync } from \"./fs.js\";\n\nexport interface PageIndex {\n mtimeMs: number;\n /** slug → filename (e.g. \"sakura-internet\" → \"sakura-internet.md\"). */\n slugs: Map<string, string>;\n}\n\n// Keyed by `pagesDir`: the engine is workspace-injected and may be\n// called with more than one pages directory in a single process. A\n// single global slot would let one workspace's slug map be returned\n// for another whenever their mtimes happen to tie (Codex P2 on #1876).\nconst cache = new Map<string, PageIndex>();\n\n/**\n * Get the page index for `pagesDir`. Returns a cached value as long as\n * THAT directory's mtime hasn't advanced; otherwise rebuilds. Safe to\n * call concurrently — racing builds produce the same result.\n */\nexport async function getPageIndex(pagesDir: string): Promise<PageIndex> {\n const stat = await statSafeAsync(pagesDir);\n if (!stat) {\n // Dir doesn't exist yet (never ingested). Return empty but don't\n // cache a stale-forever value — the next call re-stats.\n return { mtimeMs: 0, slugs: new Map() };\n }\n const cached = cache.get(pagesDir);\n if (cached && cached.mtimeMs >= stat.mtimeMs) return cached;\n const entries = await readDirSafeAsync(pagesDir);\n const slugs = new Map<string, string>();\n for (const entry of entries) {\n if (!entry.isFile()) continue;\n const { name } = entry;\n if (!name.endsWith(\".md\")) continue;\n slugs.set(name.slice(0, -\".md\".length), name);\n }\n const built: PageIndex = { mtimeMs: stat.mtimeMs, slugs };\n cache.set(pagesDir, built);\n return built;\n}\n\n/** Test-only: drop the module-level cache (all directories). */\nexport function __resetPageIndexCache(): void {\n cache.clear();\n}\n","// Read-side frontmatter helpers for the wiki engine. The generic\n// `---\\n…\\n---\\n` envelope parsing is the canonical value-preserving\n// (FAILSAFE_SCHEMA) parser in `@mulmoclaude/markdown-utils`; only the\n// wiki-specific `tags:` reader stays here. Re-exporting `parseFrontmatter`\n// keeps the `@mulmoclaude/core/wiki/server` barrel surface stable — it now\n// returns the richer `{ meta, body, hasHeader }` shape.\n\nimport { parseFrontmatter } from \"@mulmoclaude/markdown-utils/markdown/frontmatter\";\n\nexport { parseFrontmatter };\n\nfunction cleanTagToken(token: string): string {\n return token\n .trim()\n .replace(/^['\"]|['\"]$/g, \"\")\n .replace(/^#/, \"\")\n .toLowerCase();\n}\n\n/** Narrow `tags:` reader. Handles flow (`tags: [a, b]`) and block-list\n * style; anything unparseable returns `[]` (best-effort, never throws). */\nexport function parseFrontmatterTags(content: string): string[] {\n const parsed = parseFrontmatter(content);\n if (!parsed.hasHeader) return [];\n const tagsValue = parsed.meta.tags;\n if (!Array.isArray(tagsValue)) return [];\n return tagsValue\n .filter((item): item is string => typeof item === \"string\")\n .map(cleanTagToken)\n .filter((token) => token.length > 0);\n}\n","// Workspace-injected wiki read-engine — the filesystem layer that feeds\n// the pure helpers in `@mulmoclaude/core/wiki`. Shared by every host so\n// the two apps reading the same `data/wiki/` can't disagree on slug\n// resolution, graph edges, or lint findings. Mirrors the bodies that\n// previously lived in MulmoClaude's `server/api/routes/wiki.ts`; the\n// hosts keep only their HTTP response shaping on top.\n//\n// The read/write WRITE side (writeWikiPage / snapshots) stays host-side\n// until a host needs it shared.\n\nimport path from \"node:path\";\nimport { parseWikiLink } from \"../link.js\";\nimport { wikiSlugify } from \"../slug.js\";\nimport { type WikiPageEntry, parseIndexEntries } from \"../index-parse.js\";\nimport { findBrokenLinksInPage, findMissingFiles, findOrphanPages, findTagDrift } from \"../lint.js\";\nimport { type WikiGraph, buildWikiGraph } from \"../graph.js\";\nimport { readTextSafe, readTextSafeSync } from \"./fs.js\";\nimport { getPageIndex } from \"./pageIndex.js\";\nimport { parseFrontmatterTags } from \"./frontmatter.js\";\nimport { wikiDirs } from \"./paths.js\";\n\n// Below this length the fuzzy `includes` step is skipped — CJK /\n// emoji-only / very-short page names slugify down to a short noise\n// tail that partial-matches almost anything; the index.md title-match\n// fallback still handles the legitimate non-ASCII case (#1194).\nconst MIN_FUZZY_SLUG_LEN = 6;\n\nfunction readFileOrEmpty(absPath: string): string {\n return readTextSafeSync(absPath) ?? \"\";\n}\n\n/** Walk every indexed slug for an `includes`-style match. Returns the\n * single best candidate, or null when the slug is too short OR several\n * candidates tie at the top score (ambiguous → defer to the caller's\n * title-match fallback). Score = min/max length, decoupled from Map\n * iteration order so resolution is deterministic across hosts. */\nexport function pickFuzzyMatch(slug: string, slugs: ReadonlyMap<string, string>): string | null {\n if (slug.length < MIN_FUZZY_SLUG_LEN) return null;\n let bestFile: string | null = null;\n let bestScore = 0;\n let bestIsTied = false;\n for (const [key, file] of slugs) {\n if (!slug.includes(key) && !key.includes(slug)) continue;\n const shorter = Math.min(slug.length, key.length);\n const longer = Math.max(slug.length, key.length);\n const score = shorter / longer;\n if (score > bestScore) {\n bestScore = score;\n bestFile = file;\n bestIsTied = false;\n } else if (score === bestScore) {\n bestIsTied = true;\n }\n }\n return bestIsTied ? null : bestFile;\n}\n\n/** Resolve a page name to an absolute `.md` path: exact slug → fuzzy →\n * index-title fallback (for non-ASCII names that slugify to empty).\n * `pageName` may carry the `[[target|display]]` form; `parseWikiLink`\n * strips the display half so the lookup uses just the target. */\nexport async function resolvePagePath(workspace: string, pageName: string): Promise<string | null> {\n const { pagesDir, indexFile } = wikiDirs(workspace);\n const { slugs } = await getPageIndex(pagesDir);\n if (slugs.size === 0) return null;\n\n const { target } = parseWikiLink(pageName);\n const slug = wikiSlugify(target);\n\n if (slug.length > 0) {\n const exact = slugs.get(slug);\n if (exact) return path.join(pagesDir, exact);\n const fuzzy = pickFuzzyMatch(slug, slugs);\n if (fuzzy) return path.join(pagesDir, fuzzy);\n }\n\n const entries = parseIndexEntries(readFileOrEmpty(indexFile));\n const titleMatch = entries.find((entry) => entry.title === target);\n if (titleMatch) {\n const file = slugs.get(titleMatch.slug);\n if (file) return path.join(pagesDir, file);\n }\n return null;\n}\n\n/** Raw `index.md` content + its parsed entries. */\nexport function readWikiIndex(workspace: string): { content: string; entries: WikiPageEntry[] } {\n const content = readFileOrEmpty(wikiDirs(workspace).indexFile);\n return { content, entries: parseIndexEntries(content) };\n}\n\n/** Raw `log.md` content (empty string if absent). */\nexport function readWikiLog(workspace: string): string {\n return readFileOrEmpty(wikiDirs(workspace).logFile);\n}\n\nexport interface WikiPageRead {\n /** Absolute path of the resolved file, or null when nothing matched. */\n filePath: string | null;\n /** File body (empty when missing OR when the file is an empty placeholder). */\n content: string;\n /** True iff a page file resolved (distinct from empty content). */\n exists: boolean;\n /** Title to display — the resolved filename stem, or the raw pageName. */\n resolvedTitle: string;\n}\n\n/** Resolve + read a page. Distinguishes missing (`exists: false`) from\n * empty-but-present (`exists: true`, `content: \"\"`). */\nexport async function readWikiPage(workspace: string, pageName: string): Promise<WikiPageRead> {\n const filePath = await resolvePagePath(workspace, pageName);\n const content = filePath ? readFileOrEmpty(filePath) : \"\";\n const resolvedTitle = filePath ? path.basename(filePath, \".md\") : pageName;\n return { filePath, content, exists: Boolean(filePath), resolvedTitle };\n}\n\n/** Read every page + the index and build the page→page link graph.\n * No cache: the graph is requested explicitly and a content edit does\n * not advance the pagesDir mtime the page index caches on. */\nexport async function loadWikiGraph(workspace: string): Promise<WikiGraph> {\n const { pagesDir, indexFile } = wikiDirs(workspace);\n const { slugs } = await getPageIndex(pagesDir);\n const fileEntries = [...slugs.entries()];\n const contents = await Promise.all(fileEntries.map(async ([, fileName]) => (await readTextSafe(path.join(pagesDir, fileName))) ?? \"\"));\n const pages = fileEntries.map(([slug], i) => ({ slug, content: contents[i] }));\n const indexEntries = parseIndexEntries(readFileOrEmpty(indexFile));\n return buildWikiGraph(pages, indexEntries);\n}\n\n/** Run every lint rule over the on-disk wiki, returning issue strings. */\nexport async function collectLintIssues(workspace: string): Promise<string[]> {\n const { pagesDir, indexFile } = wikiDirs(workspace);\n const { slugs } = await getPageIndex(pagesDir);\n if (slugs.size === 0) {\n return [\"- Wiki `pages/` directory does not exist yet. Start ingesting sources.\"];\n }\n const pageEntries = parseIndexEntries(readFileOrEmpty(indexFile));\n const indexedSlugs = new Set(pageEntries.map((entry) => entry.slug));\n const pageFiles = [...slugs.values()];\n const fileSlugs = new Set(slugs.keys());\n\n const issues: string[] = [];\n issues.push(...findOrphanPages(fileSlugs, indexedSlugs));\n issues.push(...findMissingFiles(pageEntries, fileSlugs));\n const contents = await Promise.all(pageFiles.map(async (fileName) => (await readTextSafe(path.join(pagesDir, fileName))) ?? \"\"));\n const frontmatterTagsBySlug = new Map<string, string[]>();\n for (let i = 0; i < pageFiles.length; i++) {\n issues.push(...findBrokenLinksInPage(pageFiles[i], contents[i], fileSlugs));\n // Lowercase the key so a `MyPage.md` filename matches an\n // `entry.slug` of `mypage`; `findTagDrift` lowercases the lookup.\n const slug = pageFiles[i].replace(/\\.md$/i, \"\").toLowerCase();\n frontmatterTagsBySlug.set(slug, parseFrontmatterTags(contents[i]));\n }\n issues.push(...findTagDrift(pageEntries, frontmatterTagsBySlug));\n return issues;\n}\n"],"mappings":"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"}
|
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
import { a as findBrokenLinksInPage, c as findTagDrift, h as parseIndexEntries, o as findMissingFiles, s as findOrphanPages, t as buildWikiGraph, v as parseWikiLink } from "../../graph-
|
|
1
|
+
import { a as findBrokenLinksInPage, c as findTagDrift, h as parseIndexEntries, o as findMissingFiles, s as findOrphanPages, t as buildWikiGraph, v as parseWikiLink } from "../../graph-BcwqiSpR.js";
|
|
2
2
|
import { n as wikiSlugify } from "../../slug-CdN-pQX1.js";
|
|
3
3
|
import { wikiDirs, wikiSlugFromAbsPath } from "../paths.js";
|
|
4
|
-
import path from "node:path";
|
|
5
4
|
import { readFileSync } from "node:fs";
|
|
5
|
+
import path from "node:path";
|
|
6
6
|
import { readFile, readdir, stat } from "node:fs/promises";
|
|
7
|
-
import {
|
|
7
|
+
import { parseFrontmatter } from "@mulmoclaude/markdown-utils/markdown/frontmatter";
|
|
8
8
|
//#region src/wiki/server/fs.ts
|
|
9
9
|
async function readTextSafe(absPath) {
|
|
10
10
|
try {
|
|
@@ -71,42 +71,6 @@ function __resetPageIndexCache() {
|
|
|
71
71
|
}
|
|
72
72
|
//#endregion
|
|
73
73
|
//#region src/wiki/server/frontmatter.ts
|
|
74
|
-
var FRONTMATTER_OPEN = /^---\r?\n/;
|
|
75
|
-
var FRONTMATTER_CLOSE = /(?:^|\r?\n)---\s*(?:\r?\n|$)/;
|
|
76
|
-
/** Parse `---\n…\n---\n` frontmatter. Never throws; malformed YAML in a
|
|
77
|
-
* well-formed envelope yields `{ meta: {}, hasHeader: false }`. */
|
|
78
|
-
function parseFrontmatter(raw) {
|
|
79
|
-
if (!FRONTMATTER_OPEN.test(raw)) return {
|
|
80
|
-
meta: {},
|
|
81
|
-
hasHeader: false
|
|
82
|
-
};
|
|
83
|
-
const afterOpen = raw.replace(FRONTMATTER_OPEN, "");
|
|
84
|
-
const closeMatch = FRONTMATTER_CLOSE.exec(afterOpen);
|
|
85
|
-
if (!closeMatch || closeMatch.index === void 0) return {
|
|
86
|
-
meta: {},
|
|
87
|
-
hasHeader: false
|
|
88
|
-
};
|
|
89
|
-
const meta = safeYamlLoad(afterOpen.slice(0, closeMatch.index));
|
|
90
|
-
if (meta === null) return {
|
|
91
|
-
meta: {},
|
|
92
|
-
hasHeader: false
|
|
93
|
-
};
|
|
94
|
-
return {
|
|
95
|
-
meta,
|
|
96
|
-
hasHeader: true
|
|
97
|
-
};
|
|
98
|
-
}
|
|
99
|
-
function safeYamlLoad(text) {
|
|
100
|
-
if (text.trim() === "") return {};
|
|
101
|
-
try {
|
|
102
|
-
const loaded = load(text, { schema: FAILSAFE_SCHEMA });
|
|
103
|
-
if (loaded === null || loaded === void 0) return {};
|
|
104
|
-
if (typeof loaded !== "object" || Array.isArray(loaded)) return null;
|
|
105
|
-
return loaded;
|
|
106
|
-
} catch {
|
|
107
|
-
return null;
|
|
108
|
-
}
|
|
109
|
-
}
|
|
110
74
|
function cleanTagToken(token) {
|
|
111
75
|
return token.trim().replace(/^['"]|['"]$/g, "").replace(/^#/, "").toLowerCase();
|
|
112
76
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","names":[],"sources":["../../../src/wiki/server/fs.ts","../../../src/wiki/server/pageIndex.ts","../../../src/wiki/server/frontmatter.ts","../../../src/wiki/server/engine.ts"],"sourcesContent":["// Minimal ENOENT-swallowing fs wrappers for the wiki read-engine.\n// Self-contained (core can't reach the host's `server/utils/files/safe.ts`);\n// only the handful of helpers the wiki engine needs. Same \"return\n// null / [] on any error\" contract so callers branch on the value\n// instead of try/catch.\n\nimport type { Dirent, Stats } from \"node:fs\";\nimport { readFileSync } from \"node:fs\";\nimport { readFile, readdir, stat } from \"node:fs/promises\";\n\nexport async function readTextSafe(absPath: string): Promise<string | null> {\n try {\n return await readFile(absPath, \"utf-8\");\n } catch {\n return null;\n }\n}\n\nexport function readTextSafeSync(absPath: string): string | null {\n try {\n return readFileSync(absPath, \"utf-8\");\n } catch {\n return null;\n }\n}\n\nexport async function statSafeAsync(absPath: string): Promise<Stats | null> {\n try {\n return await stat(absPath);\n } catch {\n return null;\n }\n}\n\nexport async function readDirSafeAsync(absPath: string): Promise<Dirent[]> {\n try {\n return await readdir(absPath, { withFileTypes: true });\n } catch {\n return [];\n }\n}\n","// In-memory index of `<pagesDir>/*.md` keyed by slug (= filename\n// without the `.md` extension). Kept fresh via `pagesDir` mtime —\n// adding, removing, or renaming a file under `pagesDir` advances the\n// directory's mtime on every major filesystem we target (macOS APFS,\n// Linux ext4, Windows NTFS), so one cheap `stat()` per request is\n// enough to decide whether to rebuild.\n\nimport { readDirSafeAsync, statSafeAsync } from \"./fs.js\";\n\nexport interface PageIndex {\n mtimeMs: number;\n /** slug → filename (e.g. \"sakura-internet\" → \"sakura-internet.md\"). */\n slugs: Map<string, string>;\n}\n\n// Keyed by `pagesDir`: the engine is workspace-injected and may be\n// called with more than one pages directory in a single process. A\n// single global slot would let one workspace's slug map be returned\n// for another whenever their mtimes happen to tie (Codex P2 on #1876).\nconst cache = new Map<string, PageIndex>();\n\n/**\n * Get the page index for `pagesDir`. Returns a cached value as long as\n * THAT directory's mtime hasn't advanced; otherwise rebuilds. Safe to\n * call concurrently — racing builds produce the same result.\n */\nexport async function getPageIndex(pagesDir: string): Promise<PageIndex> {\n const stat = await statSafeAsync(pagesDir);\n if (!stat) {\n // Dir doesn't exist yet (never ingested). Return empty but don't\n // cache a stale-forever value — the next call re-stats.\n return { mtimeMs: 0, slugs: new Map() };\n }\n const cached = cache.get(pagesDir);\n if (cached && cached.mtimeMs >= stat.mtimeMs) return cached;\n const entries = await readDirSafeAsync(pagesDir);\n const slugs = new Map<string, string>();\n for (const entry of entries) {\n if (!entry.isFile()) continue;\n const { name } = entry;\n if (!name.endsWith(\".md\")) continue;\n slugs.set(name.slice(0, -\".md\".length), name);\n }\n const built: PageIndex = { mtimeMs: stat.mtimeMs, slugs };\n cache.set(pagesDir, built);\n return built;\n}\n\n/** Test-only: drop the module-level cache (all directories). */\nexport function __resetPageIndexCache(): void {\n cache.clear();\n}\n","// Read-side YAML frontmatter parsing for the wiki engine. Mirrors the\n// host's `server/utils/markdown/frontmatter.ts` parser (value-preserving\n// FAILSAFE_SCHEMA) so MulmoClaude and MulmoTerminal read `tags:` (and\n// any other field) identically. Only the READ path lives here — the\n// serialize/merge write side stays host-side until a host needs it.\n\nimport { FAILSAFE_SCHEMA, load as yamlLoad } from \"js-yaml\";\n\nconst FRONTMATTER_OPEN = /^---\\r?\\n/;\n// `(?:^|\\r?\\n)` lets the closing fence sit at the very start of\n// `afterOpen` — needed for the empty-envelope case `---\\n---\\n`.\nconst FRONTMATTER_CLOSE = /(?:^|\\r?\\n)---\\s*(?:\\r?\\n|$)/;\n\n/** Parse `---\\n…\\n---\\n` frontmatter. Never throws; malformed YAML in a\n * well-formed envelope yields `{ meta: {}, hasHeader: false }`. */\nexport function parseFrontmatter(raw: string): { meta: Record<string, unknown>; hasHeader: boolean } {\n if (!FRONTMATTER_OPEN.test(raw)) return { meta: {}, hasHeader: false };\n const afterOpen = raw.replace(FRONTMATTER_OPEN, \"\");\n const closeMatch = FRONTMATTER_CLOSE.exec(afterOpen);\n if (!closeMatch || closeMatch.index === undefined) return { meta: {}, hasHeader: false };\n const meta = safeYamlLoad(afterOpen.slice(0, closeMatch.index));\n if (meta === null) return { meta: {}, hasHeader: false };\n return { meta, hasHeader: true };\n}\n\nfunction safeYamlLoad(text: string): Record<string, unknown> | null {\n // js-yaml throws on empty/whitespace-only input; an empty header\n // (`---\\n---\\n`) means \"no metadata\", not \"malformed\".\n if (text.trim() === \"\") return {};\n try {\n // FAILSAFE_SCHEMA keeps every scalar a string (no YAML-1.1 date\n // coercion, no numeric-string truncation) — matches the host.\n const loaded = yamlLoad(text, { schema: FAILSAFE_SCHEMA });\n if (loaded === null || loaded === undefined) return {};\n if (typeof loaded !== \"object\" || Array.isArray(loaded)) return null;\n return loaded as Record<string, unknown>;\n } catch {\n return null;\n }\n}\n\nfunction cleanTagToken(token: string): string {\n return token\n .trim()\n .replace(/^['\"]|['\"]$/g, \"\")\n .replace(/^#/, \"\")\n .toLowerCase();\n}\n\n/** Narrow `tags:` reader. Handles flow (`tags: [a, b]`) and block-list\n * style; anything unparseable returns `[]` (best-effort, never throws). */\nexport function parseFrontmatterTags(content: string): string[] {\n const parsed = parseFrontmatter(content);\n if (!parsed.hasHeader) return [];\n const tagsValue = parsed.meta.tags;\n if (!Array.isArray(tagsValue)) return [];\n return tagsValue\n .filter((item): item is string => typeof item === \"string\")\n .map(cleanTagToken)\n .filter((token) => token.length > 0);\n}\n","// Workspace-injected wiki read-engine — the filesystem layer that feeds\n// the pure helpers in `@mulmoclaude/core/wiki`. Shared by every host so\n// the two apps reading the same `data/wiki/` can't disagree on slug\n// resolution, graph edges, or lint findings. Mirrors the bodies that\n// previously lived in MulmoClaude's `server/api/routes/wiki.ts`; the\n// hosts keep only their HTTP response shaping on top.\n//\n// The read/write WRITE side (writeWikiPage / snapshots) stays host-side\n// until a host needs it shared.\n\nimport path from \"node:path\";\nimport { parseWikiLink } from \"../link.js\";\nimport { wikiSlugify } from \"../slug.js\";\nimport { type WikiPageEntry, parseIndexEntries } from \"../index-parse.js\";\nimport { findBrokenLinksInPage, findMissingFiles, findOrphanPages, findTagDrift } from \"../lint.js\";\nimport { type WikiGraph, buildWikiGraph } from \"../graph.js\";\nimport { readTextSafe, readTextSafeSync } from \"./fs.js\";\nimport { getPageIndex } from \"./pageIndex.js\";\nimport { parseFrontmatterTags } from \"./frontmatter.js\";\nimport { wikiDirs } from \"./paths.js\";\n\n// Below this length the fuzzy `includes` step is skipped — CJK /\n// emoji-only / very-short page names slugify down to a short noise\n// tail that partial-matches almost anything; the index.md title-match\n// fallback still handles the legitimate non-ASCII case (#1194).\nconst MIN_FUZZY_SLUG_LEN = 6;\n\nfunction readFileOrEmpty(absPath: string): string {\n return readTextSafeSync(absPath) ?? \"\";\n}\n\n/** Walk every indexed slug for an `includes`-style match. Returns the\n * single best candidate, or null when the slug is too short OR several\n * candidates tie at the top score (ambiguous → defer to the caller's\n * title-match fallback). Score = min/max length, decoupled from Map\n * iteration order so resolution is deterministic across hosts. */\nexport function pickFuzzyMatch(slug: string, slugs: ReadonlyMap<string, string>): string | null {\n if (slug.length < MIN_FUZZY_SLUG_LEN) return null;\n let bestFile: string | null = null;\n let bestScore = 0;\n let bestIsTied = false;\n for (const [key, file] of slugs) {\n if (!slug.includes(key) && !key.includes(slug)) continue;\n const shorter = Math.min(slug.length, key.length);\n const longer = Math.max(slug.length, key.length);\n const score = shorter / longer;\n if (score > bestScore) {\n bestScore = score;\n bestFile = file;\n bestIsTied = false;\n } else if (score === bestScore) {\n bestIsTied = true;\n }\n }\n return bestIsTied ? null : bestFile;\n}\n\n/** Resolve a page name to an absolute `.md` path: exact slug → fuzzy →\n * index-title fallback (for non-ASCII names that slugify to empty).\n * `pageName` may carry the `[[target|display]]` form; `parseWikiLink`\n * strips the display half so the lookup uses just the target. */\nexport async function resolvePagePath(workspace: string, pageName: string): Promise<string | null> {\n const { pagesDir, indexFile } = wikiDirs(workspace);\n const { slugs } = await getPageIndex(pagesDir);\n if (slugs.size === 0) return null;\n\n const { target } = parseWikiLink(pageName);\n const slug = wikiSlugify(target);\n\n if (slug.length > 0) {\n const exact = slugs.get(slug);\n if (exact) return path.join(pagesDir, exact);\n const fuzzy = pickFuzzyMatch(slug, slugs);\n if (fuzzy) return path.join(pagesDir, fuzzy);\n }\n\n const entries = parseIndexEntries(readFileOrEmpty(indexFile));\n const titleMatch = entries.find((entry) => entry.title === target);\n if (titleMatch) {\n const file = slugs.get(titleMatch.slug);\n if (file) return path.join(pagesDir, file);\n }\n return null;\n}\n\n/** Raw `index.md` content + its parsed entries. */\nexport function readWikiIndex(workspace: string): { content: string; entries: WikiPageEntry[] } {\n const content = readFileOrEmpty(wikiDirs(workspace).indexFile);\n return { content, entries: parseIndexEntries(content) };\n}\n\n/** Raw `log.md` content (empty string if absent). */\nexport function readWikiLog(workspace: string): string {\n return readFileOrEmpty(wikiDirs(workspace).logFile);\n}\n\nexport interface WikiPageRead {\n /** Absolute path of the resolved file, or null when nothing matched. */\n filePath: string | null;\n /** File body (empty when missing OR when the file is an empty placeholder). */\n content: string;\n /** True iff a page file resolved (distinct from empty content). */\n exists: boolean;\n /** Title to display — the resolved filename stem, or the raw pageName. */\n resolvedTitle: string;\n}\n\n/** Resolve + read a page. Distinguishes missing (`exists: false`) from\n * empty-but-present (`exists: true`, `content: \"\"`). */\nexport async function readWikiPage(workspace: string, pageName: string): Promise<WikiPageRead> {\n const filePath = await resolvePagePath(workspace, pageName);\n const content = filePath ? readFileOrEmpty(filePath) : \"\";\n const resolvedTitle = filePath ? path.basename(filePath, \".md\") : pageName;\n return { filePath, content, exists: Boolean(filePath), resolvedTitle };\n}\n\n/** Read every page + the index and build the page→page link graph.\n * No cache: the graph is requested explicitly and a content edit does\n * not advance the pagesDir mtime the page index caches on. */\nexport async function loadWikiGraph(workspace: string): Promise<WikiGraph> {\n const { pagesDir, indexFile } = wikiDirs(workspace);\n const { slugs } = await getPageIndex(pagesDir);\n const fileEntries = [...slugs.entries()];\n const contents = await Promise.all(fileEntries.map(async ([, fileName]) => (await readTextSafe(path.join(pagesDir, fileName))) ?? \"\"));\n const pages = fileEntries.map(([slug], i) => ({ slug, content: contents[i] }));\n const indexEntries = parseIndexEntries(readFileOrEmpty(indexFile));\n return buildWikiGraph(pages, indexEntries);\n}\n\n/** Run every lint rule over the on-disk wiki, returning issue strings. */\nexport async function collectLintIssues(workspace: string): Promise<string[]> {\n const { pagesDir, indexFile } = wikiDirs(workspace);\n const { slugs } = await getPageIndex(pagesDir);\n if (slugs.size === 0) {\n return [\"- Wiki `pages/` directory does not exist yet. Start ingesting sources.\"];\n }\n const pageEntries = parseIndexEntries(readFileOrEmpty(indexFile));\n const indexedSlugs = new Set(pageEntries.map((entry) => entry.slug));\n const pageFiles = [...slugs.values()];\n const fileSlugs = new Set(slugs.keys());\n\n const issues: string[] = [];\n issues.push(...findOrphanPages(fileSlugs, indexedSlugs));\n issues.push(...findMissingFiles(pageEntries, fileSlugs));\n const contents = await Promise.all(pageFiles.map(async (fileName) => (await readTextSafe(path.join(pagesDir, fileName))) ?? \"\"));\n const frontmatterTagsBySlug = new Map<string, string[]>();\n for (let i = 0; i < pageFiles.length; i++) {\n issues.push(...findBrokenLinksInPage(pageFiles[i], contents[i], fileSlugs));\n // Lowercase the key so a `MyPage.md` filename matches an\n // `entry.slug` of `mypage`; `findTagDrift` lowercases the lookup.\n const slug = pageFiles[i].replace(/\\.md$/i, \"\").toLowerCase();\n frontmatterTagsBySlug.set(slug, parseFrontmatterTags(contents[i]));\n }\n issues.push(...findTagDrift(pageEntries, frontmatterTagsBySlug));\n return issues;\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"index.js","names":[],"sources":["../../../src/wiki/server/fs.ts","../../../src/wiki/server/pageIndex.ts","../../../src/wiki/server/frontmatter.ts","../../../src/wiki/server/engine.ts"],"sourcesContent":["// Minimal ENOENT-swallowing fs wrappers for the wiki read-engine.\n// Self-contained (core can't reach the host's `server/utils/files/safe.ts`);\n// only the handful of helpers the wiki engine needs. Same \"return\n// null / [] on any error\" contract so callers branch on the value\n// instead of try/catch.\n\nimport type { Dirent, Stats } from \"node:fs\";\nimport { readFileSync } from \"node:fs\";\nimport { readFile, readdir, stat } from \"node:fs/promises\";\n\nexport async function readTextSafe(absPath: string): Promise<string | null> {\n try {\n return await readFile(absPath, \"utf-8\");\n } catch {\n return null;\n }\n}\n\nexport function readTextSafeSync(absPath: string): string | null {\n try {\n return readFileSync(absPath, \"utf-8\");\n } catch {\n return null;\n }\n}\n\nexport async function statSafeAsync(absPath: string): Promise<Stats | null> {\n try {\n return await stat(absPath);\n } catch {\n return null;\n }\n}\n\nexport async function readDirSafeAsync(absPath: string): Promise<Dirent[]> {\n try {\n return await readdir(absPath, { withFileTypes: true });\n } catch {\n return [];\n }\n}\n","// In-memory index of `<pagesDir>/*.md` keyed by slug (= filename\n// without the `.md` extension). Kept fresh via `pagesDir` mtime —\n// adding, removing, or renaming a file under `pagesDir` advances the\n// directory's mtime on every major filesystem we target (macOS APFS,\n// Linux ext4, Windows NTFS), so one cheap `stat()` per request is\n// enough to decide whether to rebuild.\n\nimport { readDirSafeAsync, statSafeAsync } from \"./fs.js\";\n\nexport interface PageIndex {\n mtimeMs: number;\n /** slug → filename (e.g. \"sakura-internet\" → \"sakura-internet.md\"). */\n slugs: Map<string, string>;\n}\n\n// Keyed by `pagesDir`: the engine is workspace-injected and may be\n// called with more than one pages directory in a single process. A\n// single global slot would let one workspace's slug map be returned\n// for another whenever their mtimes happen to tie (Codex P2 on #1876).\nconst cache = new Map<string, PageIndex>();\n\n/**\n * Get the page index for `pagesDir`. Returns a cached value as long as\n * THAT directory's mtime hasn't advanced; otherwise rebuilds. Safe to\n * call concurrently — racing builds produce the same result.\n */\nexport async function getPageIndex(pagesDir: string): Promise<PageIndex> {\n const stat = await statSafeAsync(pagesDir);\n if (!stat) {\n // Dir doesn't exist yet (never ingested). Return empty but don't\n // cache a stale-forever value — the next call re-stats.\n return { mtimeMs: 0, slugs: new Map() };\n }\n const cached = cache.get(pagesDir);\n if (cached && cached.mtimeMs >= stat.mtimeMs) return cached;\n const entries = await readDirSafeAsync(pagesDir);\n const slugs = new Map<string, string>();\n for (const entry of entries) {\n if (!entry.isFile()) continue;\n const { name } = entry;\n if (!name.endsWith(\".md\")) continue;\n slugs.set(name.slice(0, -\".md\".length), name);\n }\n const built: PageIndex = { mtimeMs: stat.mtimeMs, slugs };\n cache.set(pagesDir, built);\n return built;\n}\n\n/** Test-only: drop the module-level cache (all directories). */\nexport function __resetPageIndexCache(): void {\n cache.clear();\n}\n","// Read-side frontmatter helpers for the wiki engine. The generic\n// `---\\n…\\n---\\n` envelope parsing is the canonical value-preserving\n// (FAILSAFE_SCHEMA) parser in `@mulmoclaude/markdown-utils`; only the\n// wiki-specific `tags:` reader stays here. Re-exporting `parseFrontmatter`\n// keeps the `@mulmoclaude/core/wiki/server` barrel surface stable — it now\n// returns the richer `{ meta, body, hasHeader }` shape.\n\nimport { parseFrontmatter } from \"@mulmoclaude/markdown-utils/markdown/frontmatter\";\n\nexport { parseFrontmatter };\n\nfunction cleanTagToken(token: string): string {\n return token\n .trim()\n .replace(/^['\"]|['\"]$/g, \"\")\n .replace(/^#/, \"\")\n .toLowerCase();\n}\n\n/** Narrow `tags:` reader. Handles flow (`tags: [a, b]`) and block-list\n * style; anything unparseable returns `[]` (best-effort, never throws). */\nexport function parseFrontmatterTags(content: string): string[] {\n const parsed = parseFrontmatter(content);\n if (!parsed.hasHeader) return [];\n const tagsValue = parsed.meta.tags;\n if (!Array.isArray(tagsValue)) return [];\n return tagsValue\n .filter((item): item is string => typeof item === \"string\")\n .map(cleanTagToken)\n .filter((token) => token.length > 0);\n}\n","// Workspace-injected wiki read-engine — the filesystem layer that feeds\n// the pure helpers in `@mulmoclaude/core/wiki`. Shared by every host so\n// the two apps reading the same `data/wiki/` can't disagree on slug\n// resolution, graph edges, or lint findings. Mirrors the bodies that\n// previously lived in MulmoClaude's `server/api/routes/wiki.ts`; the\n// hosts keep only their HTTP response shaping on top.\n//\n// The read/write WRITE side (writeWikiPage / snapshots) stays host-side\n// until a host needs it shared.\n\nimport path from \"node:path\";\nimport { parseWikiLink } from \"../link.js\";\nimport { wikiSlugify } from \"../slug.js\";\nimport { type WikiPageEntry, parseIndexEntries } from \"../index-parse.js\";\nimport { findBrokenLinksInPage, findMissingFiles, findOrphanPages, findTagDrift } from \"../lint.js\";\nimport { type WikiGraph, buildWikiGraph } from \"../graph.js\";\nimport { readTextSafe, readTextSafeSync } from \"./fs.js\";\nimport { getPageIndex } from \"./pageIndex.js\";\nimport { parseFrontmatterTags } from \"./frontmatter.js\";\nimport { wikiDirs } from \"./paths.js\";\n\n// Below this length the fuzzy `includes` step is skipped — CJK /\n// emoji-only / very-short page names slugify down to a short noise\n// tail that partial-matches almost anything; the index.md title-match\n// fallback still handles the legitimate non-ASCII case (#1194).\nconst MIN_FUZZY_SLUG_LEN = 6;\n\nfunction readFileOrEmpty(absPath: string): string {\n return readTextSafeSync(absPath) ?? \"\";\n}\n\n/** Walk every indexed slug for an `includes`-style match. Returns the\n * single best candidate, or null when the slug is too short OR several\n * candidates tie at the top score (ambiguous → defer to the caller's\n * title-match fallback). Score = min/max length, decoupled from Map\n * iteration order so resolution is deterministic across hosts. */\nexport function pickFuzzyMatch(slug: string, slugs: ReadonlyMap<string, string>): string | null {\n if (slug.length < MIN_FUZZY_SLUG_LEN) return null;\n let bestFile: string | null = null;\n let bestScore = 0;\n let bestIsTied = false;\n for (const [key, file] of slugs) {\n if (!slug.includes(key) && !key.includes(slug)) continue;\n const shorter = Math.min(slug.length, key.length);\n const longer = Math.max(slug.length, key.length);\n const score = shorter / longer;\n if (score > bestScore) {\n bestScore = score;\n bestFile = file;\n bestIsTied = false;\n } else if (score === bestScore) {\n bestIsTied = true;\n }\n }\n return bestIsTied ? null : bestFile;\n}\n\n/** Resolve a page name to an absolute `.md` path: exact slug → fuzzy →\n * index-title fallback (for non-ASCII names that slugify to empty).\n * `pageName` may carry the `[[target|display]]` form; `parseWikiLink`\n * strips the display half so the lookup uses just the target. */\nexport async function resolvePagePath(workspace: string, pageName: string): Promise<string | null> {\n const { pagesDir, indexFile } = wikiDirs(workspace);\n const { slugs } = await getPageIndex(pagesDir);\n if (slugs.size === 0) return null;\n\n const { target } = parseWikiLink(pageName);\n const slug = wikiSlugify(target);\n\n if (slug.length > 0) {\n const exact = slugs.get(slug);\n if (exact) return path.join(pagesDir, exact);\n const fuzzy = pickFuzzyMatch(slug, slugs);\n if (fuzzy) return path.join(pagesDir, fuzzy);\n }\n\n const entries = parseIndexEntries(readFileOrEmpty(indexFile));\n const titleMatch = entries.find((entry) => entry.title === target);\n if (titleMatch) {\n const file = slugs.get(titleMatch.slug);\n if (file) return path.join(pagesDir, file);\n }\n return null;\n}\n\n/** Raw `index.md` content + its parsed entries. */\nexport function readWikiIndex(workspace: string): { content: string; entries: WikiPageEntry[] } {\n const content = readFileOrEmpty(wikiDirs(workspace).indexFile);\n return { content, entries: parseIndexEntries(content) };\n}\n\n/** Raw `log.md` content (empty string if absent). */\nexport function readWikiLog(workspace: string): string {\n return readFileOrEmpty(wikiDirs(workspace).logFile);\n}\n\nexport interface WikiPageRead {\n /** Absolute path of the resolved file, or null when nothing matched. */\n filePath: string | null;\n /** File body (empty when missing OR when the file is an empty placeholder). */\n content: string;\n /** True iff a page file resolved (distinct from empty content). */\n exists: boolean;\n /** Title to display — the resolved filename stem, or the raw pageName. */\n resolvedTitle: string;\n}\n\n/** Resolve + read a page. Distinguishes missing (`exists: false`) from\n * empty-but-present (`exists: true`, `content: \"\"`). */\nexport async function readWikiPage(workspace: string, pageName: string): Promise<WikiPageRead> {\n const filePath = await resolvePagePath(workspace, pageName);\n const content = filePath ? readFileOrEmpty(filePath) : \"\";\n const resolvedTitle = filePath ? path.basename(filePath, \".md\") : pageName;\n return { filePath, content, exists: Boolean(filePath), resolvedTitle };\n}\n\n/** Read every page + the index and build the page→page link graph.\n * No cache: the graph is requested explicitly and a content edit does\n * not advance the pagesDir mtime the page index caches on. */\nexport async function loadWikiGraph(workspace: string): Promise<WikiGraph> {\n const { pagesDir, indexFile } = wikiDirs(workspace);\n const { slugs } = await getPageIndex(pagesDir);\n const fileEntries = [...slugs.entries()];\n const contents = await Promise.all(fileEntries.map(async ([, fileName]) => (await readTextSafe(path.join(pagesDir, fileName))) ?? \"\"));\n const pages = fileEntries.map(([slug], i) => ({ slug, content: contents[i] }));\n const indexEntries = parseIndexEntries(readFileOrEmpty(indexFile));\n return buildWikiGraph(pages, indexEntries);\n}\n\n/** Run every lint rule over the on-disk wiki, returning issue strings. */\nexport async function collectLintIssues(workspace: string): Promise<string[]> {\n const { pagesDir, indexFile } = wikiDirs(workspace);\n const { slugs } = await getPageIndex(pagesDir);\n if (slugs.size === 0) {\n return [\"- Wiki `pages/` directory does not exist yet. Start ingesting sources.\"];\n }\n const pageEntries = parseIndexEntries(readFileOrEmpty(indexFile));\n const indexedSlugs = new Set(pageEntries.map((entry) => entry.slug));\n const pageFiles = [...slugs.values()];\n const fileSlugs = new Set(slugs.keys());\n\n const issues: string[] = [];\n issues.push(...findOrphanPages(fileSlugs, indexedSlugs));\n issues.push(...findMissingFiles(pageEntries, fileSlugs));\n const contents = await Promise.all(pageFiles.map(async (fileName) => (await readTextSafe(path.join(pagesDir, fileName))) ?? \"\"));\n const frontmatterTagsBySlug = new Map<string, string[]>();\n for (let i = 0; i < pageFiles.length; i++) {\n issues.push(...findBrokenLinksInPage(pageFiles[i], contents[i], fileSlugs));\n // Lowercase the key so a `MyPage.md` filename matches an\n // `entry.slug` of `mypage`; `findTagDrift` lowercases the lookup.\n const slug = pageFiles[i].replace(/\\.md$/i, \"\").toLowerCase();\n frontmatterTagsBySlug.set(slug, parseFrontmatterTags(contents[i]));\n }\n issues.push(...findTagDrift(pageEntries, frontmatterTagsBySlug));\n return issues;\n}\n"],"mappings":"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"}
|
|
@@ -3,14 +3,30 @@ import { copyFileSync, existsSync, lstatSync, mkdirSync, readFileSync, readdirSy
|
|
|
3
3
|
import { randomUUID } from "node:crypto";
|
|
4
4
|
import path from "node:path";
|
|
5
5
|
import { fileURLToPath } from "node:url";
|
|
6
|
-
//#region
|
|
6
|
+
//#region ../common/dist/index.js
|
|
7
|
+
/** Narrow `unknown` to a plain object (not null, not array). */
|
|
8
|
+
function isRecord(value) {
|
|
9
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
10
|
+
}
|
|
11
|
+
/** Check that a record has a specific key with a string value. */
|
|
12
|
+
function hasStringProp(value, key) {
|
|
13
|
+
return isRecord(value) && typeof value[key] === "string";
|
|
14
|
+
}
|
|
15
|
+
/** Normalise an unknown thrown value into a human-readable string. Isomorphic
|
|
16
|
+
* (host, bridges, plugins, Vue) — this is the single home for the helper that
|
|
17
|
+
* #2217 could only consolidate for server code, since `@mulmoclaude/core/utils`
|
|
18
|
+
* is server-only.
|
|
19
|
+
*
|
|
20
|
+
* A non-Error object with a non-empty string `details` (gRPC convention) or
|
|
21
|
+
* `message` field surfaces that field — `details` wins — instead of the
|
|
22
|
+
* `[object Object]` a bare `String(err)` would print; an empty-string field
|
|
23
|
+
* falls through. `fallback` covers the error-boundary idiom where a thrown
|
|
24
|
+
* non-Error should read as a descriptive message rather than `String(err)`
|
|
25
|
+
* noise; omit it in logging contexts where `String(err)` is fine. */
|
|
7
26
|
function errorMessage(err, fallback) {
|
|
8
27
|
if (err instanceof Error) return err.message;
|
|
9
|
-
if (err
|
|
10
|
-
|
|
11
|
-
if (typeof obj.details === "string" && obj.details) return obj.details;
|
|
12
|
-
if (typeof obj.message === "string" && obj.message) return obj.message;
|
|
13
|
-
}
|
|
28
|
+
if (hasStringProp(err, "details") && err.details) return err.details;
|
|
29
|
+
if (hasStringProp(err, "message") && err.message) return err.message;
|
|
14
30
|
if (fallback !== void 0) return fallback;
|
|
15
31
|
return String(err);
|
|
16
32
|
}
|