@mulmoclaude/core 5.8.0 → 5.8.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (117) hide show
  1. package/dist/collection/firestore.cjs.map +1 -1
  2. package/dist/collection/firestore.js.map +1 -1
  3. package/dist/collection/index.cjs +3 -3
  4. package/dist/collection/index.cjs.map +1 -1
  5. package/dist/collection/index.js +3 -3
  6. package/dist/collection/registry/server/index.cjs +3 -3
  7. package/dist/collection/registry/server/index.cjs.map +1 -1
  8. package/dist/collection/registry/server/index.js +3 -3
  9. package/dist/collection/registry/server/index.js.map +1 -1
  10. package/dist/collection/server/index.cjs +2 -2
  11. package/dist/collection/server/index.js +2 -2
  12. package/dist/collection-watchers/index.cjs +4 -4
  13. package/dist/collection-watchers/index.cjs.map +1 -1
  14. package/dist/collection-watchers/index.js +4 -4
  15. package/dist/collection-watchers/index.js.map +1 -1
  16. package/dist/{discovery-XoYJI2Ot.js → discovery-CbwHtcwk.js} +3 -3
  17. package/dist/{discovery-XoYJI2Ot.js.map → discovery-CbwHtcwk.js.map} +1 -1
  18. package/dist/{discovery-CwBXvtyn.cjs → discovery-DgZRmO7F.cjs} +3 -3
  19. package/dist/discovery-DgZRmO7F.cjs.map +1 -0
  20. package/dist/{dist-Gj7ygaW2.cjs → dist-BZ62pKuU.cjs} +11 -1
  21. package/dist/dist-BZ62pKuU.cjs.map +1 -0
  22. package/dist/{dist-BzoA9pDR.js → dist-MFUV9FYo.js} +6 -2
  23. package/dist/dist-MFUV9FYo.js.map +1 -0
  24. package/dist/errorMessage-OEQzRwsi.cjs +1 -0
  25. package/dist/errorMessage-VEiXHRnX.js +1 -0
  26. package/dist/{errors-CdXsfFCF.cjs → errors-B0xloJ3E.cjs} +2 -2
  27. package/dist/errors-B0xloJ3E.cjs.map +1 -0
  28. package/dist/{errors-C7TI_t_4.js → errors-zTAsW7dS.js} +2 -2
  29. package/dist/{errors-C7TI_t_4.js.map → errors-zTAsW7dS.js.map} +1 -1
  30. package/dist/feeds/paths.cjs.map +1 -1
  31. package/dist/feeds/server/index.cjs +4 -4
  32. package/dist/feeds/server/index.cjs.map +1 -1
  33. package/dist/feeds/server/index.js +4 -4
  34. package/dist/feeds/server/index.js.map +1 -1
  35. package/dist/file-change/index.cjs.map +1 -1
  36. package/dist/files/index.cjs.map +1 -1
  37. package/dist/files/index.js.map +1 -1
  38. package/dist/global-config/index.cjs.map +1 -1
  39. package/dist/google/index.cjs +3 -3
  40. package/dist/google/index.cjs.map +1 -1
  41. package/dist/google/index.js +3 -3
  42. package/dist/google/index.js.map +1 -1
  43. package/dist/graph-CURotEkH.cjs.map +1 -1
  44. package/dist/ingestTypes-DS9HiR2I.cjs.map +1 -1
  45. package/dist/{listen-Dca-NoBi.js → listen-DUlzSOzX.js} +2 -2
  46. package/dist/{listen-Dca-NoBi.js.map → listen-DUlzSOzX.js.map} +1 -1
  47. package/dist/{listen-BXthb-9C.cjs → listen-N_8XdKz_.cjs} +2 -2
  48. package/dist/listen-N_8XdKz_.cjs.map +1 -0
  49. package/dist/notifier/index.cjs +1 -1
  50. package/dist/notifier/index.js +1 -1
  51. package/dist/{notifier-B65Mvngb.cjs → notifier-CiemUClt.cjs} +3 -6
  52. package/dist/notifier-CiemUClt.cjs.map +1 -0
  53. package/dist/{notifier-vAp5t6wL.js → notifier-dgkC-h80.js} +2 -5
  54. package/dist/notifier-dgkC-h80.js.map +1 -0
  55. package/dist/paths-C8-jxR0s.cjs.map +1 -1
  56. package/dist/plugin-vue/i18n.cjs.map +1 -1
  57. package/dist/plugin-vue/index.cjs +82 -16
  58. package/dist/plugin-vue/index.cjs.map +1 -1
  59. package/dist/plugin-vue/index.js +82 -16
  60. package/dist/plugin-vue/index.js.map +1 -1
  61. package/dist/{promptSafety-BGV1wrE6.cjs → promptSafety-auQEatax.cjs} +2 -2
  62. package/dist/promptSafety-auQEatax.cjs.map +1 -0
  63. package/dist/{promptSafety-B5uGroik.js → promptSafety-yG60Vof8.js} +2 -2
  64. package/dist/promptSafety-yG60Vof8.js.map +1 -0
  65. package/dist/relPath-CTAjGdCL.cjs.map +1 -1
  66. package/dist/remote-host/index.cjs +1 -1
  67. package/dist/remote-host/index.js +1 -1
  68. package/dist/remote-host/server/index.cjs +4 -4
  69. package/dist/remote-host/server/index.cjs.map +1 -1
  70. package/dist/remote-host/server/index.js +4 -4
  71. package/dist/remote-host/server/index.js.map +1 -1
  72. package/dist/{remote-host-DhAHBYzN.cjs → remote-host-BMjh_F-6.cjs} +2 -2
  73. package/dist/remote-host-BMjh_F-6.cjs.map +1 -0
  74. package/dist/{remote-host-Cjox2F2J.js → remote-host-CoNX9JEC.js} +2 -2
  75. package/dist/remote-host-CoNX9JEC.js.map +1 -0
  76. package/dist/remote-view/index.cjs +1 -1
  77. package/dist/remote-view/index.cjs.map +1 -1
  78. package/dist/remote-view/index.js +1 -1
  79. package/dist/root-BvwBUcol.cjs.map +1 -1
  80. package/dist/scheduler/index.cjs +2 -2
  81. package/dist/scheduler/index.cjs.map +1 -1
  82. package/dist/scheduler/index.js +2 -2
  83. package/dist/scheduler/index.js.map +1 -1
  84. package/dist/{server-CLhww8NL.cjs → server-RsnFqPt7.cjs} +5 -5
  85. package/dist/server-RsnFqPt7.cjs.map +1 -0
  86. package/dist/{server-CTkTHqEL.js → server-iYXYObKb.js} +5 -5
  87. package/dist/server-iYXYObKb.js.map +1 -0
  88. package/dist/skill-bridge/index.cjs.map +1 -1
  89. package/dist/utils/index.cjs +2 -2
  90. package/dist/utils/index.js +2 -2
  91. package/dist/whisper/index.cjs +2 -2
  92. package/dist/whisper/index.cjs.map +1 -1
  93. package/dist/whisper/index.js +2 -2
  94. package/dist/whisper/index.js.map +1 -1
  95. package/dist/wiki/index.cjs +1 -1
  96. package/dist/wiki/index.cjs.map +1 -1
  97. package/dist/wiki/index.js +1 -1
  98. package/dist/wiki/index.js.map +1 -1
  99. package/dist/wiki/paths.cjs.map +1 -1
  100. package/dist/wiki/server/index.cjs.map +1 -1
  101. package/dist/workspace-setup/index.js.map +1 -1
  102. package/package.json +6 -6
  103. package/dist/discovery-CwBXvtyn.cjs.map +0 -1
  104. package/dist/dist-BzoA9pDR.js.map +0 -1
  105. package/dist/dist-Gj7ygaW2.cjs.map +0 -1
  106. package/dist/errorMessage-BQNpSYdT.js +0 -1
  107. package/dist/errorMessage-CT-va_Pv.cjs +0 -1
  108. package/dist/errors-CdXsfFCF.cjs.map +0 -1
  109. package/dist/listen-BXthb-9C.cjs.map +0 -1
  110. package/dist/notifier-B65Mvngb.cjs.map +0 -1
  111. package/dist/notifier-vAp5t6wL.js.map +0 -1
  112. package/dist/promptSafety-B5uGroik.js.map +0 -1
  113. package/dist/promptSafety-BGV1wrE6.cjs.map +0 -1
  114. package/dist/remote-host-Cjox2F2J.js.map +0 -1
  115. package/dist/remote-host-DhAHBYzN.cjs.map +0 -1
  116. package/dist/server-CLhww8NL.cjs.map +0 -1
  117. package/dist/server-CTkTHqEL.js.map +0 -1
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","names":[],"sources":["../../src/files/json.ts","../../src/files/safe.ts","../../src/files/byPath.ts","../../src/files/htmlFileRequest.ts"],"sourcesContent":["import { writeFileAtomic, type WriteAtomicOptions } from \"./atomic.js\";\n\nconst JSON_INDENT = 2;\n\n/** Atomic JSON write (2-space indent) — serialize then write through the atomic\n * tmp-file + rename seam so a reader never sees a half-written document. */\nexport async function writeJsonAtomic(filePath: string, data: unknown, opts: WriteAtomicOptions = {}): Promise<void> {\n await writeFileAtomic(filePath, JSON.stringify(data, null, JSON_INDENT), opts);\n}\n","import { realpathSync } from \"node:fs\";\nimport path from \"node:path\";\n\n/** True for a `not found` filesystem error (ENOENT) — lets callers treat a\n * missing file as an empty/default result instead of a thrown error. */\nexport function isEnoent(err: unknown): boolean {\n return typeof err === \"object\" && err !== null && \"code\" in err && err.code === \"ENOENT\";\n}\n\n/** Realpath-based read-time containment: resolve `relPath` against the root's\n * realpath and require the target's realpath to stay inside it. Returns null\n * on ENOENT or traversal (symlink escapes included). `rootReal` MUST already\n * be a realpath. The one implementation for host and plugins (#2461) — this\n * security-critical primitive must not drift per consumer. */\nexport function resolveWithinRoot(rootReal: string, relPath: string): string | null {\n const normalized = path.normalize(relPath || \"\");\n const resolved = path.resolve(rootReal, normalized);\n const resolvedReal = realpathOrNull(resolved);\n if (resolvedReal === null) return null;\n if (resolvedReal !== rootReal && !resolvedReal.startsWith(rootReal + path.sep)) {\n return null;\n }\n return resolvedReal;\n}\n\nfunction realpathOrNull(absPath: string): string | null {\n try {\n return realpathSync(absPath);\n } catch {\n return null;\n }\n}\n","// Read / overwrite files the TOOL CALL named, rather than files the app minted.\n// `presentDocument(path)` / `presentHtml(path)` open a document that already\n// exists — a repo's `README.md`, `docs/report.html`, an absolute path — and the\n// user's edits in the view overwrite it in place.\n//\n// Shared by both hosts (MulmoClaude's `server/utils/files/by-path.ts` and\n// MulmoTerminal's backend bind their own workspace root to it), because the\n// judgement of what a `path` argument may reach is exactly the thing that must\n// not drift between them: a host that accepts what the other refuses turns one\n// tool call into two different behaviours.\n//\n// The rules:\n// - `classifyFilePath` (also what the plugins' `path` gate calls) decides the\n// shape: right extension, no NUL, no `.` / `..` / empty segment. That\n// lexical guard is what stops a vetted path from being re-pointed later.\n// - relative paths resolve against the injected root; absolute paths are taken\n// as given. There is deliberately NO containment check — opening a file\n// outside the workspace is the documented purpose of the `path` form. The\n// agent can already read and write those files directly; what is new is that\n// the view can too.\n// - a value that is absolute only under ANOTHER platform's rules is refused:\n// `classifyFilePath` recognises `C:\\proj\\x.md` everywhere (the value may\n// come from a remote host), but on POSIX `path.resolve(\"C:/proj/x.md\")`\n// lands under the process cwd — a file nobody named.\n// - reads and writes require a REGULAR FILE, judged through `realpath` so a\n// symlink is assessed by what it points at and a directory named `x.md`\n// cannot masquerade as a document.\n// - `write` overwrites only. Neither tool creates a file at a caller-supplied\n// path, so a write to a path that does not exist means the view is stale or\n// the path was wrong — refusing keeps a typo from scattering files.\n// - `readDir` and `unlink` are refused outright: this capability exists to\n// read and re-save ONE named document, not to browse or delete.\n\nimport { readFile, realpath, stat as fsStat } from \"node:fs/promises\";\nimport path from \"node:path\";\nimport { classifyFilePath } from \"../artifacts/paths.js\";\nimport { writeFileAtomic } from \"./atomic.js\";\n\nexport const MARKDOWN_EXTENSIONS = [\".md\"] as const;\nexport const HTML_EXTENSIONS = [\".html\", \".htm\"] as const;\n\n/** Minimal structural echo of gui-chat-protocol's `FileOps`. Declared here\n * rather than imported so core keeps no dependency on the protocol package;\n * a host assigns the result straight to a `FileOps`-typed capability. */\nexport interface ByPathFileOps {\n read: (rel: string) => Promise<string>;\n readBytes: (rel: string) => Promise<Uint8Array>;\n write: (rel: string, content: string | Uint8Array) => Promise<void>;\n readDir: (rel: string) => Promise<string[]>;\n stat: (rel: string) => Promise<{ mtimeMs: number; size: number }>;\n exists: (rel: string) => Promise<boolean>;\n unlink: (rel: string) => Promise<void>;\n}\n\n/** Absolute on-disk path for a caller-supplied path with one of `extensions`,\n * or null when the value is not usable on this platform. */\nexport function resolveByPath(root: string, value: string, extensions: readonly string[]): string | null {\n const kind = classifyFilePath(value, extensions);\n if (kind === null) return null;\n if (kind === \"relative\") return path.resolve(root, value);\n return path.isAbsolute(value) ? path.resolve(value) : null;\n}\n\n/** True when the path names an existing regular file. */\nexport async function existsAsFile(root: string, value: string, extensions: readonly string[]): Promise<boolean> {\n const absPath = resolveByPath(root, value, extensions);\n return absPath === null ? false : (await regularFileTarget(absPath)) !== null;\n}\n\nexport interface ByPathOptions {\n /** The root relative values resolve against, read PER CALL — hosts inject the\n * workspace after these ops are already bound into plugin closures. */\n rootFor: () => string;\n extensions: readonly string[];\n}\n\nfunction resolveOrThrow({ rootFor, extensions }: ByPathOptions, value: string): string {\n const absPath = resolveByPath(rootFor(), value, extensions);\n if (absPath === null) throw new Error(`invalid path: ${value}`);\n return absPath;\n}\n\n/** The canonical REGULAR FILE a path names, or null when it is missing, is a\n * directory, or is anything else (a FIFO would block a read forever).\n * Resolved through `realpath` so a symlink is judged — and later written — by\n * what it points at: `writeFileAtomic` renames a temp file into place, which\n * through a link would replace the link itself and leave the real document\n * untouched. */\nasync function regularFileTarget(absPath: string): Promise<string | null> {\n try {\n const target = await realpath(absPath);\n return (await fsStat(target)).isFile() ? target : null;\n } catch {\n return null;\n }\n}\n\n/** The file to read or overwrite. Throws rather than creating: neither tool\n * ever writes to a path that does not already hold a document. */\nasync function existingFileFor(options: ByPathOptions, rel: string): Promise<string> {\n const target = await regularFileTarget(resolveOrThrow(options, rel));\n if (target === null) throw new Error(`no file exists at ${rel}`);\n return target;\n}\n\n/** Browsing and deleting are NOT part of this capability: it exists so a view\n * can read and re-save the ONE document the tool call named, not to enumerate\n * a directory it was never pointed at or remove a file. */\nfunction unsupported(operation: string): () => Promise<never> {\n return () => Promise.reject(new Error(`byPath FileOps does not support ${operation}`));\n}\n\n/** FileOps over caller-supplied paths — what a host injects as `files.byPath`\n * for plugins whose `path` argument may leave their artifact directory. Every\n * method takes the same value the tool call carried, not a scope-relative one. */\nexport function createByPathFileOps(options: ByPathOptions): ByPathFileOps {\n return {\n read: async (rel) => readFile(await existingFileFor(options, rel), \"utf-8\"),\n readBytes: async (rel) => {\n const buf = await readFile(await existingFileFor(options, rel));\n return new Uint8Array(buf.buffer, buf.byteOffset, buf.byteLength);\n },\n write: async (rel, content) => writeFileAtomic(await existingFileFor(options, rel), content),\n readDir: unsupported(\"readDir\"),\n stat: async (rel) => {\n const { mtimeMs, size } = await fsStat(resolveOrThrow(options, rel));\n return { mtimeMs, size };\n },\n exists: (rel) => existsAsFile(options.rootFor(), rel, options.extensions),\n unlink: unsupported(\"unlink\"),\n };\n}\n","// Server half of the `/htmlfile` scheme (client half: `src/config/htmlFileUrl.ts`).\n// Turns a request path into the absolute file to serve, or null.\n//\n// Shared by both hosts so the URL the View asks for and the file a host resolves\n// cannot drift apart.\n//\n// This mount is deliberately NOT contained to a root: presentHtml's `path` form\n// may name any HTML page on disk, and serving it is the point. What still holds:\n//\n// - only `ws` (workspace-relative) and `abs` (absolute) scopes exist; anything\n// else 404s, so a bare `/htmlfile/etc/passwd` is not a path at all.\n// - no `.` / `..` / empty segment survives, so a vetted URL cannot be\n// re-pointed by traversal, and no dotfile segment is reachable (same policy\n// as `resolveArtifactRequestPath`, which the artifact mounts share).\n// - extension allowlist, realpath and regular-file checks stay with the host's\n// mount, which is also where the CSP header and rate limit are applied.\n//\n// The trust boundary is whatever the host applies to its `/artifacts/html`\n// mount — in MulmoClaude, the loopback-only listener plus `requireSameOrigin`\n// (bearer auth does not apply outside `/api`, and an iframe `src` request cannot\n// carry an Authorization header anyway).\n\nimport path from \"node:path\";\n// The scope constants are re-declared rather than imported: core must not\n// depend on a plugin (dependencies flow the other way). `html-plugin`'s\n// `htmlFileUrl` builds the URLs these parse, and the round-trip test in the host\n// is what keeps the two halves honest.\nconst HTML_FILE_SCOPE_WORKSPACE = \"ws\";\nconst HTML_FILE_SCOPE_ABSOLUTE = \"abs\";\n\nconst WINDOWS_DRIVE_ONLY_RE = /^[a-zA-Z]:$/;\n\nfunction decodeSegments(reqPath: string): string[] | null {\n let decoded: string[];\n try {\n decoded = reqPath\n .replace(/^\\//, \"\")\n .split(\"/\")\n .map((segment) => decodeURIComponent(segment));\n } catch {\n // Malformed escape (`%ZZ`) — fail closed rather than bubbling a URIError.\n return null;\n }\n // Segmentation happens on the ENCODED path, so a `%2F` (or `%5C`) becomes a\n // separator only after this decode — `a%2F..%2F..%2Ftmp%2Fx.html` would arrive\n // as one segment that the `..` check below never sees, and then split apart\n // inside `path.resolve`. A decoded segment that still contains a separator is\n // never legitimate here, so refuse it.\n return decoded.some((segment) => segment.includes(\"/\") || segment.includes(\"\\\\\")) ? null : decoded;\n}\n\n/** Absolute path for a `/htmlfile/<scope>/<segments…>` request, or null when\n * the URL is malformed, uses an unknown scope, or touches a `.` / `..` /\n * dotfile segment. `workspaceRoot` should already be a realpath. */\nexport function resolveHtmlFileRequestPath(workspaceRoot: string, reqPath: string): string | null {\n const decoded = decodeSegments(reqPath);\n if (decoded === null || decoded.length < 2) return null;\n const [scope, ...rest] = decoded;\n if (scope !== HTML_FILE_SCOPE_WORKSPACE && scope !== HTML_FILE_SCOPE_ABSOLUTE) return null;\n const [firstRest] = rest;\n if (firstRest === undefined) return null;\n if (rest.some((segment) => segment === \"\" || segment === \".\" || segment === \"..\" || segment.startsWith(\".\") || segment.includes(\"\\0\"))) {\n return null;\n }\n if (scope === HTML_FILE_SCOPE_WORKSPACE) return path.resolve(workspaceRoot, ...rest);\n // `abs/C:/proj/page.html` — the drive letter arrives as its own segment, so\n // rejoin it before resolving; everything else is rooted at `/`.\n const candidate = WINDOWS_DRIVE_ONLY_RE.test(firstRest) ? rest.join(\"/\") : `/${rest.join(\"/\")}`;\n // Only THIS platform's `path` can say whether that is really absolute: on\n // POSIX, `path.resolve(\"C:/proj/page.html\")` would land under the process cwd\n // rather than at any drive, silently serving the wrong file. Refuse instead.\n return path.isAbsolute(candidate) ? path.resolve(candidate) : null;\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"index.js","names":["fsStat"],"sources":["../../src/files/json.ts","../../src/files/safe.ts","../../src/files/byPath.ts","../../src/files/htmlFileRequest.ts"],"sourcesContent":["import { writeFileAtomic, type WriteAtomicOptions } from \"./atomic.js\";\n\nconst JSON_INDENT = 2;\n\n/** Atomic JSON write (2-space indent) — serialize then write through the atomic\n * tmp-file + rename seam so a reader never sees a half-written document. */\nexport async function writeJsonAtomic(filePath: string, data: unknown, opts: WriteAtomicOptions = {}): Promise<void> {\n await writeFileAtomic(filePath, JSON.stringify(data, null, JSON_INDENT), opts);\n}\n","import { realpathSync } from \"node:fs\";\nimport path from \"node:path\";\n\n/** True for a `not found` filesystem error (ENOENT) — lets callers treat a\n * missing file as an empty/default result instead of a thrown error. */\nexport function isEnoent(err: unknown): boolean {\n return typeof err === \"object\" && err !== null && \"code\" in err && err.code === \"ENOENT\";\n}\n\n/** Realpath-based read-time containment: resolve `relPath` against the root's\n * realpath and require the target's realpath to stay inside it. Returns null\n * on ENOENT or traversal (symlink escapes included). `rootReal` MUST already\n * be a realpath. The one implementation for host and plugins (#2461) — this\n * security-critical primitive must not drift per consumer. */\nexport function resolveWithinRoot(rootReal: string, relPath: string): string | null {\n const normalized = path.normalize(relPath || \"\");\n const resolved = path.resolve(rootReal, normalized);\n const resolvedReal = realpathOrNull(resolved);\n if (resolvedReal === null) return null;\n if (resolvedReal !== rootReal && !resolvedReal.startsWith(rootReal + path.sep)) {\n return null;\n }\n return resolvedReal;\n}\n\nfunction realpathOrNull(absPath: string): string | null {\n try {\n return realpathSync(absPath);\n } catch {\n return null;\n }\n}\n","// Read / overwrite files the TOOL CALL named, rather than files the app minted.\n// `presentDocument(path)` / `presentHtml(path)` open a document that already\n// exists — a repo's `README.md`, `docs/report.html`, an absolute path — and the\n// user's edits in the view overwrite it in place.\n//\n// Shared by both hosts (MulmoClaude's `server/utils/files/by-path.ts` and\n// MulmoTerminal's backend bind their own workspace root to it), because the\n// judgement of what a `path` argument may reach is exactly the thing that must\n// not drift between them: a host that accepts what the other refuses turns one\n// tool call into two different behaviours.\n//\n// The rules:\n// - `classifyFilePath` (also what the plugins' `path` gate calls) decides the\n// shape: right extension, no NUL, no `.` / `..` / empty segment. That\n// lexical guard is what stops a vetted path from being re-pointed later.\n// - relative paths resolve against the injected root; absolute paths are taken\n// as given. There is deliberately NO containment check — opening a file\n// outside the workspace is the documented purpose of the `path` form. The\n// agent can already read and write those files directly; what is new is that\n// the view can too.\n// - a value that is absolute only under ANOTHER platform's rules is refused:\n// `classifyFilePath` recognises `C:\\proj\\x.md` everywhere (the value may\n// come from a remote host), but on POSIX `path.resolve(\"C:/proj/x.md\")`\n// lands under the process cwd — a file nobody named.\n// - reads and writes require a REGULAR FILE, judged through `realpath` so a\n// symlink is assessed by what it points at and a directory named `x.md`\n// cannot masquerade as a document.\n// - `write` overwrites only. Neither tool creates a file at a caller-supplied\n// path, so a write to a path that does not exist means the view is stale or\n// the path was wrong — refusing keeps a typo from scattering files.\n// - `readDir` and `unlink` are refused outright: this capability exists to\n// read and re-save ONE named document, not to browse or delete.\n\nimport { readFile, realpath, stat as fsStat } from \"node:fs/promises\";\nimport path from \"node:path\";\nimport { classifyFilePath } from \"../artifacts/paths.js\";\nimport { writeFileAtomic } from \"./atomic.js\";\n\nexport const MARKDOWN_EXTENSIONS = [\".md\"] as const;\nexport const HTML_EXTENSIONS = [\".html\", \".htm\"] as const;\n\n/** Minimal structural echo of gui-chat-protocol's `FileOps`. Declared here\n * rather than imported so core keeps no dependency on the protocol package;\n * a host assigns the result straight to a `FileOps`-typed capability. */\nexport interface ByPathFileOps {\n read: (rel: string) => Promise<string>;\n readBytes: (rel: string) => Promise<Uint8Array>;\n write: (rel: string, content: string | Uint8Array) => Promise<void>;\n readDir: (rel: string) => Promise<string[]>;\n stat: (rel: string) => Promise<{ mtimeMs: number; size: number }>;\n exists: (rel: string) => Promise<boolean>;\n unlink: (rel: string) => Promise<void>;\n}\n\n/** Absolute on-disk path for a caller-supplied path with one of `extensions`,\n * or null when the value is not usable on this platform. */\nexport function resolveByPath(root: string, value: string, extensions: readonly string[]): string | null {\n const kind = classifyFilePath(value, extensions);\n if (kind === null) return null;\n if (kind === \"relative\") return path.resolve(root, value);\n return path.isAbsolute(value) ? path.resolve(value) : null;\n}\n\n/** True when the path names an existing regular file. */\nexport async function existsAsFile(root: string, value: string, extensions: readonly string[]): Promise<boolean> {\n const absPath = resolveByPath(root, value, extensions);\n return absPath === null ? false : (await regularFileTarget(absPath)) !== null;\n}\n\nexport interface ByPathOptions {\n /** The root relative values resolve against, read PER CALL — hosts inject the\n * workspace after these ops are already bound into plugin closures. */\n rootFor: () => string;\n extensions: readonly string[];\n}\n\nfunction resolveOrThrow({ rootFor, extensions }: ByPathOptions, value: string): string {\n const absPath = resolveByPath(rootFor(), value, extensions);\n if (absPath === null) throw new Error(`invalid path: ${value}`);\n return absPath;\n}\n\n/** The canonical REGULAR FILE a path names, or null when it is missing, is a\n * directory, or is anything else (a FIFO would block a read forever).\n * Resolved through `realpath` so a symlink is judged — and later written — by\n * what it points at: `writeFileAtomic` renames a temp file into place, which\n * through a link would replace the link itself and leave the real document\n * untouched. */\nasync function regularFileTarget(absPath: string): Promise<string | null> {\n try {\n const target = await realpath(absPath);\n return (await fsStat(target)).isFile() ? target : null;\n } catch {\n return null;\n }\n}\n\n/** The file to read or overwrite. Throws rather than creating: neither tool\n * ever writes to a path that does not already hold a document. */\nasync function existingFileFor(options: ByPathOptions, rel: string): Promise<string> {\n const target = await regularFileTarget(resolveOrThrow(options, rel));\n if (target === null) throw new Error(`no file exists at ${rel}`);\n return target;\n}\n\n/** Browsing and deleting are NOT part of this capability: it exists so a view\n * can read and re-save the ONE document the tool call named, not to enumerate\n * a directory it was never pointed at or remove a file. */\nfunction unsupported(operation: string): () => Promise<never> {\n return () => Promise.reject(new Error(`byPath FileOps does not support ${operation}`));\n}\n\n/** FileOps over caller-supplied paths — what a host injects as `files.byPath`\n * for plugins whose `path` argument may leave their artifact directory. Every\n * method takes the same value the tool call carried, not a scope-relative one. */\nexport function createByPathFileOps(options: ByPathOptions): ByPathFileOps {\n return {\n read: async (rel) => readFile(await existingFileFor(options, rel), \"utf-8\"),\n readBytes: async (rel) => {\n const buf = await readFile(await existingFileFor(options, rel));\n return new Uint8Array(buf.buffer, buf.byteOffset, buf.byteLength);\n },\n write: async (rel, content) => writeFileAtomic(await existingFileFor(options, rel), content),\n readDir: unsupported(\"readDir\"),\n stat: async (rel) => {\n const { mtimeMs, size } = await fsStat(resolveOrThrow(options, rel));\n return { mtimeMs, size };\n },\n exists: (rel) => existsAsFile(options.rootFor(), rel, options.extensions),\n unlink: unsupported(\"unlink\"),\n };\n}\n","// Server half of the `/htmlfile` scheme (client half: `src/config/htmlFileUrl.ts`).\n// Turns a request path into the absolute file to serve, or null.\n//\n// Shared by both hosts so the URL the View asks for and the file a host resolves\n// cannot drift apart.\n//\n// This mount is deliberately NOT contained to a root: presentHtml's `path` form\n// may name any HTML page on disk, and serving it is the point. What still holds:\n//\n// - only `ws` (workspace-relative) and `abs` (absolute) scopes exist; anything\n// else 404s, so a bare `/htmlfile/etc/passwd` is not a path at all.\n// - no `.` / `..` / empty segment survives, so a vetted URL cannot be\n// re-pointed by traversal, and no dotfile segment is reachable (same policy\n// as `resolveArtifactRequestPath`, which the artifact mounts share).\n// - extension allowlist, realpath and regular-file checks stay with the host's\n// mount, which is also where the CSP header and rate limit are applied.\n//\n// The trust boundary is whatever the host applies to its `/artifacts/html`\n// mount — in MulmoClaude, the loopback-only listener plus `requireSameOrigin`\n// (bearer auth does not apply outside `/api`, and an iframe `src` request cannot\n// carry an Authorization header anyway).\n\nimport path from \"node:path\";\n// The scope constants are re-declared rather than imported: core must not\n// depend on a plugin (dependencies flow the other way). `html-plugin`'s\n// `htmlFileUrl` builds the URLs these parse, and the round-trip test in the host\n// is what keeps the two halves honest.\nconst HTML_FILE_SCOPE_WORKSPACE = \"ws\";\nconst HTML_FILE_SCOPE_ABSOLUTE = \"abs\";\n\nconst WINDOWS_DRIVE_ONLY_RE = /^[a-zA-Z]:$/;\n\nfunction decodeSegments(reqPath: string): string[] | null {\n let decoded: string[];\n try {\n decoded = reqPath\n .replace(/^\\//, \"\")\n .split(\"/\")\n .map((segment) => decodeURIComponent(segment));\n } catch {\n // Malformed escape (`%ZZ`) — fail closed rather than bubbling a URIError.\n return null;\n }\n // Segmentation happens on the ENCODED path, so a `%2F` (or `%5C`) becomes a\n // separator only after this decode — `a%2F..%2F..%2Ftmp%2Fx.html` would arrive\n // as one segment that the `..` check below never sees, and then split apart\n // inside `path.resolve`. A decoded segment that still contains a separator is\n // never legitimate here, so refuse it.\n return decoded.some((segment) => segment.includes(\"/\") || segment.includes(\"\\\\\")) ? null : decoded;\n}\n\n/** Absolute path for a `/htmlfile/<scope>/<segments…>` request, or null when\n * the URL is malformed, uses an unknown scope, or touches a `.` / `..` /\n * dotfile segment. `workspaceRoot` should already be a realpath. */\nexport function resolveHtmlFileRequestPath(workspaceRoot: string, reqPath: string): string | null {\n const decoded = decodeSegments(reqPath);\n if (decoded === null || decoded.length < 2) return null;\n const [scope, ...rest] = decoded;\n if (scope !== HTML_FILE_SCOPE_WORKSPACE && scope !== HTML_FILE_SCOPE_ABSOLUTE) return null;\n const [firstRest] = rest;\n if (firstRest === undefined) return null;\n if (rest.some((segment) => segment === \"\" || segment === \".\" || segment === \"..\" || segment.startsWith(\".\") || segment.includes(\"\\0\"))) {\n return null;\n }\n if (scope === HTML_FILE_SCOPE_WORKSPACE) return path.resolve(workspaceRoot, ...rest);\n // `abs/C:/proj/page.html` — the drive letter arrives as its own segment, so\n // rejoin it before resolving; everything else is rooted at `/`.\n const candidate = WINDOWS_DRIVE_ONLY_RE.test(firstRest) ? rest.join(\"/\") : `/${rest.join(\"/\")}`;\n // Only THIS platform's `path` can say whether that is really absolute: on\n // POSIX, `path.resolve(\"C:/proj/page.html\")` would land under the process cwd\n // rather than at any drive, silently serving the wrong file. Refuse instead.\n return path.isAbsolute(candidate) ? path.resolve(candidate) : null;\n}\n"],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"index.cjs","names":[],"sources":["../../src/global-config/schema.ts","../../src/global-config/index.ts"],"sourcesContent":["// Pure readers over the parsed `~/.config/mulmo/config.json` document. No IO\n// here so the validation rules are unit-testable without a filesystem, and so a\n// host that already has the document in memory can reuse them.\n//\n// Every reader is TOLERANT by design: this file is hand-edited (there is no UI\n// for it yet), and a typo in one key must not take out the app or the other\n// keys. An unusable value reads as \"not configured\" and the caller applies its\n// own default.\n\n/** A user-supplied regular expression source is compiled and then run against\n * whole documents on every keystroke. Length is the cheap, honest guard: it\n * bounds how baroque — and so how catastrophically backtracking — the pattern\n * can be, without pretending to be a ReDoS analyser. The file is machine-local\n * and hand-written, so the threat model is \"the user's own typo\", not attack. */\nexport const MAX_BOOKMARK_PATTERN_LENGTH = 200;\n\nconst isRecord = (value: unknown): value is Record<string, unknown> => typeof value === \"object\" && value !== null && !Array.isArray(value);\n\n/** True when `source` compiles as a regular expression. `new RegExp` is the\n * only real validator — there is no cheaper way to reject `\"^([a-z\"`. */\nfunction compiles(source: string): boolean {\n try {\n return RegExp(source) instanceof RegExp;\n } catch {\n return false;\n }\n}\n\n/**\n * `documentBookmarks.pattern` — the regex whose matches the markdown source\n * editor marks in its left-edge rail. Returns the raw SOURCE string (not a\n * compiled RegExp): the value crosses a JSON dispatch hop to the browser, which\n * compiles it there.\n *\n * Returns null — meaning \"not configured, use your default\" — for a missing\n * file, a missing key, a non-string, an empty string, an over-long string, or\n * one that does not compile.\n */\nexport function readDocumentBookmarkPattern(config: unknown): string | null {\n if (!isRecord(config)) return null;\n const { documentBookmarks } = config;\n if (!isRecord(documentBookmarks)) return null;\n const { pattern } = documentBookmarks;\n if (typeof pattern !== \"string\") return null;\n if (pattern.length === 0 || pattern.length > MAX_BOOKMARK_PATTERN_LENGTH) return null;\n return compiles(pattern) ? pattern : null;\n}\n","// Entry point for `@mulmoclaude/core/global-config` — the host-neutral\n// per-user config file (`~/.config/mulmo/config.json`) shared by MulmoClaude\n// and MulmoTerminal. Server-only (node:fs); the values it yields are plain\n// JSON, so they cross a dispatch hop to a plugin View unchanged.\nimport { readFile } from \"node:fs/promises\";\nimport { mulmoGlobalConfigPath } from \"./paths.js\";\nimport { readDocumentBookmarkPattern } from \"./schema.js\";\n\nexport { mulmoConfigDir, mulmoGlobalConfigPath } from \"./paths.js\";\nexport { readDocumentBookmarkPattern, MAX_BOOKMARK_PATTERN_LENGTH } from \"./schema.js\";\n\n/**\n * Parse `~/.config/mulmo/config.json`. Returns `null` when the file is absent\n * or unparseable — both are ordinary states (there is no UI that creates it),\n * and every reader in `./schema` treats `null` as \"nothing configured\".\n *\n * Not cached: the file is read at most once per view open, and caching would\n * mean a hand-edit needs an app restart to take effect.\n */\nexport async function readMulmoGlobalConfig(home?: string): Promise<unknown> {\n try {\n return JSON.parse(await readFile(mulmoGlobalConfigPath(home), \"utf-8\"));\n } catch {\n return null;\n }\n}\n\n/** Convenience: the configured document-bookmark regex source, or null. */\nexport async function loadDocumentBookmarkPattern(home?: string): Promise<string | null> {\n return readDocumentBookmarkPattern(await readMulmoGlobalConfig(home));\n}\n"],"mappings":";;;;;;;;;AAcA,IAAa,8BAA8B;AAE3C,IAAM,YAAY,UAAqD,OAAO,UAAU,YAAY,UAAU,QAAQ,CAAC,MAAM,QAAQ,KAAK;;;AAI1I,SAAS,SAAS,QAAyB;CACzC,IAAI;EACF,OAAO,OAAO,MAAM,aAAa;CACnC,QAAQ;EACN,OAAO;CACT;AACF;;;;;;;;;;;AAYA,SAAgB,4BAA4B,QAAgC;CAC1E,IAAI,CAAC,SAAS,MAAM,GAAG,OAAO;CAC9B,MAAM,EAAE,sBAAsB;CAC9B,IAAI,CAAC,SAAS,iBAAiB,GAAG,OAAO;CACzC,MAAM,EAAE,YAAY;CACpB,IAAI,OAAO,YAAY,UAAU,OAAO;CACxC,IAAI,QAAQ,WAAW,KAAK,QAAQ,SAAA,KAAsC,OAAO;CACjF,OAAO,SAAS,OAAO,IAAI,UAAU;AACvC;;;;;;;;;;;AC3BA,eAAsB,sBAAsB,MAAiC;CAC3E,IAAI;EACF,OAAO,KAAK,MAAM,OAAA,GAAM,iBAAA,SAAA,CAAS,cAAA,sBAAsB,IAAI,GAAG,OAAO,CAAC;CACxE,QAAQ;EACN,OAAO;CACT;AACF;;AAGA,eAAsB,4BAA4B,MAAuC;CACvF,OAAO,4BAA4B,MAAM,sBAAsB,IAAI,CAAC;AACtE"}
1
+ {"version":3,"file":"index.cjs","names":["readFile","mulmoGlobalConfigPath"],"sources":["../../src/global-config/schema.ts","../../src/global-config/index.ts"],"sourcesContent":["// Pure readers over the parsed `~/.config/mulmo/config.json` document. No IO\n// here so the validation rules are unit-testable without a filesystem, and so a\n// host that already has the document in memory can reuse them.\n//\n// Every reader is TOLERANT by design: this file is hand-edited (there is no UI\n// for it yet), and a typo in one key must not take out the app or the other\n// keys. An unusable value reads as \"not configured\" and the caller applies its\n// own default.\n\n/** A user-supplied regular expression source is compiled and then run against\n * whole documents on every keystroke. Length is the cheap, honest guard: it\n * bounds how baroque — and so how catastrophically backtracking — the pattern\n * can be, without pretending to be a ReDoS analyser. The file is machine-local\n * and hand-written, so the threat model is \"the user's own typo\", not attack. */\nexport const MAX_BOOKMARK_PATTERN_LENGTH = 200;\n\nconst isRecord = (value: unknown): value is Record<string, unknown> => typeof value === \"object\" && value !== null && !Array.isArray(value);\n\n/** True when `source` compiles as a regular expression. `new RegExp` is the\n * only real validator — there is no cheaper way to reject `\"^([a-z\"`. */\nfunction compiles(source: string): boolean {\n try {\n return RegExp(source) instanceof RegExp;\n } catch {\n return false;\n }\n}\n\n/**\n * `documentBookmarks.pattern` — the regex whose matches the markdown source\n * editor marks in its left-edge rail. Returns the raw SOURCE string (not a\n * compiled RegExp): the value crosses a JSON dispatch hop to the browser, which\n * compiles it there.\n *\n * Returns null — meaning \"not configured, use your default\" — for a missing\n * file, a missing key, a non-string, an empty string, an over-long string, or\n * one that does not compile.\n */\nexport function readDocumentBookmarkPattern(config: unknown): string | null {\n if (!isRecord(config)) return null;\n const { documentBookmarks } = config;\n if (!isRecord(documentBookmarks)) return null;\n const { pattern } = documentBookmarks;\n if (typeof pattern !== \"string\") return null;\n if (pattern.length === 0 || pattern.length > MAX_BOOKMARK_PATTERN_LENGTH) return null;\n return compiles(pattern) ? pattern : null;\n}\n","// Entry point for `@mulmoclaude/core/global-config` — the host-neutral\n// per-user config file (`~/.config/mulmo/config.json`) shared by MulmoClaude\n// and MulmoTerminal. Server-only (node:fs); the values it yields are plain\n// JSON, so they cross a dispatch hop to a plugin View unchanged.\nimport { readFile } from \"node:fs/promises\";\nimport { mulmoGlobalConfigPath } from \"./paths.js\";\nimport { readDocumentBookmarkPattern } from \"./schema.js\";\n\nexport { mulmoConfigDir, mulmoGlobalConfigPath } from \"./paths.js\";\nexport { readDocumentBookmarkPattern, MAX_BOOKMARK_PATTERN_LENGTH } from \"./schema.js\";\n\n/**\n * Parse `~/.config/mulmo/config.json`. Returns `null` when the file is absent\n * or unparseable — both are ordinary states (there is no UI that creates it),\n * and every reader in `./schema` treats `null` as \"nothing configured\".\n *\n * Not cached: the file is read at most once per view open, and caching would\n * mean a hand-edit needs an app restart to take effect.\n */\nexport async function readMulmoGlobalConfig(home?: string): Promise<unknown> {\n try {\n return JSON.parse(await readFile(mulmoGlobalConfigPath(home), \"utf-8\"));\n } catch {\n return null;\n }\n}\n\n/** Convenience: the configured document-bookmark regex source, or null. */\nexport async function loadDocumentBookmarkPattern(home?: string): Promise<string | null> {\n return readDocumentBookmarkPattern(await readMulmoGlobalConfig(home));\n}\n"],"mappings":";;;;;;;;;AAcA,IAAa,8BAA8B;AAE3C,IAAM,YAAY,UAAqD,OAAO,UAAU,YAAY,UAAU,QAAQ,CAAC,MAAM,QAAQ,KAAK;;;AAI1I,SAAS,SAAS,QAAyB;CACzC,IAAI;EACF,OAAO,OAAO,MAAM,aAAa;CACnC,QAAQ;EACN,OAAO;CACT;AACF;;;;;;;;;;;AAYA,SAAgB,4BAA4B,QAAgC;CAC1E,IAAI,CAAC,SAAS,MAAM,GAAG,OAAO;CAC9B,MAAM,EAAE,sBAAsB;CAC9B,IAAI,CAAC,SAAS,iBAAiB,GAAG,OAAO;CACzC,MAAM,EAAE,YAAY;CACpB,IAAI,OAAO,YAAY,UAAU,OAAO;CACxC,IAAI,QAAQ,WAAW,KAAK,QAAQ,SAAA,KAAsC,OAAO;CACjF,OAAO,SAAS,OAAO,IAAI,UAAU;AACvC;;;;;;;;;;;AC3BA,eAAsB,sBAAsB,MAAiC;CAC3E,IAAI;EACF,OAAO,KAAK,MAAM,OAAA,GAAMA,iBAAAA,SAAAA,CAASC,cAAAA,sBAAsB,IAAI,GAAG,OAAO,CAAC;CACxE,QAAQ;EACN,OAAO;CACT;AACF;;AAGA,eAAsB,4BAA4B,MAAuC;CACvF,OAAO,4BAA4B,MAAM,sBAAsB,IAAI,CAAC;AACtE"}
@@ -1,13 +1,13 @@
1
1
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
2
  const require_rolldown_runtime = require("../rolldown-runtime-VH7oDXx4.cjs");
3
- const require_dist = require("../dist-Gj7ygaW2.cjs");
4
- require("../errors-CdXsfFCF.cjs");
3
+ const require_dist = require("../dist-BZ62pKuU.cjs");
4
+ require("../errors-B0xloJ3E.cjs");
5
5
  const require_text = require("../text-CaAh9ZSO.cjs");
6
6
  const require_root = require("../root-BvwBUcol.cjs");
7
7
  const require_utils_fetch = require("../utils/fetch.cjs");
8
8
  const require_itemId = require("../itemId-CVrNibwT.cjs");
9
9
  const require_project = require("../project-C1ep9pvo.cjs");
10
- const require_discovery = require("../discovery-CwBXvtyn.cjs");
10
+ const require_discovery = require("../discovery-DgZRmO7F.cjs");
11
11
  const require_paths = require("../paths-C8-jxR0s.cjs");
12
12
  let node_fs = require("node:fs");
13
13
  let node_path = require("node:path");