@mulmoclaude/core 0.2.14 → 0.2.16

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 (50) hide show
  1. package/dist/collection/registry/guards.d.ts +2 -0
  2. package/dist/collection/registry/index.cjs +4 -0
  3. package/dist/collection/registry/index.d.ts +2 -0
  4. package/dist/collection/registry/index.js +2 -0
  5. package/dist/collection/registry/registryIndex.d.ts +48 -0
  6. package/dist/collection/registry/server/client.d.ts +60 -0
  7. package/dist/collection/registry/server/collectionFiles.d.ts +51 -0
  8. package/dist/collection/registry/server/exportCollection.d.ts +29 -0
  9. package/dist/collection/registry/server/fetch.d.ts +8 -0
  10. package/dist/collection/registry/server/importCollection.d.ts +35 -0
  11. package/dist/collection/registry/server/importWriter.d.ts +30 -0
  12. package/dist/collection/registry/server/index.cjs +1110 -0
  13. package/dist/collection/registry/server/index.cjs.map +1 -0
  14. package/dist/collection/registry/server/index.d.ts +28 -0
  15. package/dist/collection/registry/server/index.js +1076 -0
  16. package/dist/collection/registry/server/index.js.map +1 -0
  17. package/dist/collection/registry/server/performExport.d.ts +6 -0
  18. package/dist/collection/registry/server/registriesConfig.d.ts +11 -0
  19. package/dist/collection/registry/server/skillDescription.d.ts +4 -0
  20. package/dist/collection/registry/types.d.ts +37 -0
  21. package/dist/collection/server/host.d.ts +7 -0
  22. package/dist/collection/server/index.cjs +1 -1
  23. package/dist/collection/server/index.js +1 -1
  24. package/dist/collection/server/util.d.ts +1 -0
  25. package/dist/collection-watchers/index.cjs +1 -1
  26. package/dist/collection-watchers/index.js +1 -1
  27. package/dist/feeds/server/index.cjs +1 -1
  28. package/dist/feeds/server/index.cjs.map +1 -1
  29. package/dist/feeds/server/index.js +1 -1
  30. package/dist/feeds/server/index.js.map +1 -1
  31. package/dist/notifier-ChpY0XrY.js.map +1 -1
  32. package/dist/notifier-bS8IEeLA.cjs.map +1 -1
  33. package/dist/{server-CRlmOBVQ.js → server-DRoqc8dL.js} +8 -2
  34. package/dist/server-DRoqc8dL.js.map +1 -0
  35. package/dist/{server-BjXvqGrE.cjs → server-DoDXibCq.cjs} +31 -1
  36. package/dist/server-DoDXibCq.cjs.map +1 -0
  37. package/dist/translation/client.cjs +45 -0
  38. package/dist/translation/client.cjs.map +1 -0
  39. package/dist/translation/client.d.ts +32 -0
  40. package/dist/translation/client.js +44 -0
  41. package/dist/translation/client.js.map +1 -0
  42. package/dist/types-BKVZrtyc.js +123 -0
  43. package/dist/types-BKVZrtyc.js.map +1 -0
  44. package/dist/types-CNqkLT4M.cjs +140 -0
  45. package/dist/types-CNqkLT4M.cjs.map +1 -0
  46. package/dist/whisper/client.cjs.map +1 -1
  47. package/dist/whisper/client.js.map +1 -1
  48. package/package.json +20 -2
  49. package/dist/server-BjXvqGrE.cjs.map +0 -1
  50. package/dist/server-CRlmOBVQ.js.map +0 -1
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","names":[],"sources":["../../../src/feeds/server/host.ts","../../../src/feeds/server/registry.ts","../../../src/feeds/server/retrievers/index.ts","../../../src/feeds/server/fetch/httpClient.ts","../../../src/feeds/server/fetch/rssParser.ts","../../../src/feeds/server/pathResolver.ts","../../../src/feeds/server/projectItem.ts","../../../src/feeds/server/retrievers/rss.ts","../../../src/feeds/server/retrievers/httpJson.ts","../../../src/feeds/server/state.ts","../../../src/feeds/server/agentIngest.ts","../../../src/feeds/server/engine.ts","../../../src/feeds/server/scheduledRefresh.ts"],"sourcesContent":["// Host injection seam for the Feeds engine — mirrors `configureCollectionHost`\n// / `configureScheduler`. The engine is host-agnostic: the workspace root, the\n// logger, an atomic file writer, and the hidden/visible agent-ingest worker\n// launcher are injected once at boot via `configureFeedsHost`. Everything else\n// the engine needs (collection IO, the notifier) is a sibling `@mulmoclaude/core`\n// subpath, imported directly. Both MulmoClaude and MulmoTerminal supply their\n// own host shim.\n\n/** Outcome of launching one hidden/visible agent-ingest worker. `chatId` lets\n * the caller register a completion hook so a failed refresh doesn't die\n * silently. */\nexport type AgentWorkerResult = { ok: true; chatId: string } | { ok: false; error: string };\n\n/** Launches a worker chat. Injected at boot to keep the feeds engine from\n * importing a host's routes/session layer. `hidden` chooses an invisible\n * system worker (scheduled refresh) vs a visible session the user can watch\n * (manual Refresh — debuggable). `onComplete` is a one-shot completion hook\n * (only honoured for hidden workers) so the dispatcher learns success/failure.\n * Returns `ok:false` on the concurrency-cap miss or a launch error — the caller\n * leaves state untouched and retries next tick. */\nexport type AgentWorkerRunner = (args: {\n message: string;\n roleId: string;\n hidden: boolean;\n onComplete?: (outcome: { didError: boolean }) => void | Promise<void>;\n}) => Promise<AgentWorkerResult>;\n\n/** Structured logger, `(prefix, msg, data?)` — same shape as `CollectionLogger`. */\nexport interface FeedsLogger {\n error: (prefix: string, message: string, data?: Record<string, unknown>) => void;\n warn: (prefix: string, message: string, data?: Record<string, unknown>) => void;\n info: (prefix: string, message: string, data?: Record<string, unknown>) => void;\n debug: (prefix: string, message: string, data?: Record<string, unknown>) => void;\n}\n\nexport interface FeedsHost {\n /** Absolute workspace root — the default for `refreshDue()` and state paths. */\n workspaceRoot: string;\n /** Host logger. */\n log: FeedsLogger;\n /** Host atomic file writer (state files). */\n writeFileAtomic: (filePath: string, content: string) => Promise<void>;\n /** Launches the agent-ingest worker (was `setAgentWorkerRunner`). */\n spawnWorker: AgentWorkerRunner;\n}\n\nlet current: FeedsHost | null = null;\n\n/** Wire the feeds engine to a host. Call once at startup, before any refresh. */\nexport function configureFeedsHost(host: FeedsHost): void {\n if (current && current !== host) {\n throw new Error(\"@mulmoclaude/core/feeds: configureFeedsHost() was already called with a different host\");\n }\n current = host;\n}\n\n/** The configured host, or throw if `configureFeedsHost` was never called. */\nexport function requireFeedsHost(): FeedsHost {\n if (!current) throw new Error(\"@mulmoclaude/core/feeds: configureFeedsHost() was not called by the host\");\n return current;\n}\n\n/** Test-only: clear the configured host. */\nexport function resetFeedsHostForTesting(): void {\n current = null;\n}\n\n/** Forwarding logger so engine modules can `import { log }` without each\n * reaching for `requireFeedsHost().log`. */\nexport const log: FeedsLogger = {\n error: (prefix, msg, data) => requireFeedsHost().log.error(prefix, msg, data),\n warn: (prefix, msg, data) => requireFeedsHost().log.warn(prefix, msg, data),\n info: (prefix, msg, data) => requireFeedsHost().log.info(prefix, msg, data),\n debug: (prefix, msg, data) => requireFeedsHost().log.debug(prefix, msg, data),\n};\n","// List the registered data-source feeds. Feeds are CREATED / REMOVED by\n// the agent writing / deleting `feeds/<slug>/schema.json` directly (see\n// config/helps/feeds.md) — the host only discovers + retrieves them.\n// icon / dataPath defaults for agent-authored feed schemas are applied in\n// `collection/server/discovery.ts` (source === \"feed\").\n\nimport { rm } from \"node:fs/promises\";\nimport { discoverCollections, resolveDataDir, safeSlugName, type LoadedCollection } from \"../../collection/server/index.js\";\nimport { log, requireFeedsHost } from \"./host.js\";\nimport { feedDir } from \"../paths.js\";\n\n/** Every registered feed, as a discovered collection (carrying its\n * validated schema, `ingest`, and resolved `dataDir`). */\nexport async function listFeeds(workspaceRoot: string = requireFeedsHost().workspaceRoot): Promise<LoadedCollection[]> {\n const all = await discoverCollections({ workspaceRoot });\n return all.filter((collection) => collection.source === \"feed\");\n}\n\n/** Delete a feed entirely: its records AND its `feeds/<slug>/` directory\n * (schema + state). Idempotent. Host-side only (backs the UI delete\n * button); the agent removes a feed by deleting both directories itself.\n *\n * The records dir is derived from the SLUG (`data/feeds/<slug>`), never\n * from the schema's `dataPath` — feeds are forced into that namespace at\n * discovery, so a malformed/hostile `dataPath` can't redirect this delete\n * at another app's data (e.g. `data/wiki`). `resolveDataDir` also rejects\n * any path that escapes the workspace. */\nexport async function removeFeed(workspaceRoot: string, slug: string): Promise<boolean> {\n const safe = safeSlugName(slug);\n if (safe === null) return false;\n const recordsDir = resolveDataDir(`data/feeds/${safe}`, workspaceRoot);\n try {\n if (recordsDir) await rm(recordsDir, { recursive: true, force: true });\n await rm(feedDir(safe, workspaceRoot), { recursive: true, force: true });\n log.info(\"feeds\", \"feed + records removed\", { slug: safe });\n return true;\n } catch (error) {\n log.warn(\"feeds\", \"feed remove failed\", { slug: safe, error: String(error) });\n return false;\n }\n}\n","// Pluggable retriever registry. Each declarative `ingest.kind` maps to one\n// RetrieveFn that fetches the endpoint and returns projected records.\n// Side-effect registration keeps the engine decoupled from the kinds.\n// The `agent` kind is NOT a retriever (it dispatches a hidden worker before\n// the engine consults this registry); a future `code` kind would register here.\n\nimport type { CollectionItem, CollectionSchema } from \"../../../collection/index.js\";\nimport type { DeclarativeIngestSpec } from \"../../ingestTypes.js\";\nimport type { FeedState } from \"../state.js\";\n\nexport interface RetrieveResult {\n /** Projected records, keyed by primaryKey (the engine upserts them). */\n items: CollectionItem[];\n /** Updated retriever cursor to persist (incremental fetches). */\n cursor: Record<string, string>;\n}\n\n// Declarative-only: the engine branches `agent` ingest off BEFORE looking up a\n// retriever, so a RetrieveFn never sees a non-fetch spec (and rss/http-json can\n// read `ingest.url`/`map` without union narrowing).\nexport type RetrieveFn = (ingest: DeclarativeIngestSpec, schema: CollectionSchema, state: FeedState) => Promise<RetrieveResult>;\n\nconst registry = new Map<string, RetrieveFn>();\n\nexport function registerRetriever(kind: string, retriever: RetrieveFn): void {\n registry.set(kind, retriever);\n}\n\nexport function getRetriever(kind: string): RetrieveFn | undefined {\n return registry.get(kind);\n}\n","// Minimal HTTP client for feed retrievers. Feed URLs are model-authored /\n// user-supplied, so beyond a User-Agent + timeout this guards against SSRF:\n// every URL (and every redirect hop) is DNS-resolved and rejected if it\n// points at a loopback / private / link-local / cloud-metadata address.\n// Redirects are followed MANUALLY so a public URL can't 302 to an internal\n// one and bypass the guard. It does NOT do robots.txt / rate limiting (the\n// engine fetches feeds sequentially to stay gentle).\n\nimport { lookup } from \"node:dns/promises\";\nimport { isIP } from \"node:net\";\n\n// Inlined — the client needs only this one time constant and must stay free of\n// host-side time-constant modules.\nconst ONE_SECOND_MS = 1_000;\n\n/** Identifies the bot to site operators. */\nexport const FEED_USER_AGENT = \"MulmoClaude-FeedBot/1.0 (+https://github.com/receptron/mulmoclaude)\";\n\n/** Per-request wall-clock cap so a hung server can't wedge a refresh. */\nexport const DEFAULT_FEED_TIMEOUT_MS = 30 * ONE_SECOND_MS;\n\n/** Cap on redirect hops followed (each re-checked for SSRF). */\nconst MAX_REDIRECTS = 5;\n\n// CIDR blocks we refuse to fetch: unspecified, loopback, RFC1918, CGNAT,\n// and link-local (which also covers the 169.254.169.254 metadata IP).\n/* eslint-disable sonarjs/no-hardcoded-ip -- intentional SSRF deny-list of loopback / private / link-local / CGNAT CIDRs */\nconst BLOCKED_V4_CIDRS: readonly (readonly [string, number])[] = [\n [\"0.0.0.0\", 8],\n [\"10.0.0.0\", 8],\n [\"100.64.0.0\", 10],\n [\"127.0.0.0\", 8],\n [\"169.254.0.0\", 16],\n [\"172.16.0.0\", 12],\n [\"192.168.0.0\", 16],\n];\n/* eslint-enable sonarjs/no-hardcoded-ip */\n\nfunction ipv4ToInt(address: string): number | null {\n const octets = address.split(\".\").map(Number);\n if (octets.length !== 4 || octets.some((part) => !Number.isInteger(part) || part < 0 || part > 255)) return null;\n return ((octets[0] << 24) | (octets[1] << 16) | (octets[2] << 8) | octets[3]) >>> 0;\n}\n\nfunction isBlockedIpv4(address: string): boolean {\n const value = ipv4ToInt(address);\n if (value === null) return true; // malformed → block\n return BLOCKED_V4_CIDRS.some(([base, bits]) => {\n const baseInt = ipv4ToInt(base) ?? 0;\n const mask = bits === 0 ? 0 : (0xffffffff << (32 - bits)) >>> 0;\n return (value & mask) === (baseInt & mask);\n });\n}\n\nfunction isBlockedIpv6(address: string): boolean {\n const lower = address.toLowerCase();\n if (lower === \"::1\" || lower === \"::\") return true; // loopback, unspecified\n if (lower.startsWith(\"fe80\")) return true; // link-local fe80::/10\n if (lower.startsWith(\"fc\") || lower.startsWith(\"fd\")) return true; // ULA fc00::/7\n const mapped = /^::ffff:(\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}\\.\\d{1,3})$/.exec(lower); // IPv4-mapped\n return mapped ? isBlockedIpv4(mapped[1]) : false;\n}\n\n/** True for any address we must not fetch (also blocks non-IP input). */\nfunction isBlockedIp(address: string): boolean {\n const kind = isIP(address);\n if (kind === 4) return isBlockedIpv4(address);\n if (kind === 6) return isBlockedIpv6(address);\n return true;\n}\n\n/** Reject non-http(s) URLs and URLs that resolve to a private/loopback\n * address (SSRF guard). Throws with a clear reason; returns void on pass. */\nasync function assertFetchableUrl(rawUrl: string): Promise<void> {\n if (!/^https?:\\/\\//i.test(rawUrl)) throw new Error(`refusing non-http(s) URL: ${rawUrl}`);\n const { hostname } = new URL(rawUrl);\n // URL.hostname keeps the brackets for IPv6 literals (`[::1]`); strip them\n // so isIP / the block check see the bare address.\n const host = hostname.startsWith(\"[\") && hostname.endsWith(\"]\") ? hostname.slice(1, -1) : hostname;\n if (isIP(host)) {\n if (isBlockedIp(host)) throw new Error(`refusing to fetch a private/loopback address: ${host}`);\n return;\n }\n let addresses: { address: string }[];\n try {\n addresses = await lookup(host, { all: true });\n } catch (err) {\n throw new Error(`could not resolve host '${host}': ${String(err)}`);\n }\n const blocked = addresses.find((entry) => isBlockedIp(entry.address));\n if (blocked) throw new Error(`refusing to fetch '${host}' — resolves to a private/loopback address (${blocked.address})`);\n}\n\nasync function fetchOnce(url: string, timeoutMs: number): Promise<Response> {\n const controller = new AbortController();\n const timer = setTimeout(() => controller.abort(new DOMException(`feed fetch timed out after ${timeoutMs}ms`, \"TimeoutError\")), timeoutMs);\n try {\n return await fetch(url, { headers: { \"User-Agent\": FEED_USER_AGENT }, signal: controller.signal, redirect: \"manual\" });\n } finally {\n clearTimeout(timer);\n }\n}\n\n// Follow redirects manually, re-running the SSRF guard on every hop so a\n// public URL cannot bounce to an internal target.\nasync function fetchGuarded(rawUrl: string, timeoutMs: number): Promise<Response> {\n let current = rawUrl;\n for (let hop = 0; hop <= MAX_REDIRECTS; hop++) {\n await assertFetchableUrl(current);\n const response = await fetchOnce(current, timeoutMs);\n const redirect = response.status >= 300 && response.status < 400 && response.status !== 304 ? response.headers.get(\"location\") : null;\n if (!redirect) return response;\n current = new URL(redirect, current).toString();\n }\n throw new Error(`too many redirects (>${MAX_REDIRECTS}) starting from ${rawUrl}`);\n}\n\n/** Fetch a URL as text, throwing on guard rejection, network error, or non-2xx. */\nexport async function fetchText(url: string, timeoutMs: number = DEFAULT_FEED_TIMEOUT_MS): Promise<string> {\n const response = await fetchGuarded(url, timeoutMs);\n if (!response.ok) throw new Error(`HTTP ${response.status} ${response.statusText} fetching ${url}`);\n return response.text();\n}\n\n/** Fetch a URL as parsed JSON, throwing on guard rejection, network error, or non-2xx. */\nexport async function fetchJson(url: string, timeoutMs: number = DEFAULT_FEED_TIMEOUT_MS): Promise<unknown> {\n const response = await fetchGuarded(url, timeoutMs);\n if (!response.ok) throw new Error(`HTTP ${response.status} ${response.statusText} fetching ${url}`);\n return response.json();\n}\n","// RSS 2.0 + Atom 1.0 + RSS 1.0 (RDF) parser for the Feeds engine.\n//\n// Deliberately NON-normalizing: it locates the feed's items and hands\n// each one back as its RAW parsed XML element. The host hard-codes no\n// per-feed field list — `ingest.map` resolves source paths against the\n// raw item (tags are keys; attributes are prefixed `@_`; namespaced tags\n// keep their prefix), and the caller inspects the feed to decide what to\n// map. Generic value handling (text-node unwrapping, date parsing by the\n// target field's declared type) lives in `projectItem.ts`, not here.\n//\n// Own copy of the XML plumbing — the Feeds tree does not import the\n// legacy `sources` tree. Pure; unit-testable with fixture strings.\n\nimport { XMLParser } from \"fast-xml-parser\";\n\n// Tiny inline type guards (the host's shared `utils/types` is not available in\n// this shared package — these are the only two we need here).\nfunction isRecord(value: unknown): value is Record<string, unknown> {\n return typeof value === \"object\" && value !== null && !Array.isArray(value);\n}\nfunction isNonEmptyString(value: unknown): value is string {\n return typeof value === \"string\" && value.trim().length > 0;\n}\n\nexport interface ParsedFeedItem {\n /** The raw parsed XML <item>/<entry> object, verbatim. */\n raw: Record<string, unknown>;\n}\n\nexport interface ParsedFeed {\n kind: \"rss\" | \"atom\";\n title: string | null;\n items: ParsedFeedItem[];\n}\n\nconst xml = new XMLParser({\n ignoreAttributes: false,\n attributeNamePrefix: \"@_\",\n cdataPropName: \"#cdata\",\n parseTagValue: false,\n parseAttributeValue: false,\n trimValues: true,\n // Only the item containers are forced to arrays (single vs. many).\n // Everything else stays in its natural parsed shape so `ingest.map`\n // resolves paths the way the caller sees them in the feed.\n isArray: (name) => name === \"item\" || name === \"entry\",\n});\n\n/** Parse an RSS/Atom/RDF feed body. Returns null when the input doesn't\n * look like a feed we understand. */\nexport function parseFeed(body: string): ParsedFeed | null {\n const text = stripBom(body);\n if (!text.trim()) return null;\n let parsed: unknown;\n try {\n parsed = xml.parse(text);\n } catch {\n return null;\n }\n if (!isRecord(parsed)) return null;\n if (isRecord(parsed.rss)) return parseRss(parsed.rss);\n if (isRecord(parsed.feed)) return parseAtom(parsed.feed);\n const rdf = parsed[\"rdf:RDF\"] ?? parsed.RDF;\n if (isRecord(rdf)) return parseRss10(rdf);\n return null;\n}\n\n// Collect the raw item objects, skipping anything that isn't a record or\n// carries no <title> (drops feed-level noise / empty entries).\nfunction collectItems(value: unknown): ParsedFeedItem[] {\n const rawItems = Array.isArray(value) ? value : [];\n const items: ParsedFeedItem[] = [];\n for (const raw of rawItems) {\n if (isRecord(raw) && readString(raw.title) !== null) items.push({ raw });\n }\n return items;\n}\n\nfunction parseRss(rss: Record<string, unknown>): ParsedFeed | null {\n const { channel } = rss;\n if (!isRecord(channel)) return null;\n return { kind: \"rss\", title: readString(channel.title), items: collectItems(channel.item) };\n}\n\nfunction parseRss10(rdf: Record<string, unknown>): ParsedFeed | null {\n const channel = isRecord(rdf.channel) ? rdf.channel : null;\n return { kind: \"rss\", title: channel ? readString(channel.title) : null, items: collectItems(rdf.item) };\n}\n\nfunction parseAtom(feed: Record<string, unknown>): ParsedFeed | null {\n return { kind: \"atom\", title: readString(feed.title), items: collectItems(feed.entry) };\n}\n\n// --- helpers ------------------------------------------------------------\n\n// Extract a string from a plain string, a `{ \"#text\" }` / `{ \"#cdata\" }`\n// node, or an array (first non-empty). Used only to read the feed/item\n// title for the channel name + the empty-item filter.\nfunction readString(value: unknown): string | null {\n if (isNonEmptyString(value)) return value;\n if (typeof value === \"string\") return null;\n if (isRecord(value)) return readStringFromRecord(value);\n if (Array.isArray(value)) return readStringFromArray(value);\n return null;\n}\n\nfunction readStringFromRecord(record: Record<string, unknown>): string | null {\n const text = record[\"#text\"];\n if (isNonEmptyString(text)) return text;\n const cdata = record[\"#cdata\"];\n if (isNonEmptyString(cdata)) return cdata;\n return null;\n}\n\nfunction readStringFromArray(array: readonly unknown[]): string | null {\n for (const entry of array) {\n const resolved = readString(entry);\n if (resolved !== null) return resolved;\n }\n return null;\n}\n\nfunction stripBom(text: string): string {\n return text.charCodeAt(0) === 0xfeff ? text.slice(1) : text;\n}\n","// Tiny dot/bracket path resolver used by the declarative `ingest.map`\n// and `ingest.itemsAt`. Pure, no I/O, exhaustively unit-testable.\n//\n// Supported syntax:\n// \"title\" → root.title\n// \"data.name\" → root.data.name\n// \"results[0].id\" → root.results[0].id\n// \"hourly[]\" → root.hourly (trailing [] = \"the array\n// here\"; the marker is a no-op for value reads)\n// \"data.results[]\" → root.data.results\n//\n// Any miss (wrong type, out-of-range index, absent key) yields\n// `undefined` rather than throwing — declarative configs fail soft.\n\ninterface KeyToken {\n kind: \"key\";\n key: string;\n}\ninterface IndexToken {\n kind: \"index\";\n index: number;\n}\ntype PathToken = KeyToken | IndexToken;\n\nconst BRACKET_RE = /\\[(\\d*)\\]/g;\n\n/** Split one dot-segment (`results[0]`, `hourly[]`, `name`) into tokens.\n * An empty `[]` is an array-identity marker and emits no token — the\n * array value is read by the preceding key. */\nfunction parseSegment(segment: string, tokens: PathToken[]): void {\n const bracketStart = segment.indexOf(\"[\");\n const name = bracketStart === -1 ? segment : segment.slice(0, bracketStart);\n if (name.length > 0) tokens.push({ kind: \"key\", key: name });\n if (bracketStart === -1) return;\n for (const match of segment.slice(bracketStart).matchAll(BRACKET_RE)) {\n const [, inner] = match;\n if (inner.length > 0) tokens.push({ kind: \"index\", index: Number(inner) });\n }\n}\n\nfunction tokenize(path: string): PathToken[] {\n const tokens: PathToken[] = [];\n for (const segment of path.split(\".\")) {\n if (segment.length > 0) parseSegment(segment, tokens);\n }\n return tokens;\n}\n\nfunction step(current: unknown, token: PathToken): unknown {\n if (current === null || current === undefined) return undefined;\n if (token.kind === \"key\") {\n if (typeof current !== \"object\" || Array.isArray(current)) return undefined;\n return (current as Record<string, unknown>)[token.key];\n }\n return Array.isArray(current) ? current[token.index] : undefined;\n}\n\n/** Resolve a dot/bracket path against `root`, or `undefined` on any miss. */\nexport function getByPath(root: unknown, path: string): unknown {\n let current: unknown = root;\n for (const token of tokenize(path)) {\n current = step(current, token);\n }\n return current;\n}\n\n/** Locate the array of raw items for a fetch. With `itemsAt` set, walk\n * to it; without it, the response itself must be the array. Non-arrays\n * yield `[]` so a malformed response is a no-op, not a crash. */\nexport function getItemsArray(root: unknown, itemsAt: string | undefined): unknown[] {\n if (!itemsAt) return Array.isArray(root) ? root : [];\n const value = getByPath(root, itemsAt);\n return Array.isArray(value) ? value : [];\n}\n","// Project one raw retrieved item (a parsed RSS entry, or a JSON object)\n// into a CollectionItem whose keys match the schema's fields, using the\n// declarative `ingest.map`. Also derives the record's id.\n//\n// Id rule: a collection record's filename IS its primaryKey value, and\n// that value must be a safe slug. Feed-native keys (RSS guids/URLs,\n// ISO datetimes) usually are NOT slug-safe, so we slugify the natural\n// key into a deterministic, stable id — same natural key → same id, so\n// re-fetches upsert in place. The natural key comes from the mapped\n// primaryKey value, then `ingest.idFrom`, then a content hash.\n\nimport { createHash } from \"node:crypto\";\nimport { getByPath } from \"./pathResolver.js\";\nimport type { CollectionItem, CollectionSchema } from \"../../collection/index.js\";\nimport type { DeclarativeIngestSpec } from \"../ingestTypes.js\";\n\nfunction asKeyString(value: unknown): string | null {\n if (typeof value === \"string\" && value.trim().length > 0) return value.trim();\n if (typeof value === \"number\" && Number.isFinite(value)) return String(value);\n return null;\n}\n\n/** Collapse an XML element object that carries body text alongside\n * attributes (`{ \"#text\": \"v\", \"@_x\": \"...\" }`) or CDATA to its text.\n * Generic — the host knows no field names, only this fast-xml-parser\n * shape — so a tag like `<guid isPermaLink=\"false\">id</guid>` maps to\n * its text without the caller needing to write `.#text`. */\nfunction unwrapTextNode(value: unknown): unknown {\n if (value && typeof value === \"object\" && !Array.isArray(value)) {\n const obj = value as Record<string, unknown>;\n if (typeof obj[\"#text\"] === \"string\") return obj[\"#text\"];\n if (typeof obj[\"#cdata\"] === \"string\") return obj[\"#cdata\"];\n }\n return value;\n}\n\n/** Normalize a mapped value generically — driven by the SCHEMA's declared\n * field type, never by the source field name. Today: unwrap XML text\n * nodes, and coerce anything mapped into a `date` field to a `YYYY-MM-DD`\n * civil date (the collection `date` type + calendar are day-granularity\n * and parse strictly — a full RFC-3339 timestamp would be rejected). */\nfunction normalizeValue(value: unknown, fieldType: string | undefined): unknown {\n const unwrapped = unwrapTextNode(value);\n if (fieldType === \"date\" && typeof unwrapped === \"string\") {\n const millis = Date.parse(unwrapped);\n if (Number.isFinite(millis)) return new Date(millis).toISOString().slice(0, 10);\n }\n return unwrapped;\n}\n\n/** Slugify a natural key into a stable, filename-safe id. Short, safe\n * keys pass through (lowercased); long or unsafe keys collapse to a\n * hash so the filename stays bounded and valid. */\nfunction toSafeId(natural: string): string {\n const collapsed = natural\n .trim()\n .toLowerCase()\n .replace(/[^a-z0-9]+/g, \"-\");\n // eslint-disable-next-line sonarjs/slow-regex -- anchored hyphen trim, linear, no catastrophic backtracking\n const slug = collapsed.replace(/^-+|-+$/g, \"\");\n if (slug.length > 0 && slug.length <= 80) return slug;\n const hash = createHash(\"sha256\")\n .update(natural || \"item\", \"utf-8\")\n .digest(\"hex\")\n .slice(0, 16);\n return slug.length > 80 ? `${slug.slice(0, 60)}-${hash}` : `feed-${hash}`;\n}\n\nfunction naturalKey(record: CollectionItem, rawItem: unknown, ingest: DeclarativeIngestSpec, schema: CollectionSchema): string {\n const fromMapped = asKeyString(record[schema.primaryKey]);\n if (fromMapped) return fromMapped;\n if (ingest.idFrom) {\n const fromId = asKeyString(unwrapTextNode(getByPath(rawItem, ingest.idFrom)));\n if (fromId) return fromId;\n }\n return JSON.stringify(record);\n}\n\n/** Build a record from a raw fetched item (a parsed RSS/Atom element or a\n * JSON object) using `ingest.map`. Each source path is resolved against\n * the raw item and normalized per the target field's declared type. The\n * returned record's primaryKey is set to the derived safe id (so it\n * doubles as the filename). */\nexport function projectRecord(rawItem: unknown, ingest: DeclarativeIngestSpec, schema: CollectionSchema): CollectionItem {\n const record: CollectionItem = {};\n for (const [targetField, sourcePath] of Object.entries(ingest.map)) {\n const value = normalizeValue(getByPath(rawItem, sourcePath), schema.fields?.[targetField]?.type);\n if (value !== undefined) record[targetField] = value;\n }\n record[schema.primaryKey] = toSafeId(naturalKey(record, rawItem, ingest, schema));\n return record;\n}\n","// RSS / Atom retriever. Fetches the feed, parses it, and projects each\n// item's RAW parsed XML element through `ingest.map`. The map's source\n// paths are the item's own tags/attributes (e.g. `title`, `pubDate`,\n// `enclosure.@_url`, `itunes:duration`) — the host hard-codes no field\n// list; the caller inspects the feed and maps what it carries.\n\nimport { fetchText } from \"../fetch/httpClient.js\";\nimport { parseFeed } from \"../fetch/rssParser.js\";\nimport { projectRecord } from \"../projectItem.js\";\nimport { registerRetriever, type RetrieveFn } from \"./index.js\";\n\nconst retrieveRss: RetrieveFn = async (ingest, schema) => {\n const body = await fetchText(ingest.url);\n const feed = parseFeed(body);\n if (!feed) return { items: [], cursor: {} };\n const items = feed.items.map((item) => projectRecord(item.raw, ingest, schema));\n return { items, cursor: {} };\n};\n\n// Atom shares the same parser + projection path.\nregisterRetriever(\"rss\", retrieveRss);\nregisterRetriever(\"atom\", retrieveRss);\n\nexport { retrieveRss };\n","// Generic JSON-API retriever. Fetches JSON, walks `ingest.itemsAt` to\n// the array of raw items, and projects each through `ingest.map` (whose\n// source paths are dot/bracket paths into each raw item).\n\nimport { fetchJson } from \"../fetch/httpClient.js\";\nimport { getItemsArray } from \"../pathResolver.js\";\nimport { projectRecord } from \"../projectItem.js\";\nimport { registerRetriever, type RetrieveFn } from \"./index.js\";\n\nconst retrieveHttpJson: RetrieveFn = async (ingest, schema) => {\n const json = await fetchJson(ingest.url);\n const rawItems = getItemsArray(json, ingest.itemsAt);\n const items = rawItems.map((raw) => projectRecord(raw, ingest, schema));\n return { items, cursor: {} };\n};\n\nregisterRetriever(\"http-json\", retrieveHttpJson);\n\nexport { retrieveHttpJson };\n","// Per-collection retrieval state — when we last fetched/dispatched, the\n// retriever's cursor (for incremental fetches), and a consecutive-failure\n// counter. NOT committed to git. Location depends on the collection's source:\n// feeds keep it alongside the schema at `<workspace>/feeds/<slug>/_state.json`;\n// skill-backed collections with `ingest.kind: \"agent\"` store it at\n// `<workspace>/data/ingest-state/<slug>.json` (a schema-less `feeds/<slug>/`\n// dir would confuse feed discovery, and `_state.json` must never live in a\n// collection's dataDir where `listItems` would read it as a record).\n// Deliberately minimal: the legacy `sources` tree carries richer backoff\n// state, but the engine starts simple and grows on real need.\n\nimport { mkdir, readFile } from \"node:fs/promises\";\nimport path from \"node:path\";\nimport type { CollectionSource } from \"../../collection/index.js\";\nimport { log, requireFeedsHost } from \"./host.js\";\nimport { feedStatePath, ingestStatePath } from \"../paths.js\";\n\n/** Minimal shape needed to locate a collection's state file. `LoadedCollection`\n * satisfies it. */\nexport interface StateTarget {\n slug: string;\n source: CollectionSource;\n}\n\n/** Resolve the on-disk state file for a collection, branching on source. */\nfunction stateFilePath(target: StateTarget, workspaceRoot: string): string {\n return target.source === \"feed\" ? feedStatePath(target.slug, workspaceRoot) : ingestStatePath(target.slug, workspaceRoot);\n}\n\nexport interface FeedState {\n slug: string;\n /** ISO timestamp of the last successful fetch (declarative) or dispatch\n * (agent ingest), or null if never. */\n lastFetchedAt: string | null;\n /** Free-form retriever cursor (e.g. last-seen id / etag). */\n cursor: Record<string, string>;\n /** Consecutive failed fetches/runs; reset to 0 on success. */\n consecutiveFailures: number;\n /** Agent ingest only: the notifier entry id of the active \"refresh failed\"\n * bell, so a later success can clear exactly that entry. Absent when no\n * failure bell is showing. */\n failureBellId?: string;\n}\n\nexport function defaultFeedState(slug: string): FeedState {\n return { slug, lastFetchedAt: null, cursor: {}, consecutiveFailures: 0 };\n}\n\nfunction normalizeState(slug: string, parsed: Partial<FeedState>): FeedState {\n const base = defaultFeedState(slug);\n const cursor = parsed.cursor && typeof parsed.cursor === \"object\" ? (parsed.cursor as Record<string, string>) : base.cursor;\n return {\n slug,\n lastFetchedAt: typeof parsed.lastFetchedAt === \"string\" ? parsed.lastFetchedAt : base.lastFetchedAt,\n cursor,\n consecutiveFailures: typeof parsed.consecutiveFailures === \"number\" ? parsed.consecutiveFailures : base.consecutiveFailures,\n ...(typeof parsed.failureBellId === \"string\" ? { failureBellId: parsed.failureBellId } : {}),\n };\n}\n\n/** Read a collection's retrieval state, tolerating a missing file (first run →\n * default). The state path branches on `target.source`. */\nexport async function readFeedState(workspaceRoot: string, target: StateTarget): Promise<FeedState> {\n const { slug } = target;\n try {\n const raw = await readFile(stateFilePath(target, workspaceRoot), \"utf-8\");\n return normalizeState(slug, JSON.parse(raw) as Partial<FeedState>);\n } catch (err) {\n const error = err as { code?: string };\n if (error.code !== \"ENOENT\") {\n log.warn(\"feeds\", \"failed to read feed state, using default\", { slug, error: String(err) });\n }\n return defaultFeedState(slug);\n }\n}\n\n/** Persist a collection's retrieval state atomically (creating the parent dir\n * if needed). The state path branches on `target.source`. */\nexport async function writeFeedState(workspaceRoot: string, target: StateTarget, state: FeedState): Promise<void> {\n const file = stateFilePath(target, workspaceRoot);\n await mkdir(path.dirname(file), { recursive: true });\n await requireFeedsHost().writeFileAtomic(file, `${JSON.stringify(state, null, 2)}\\n`);\n}\n","// Agent-performed ingest (`ingest.kind: \"agent\"`). On schedule (or a manual\n// Refresh), the host seeds a hidden background worker — origin `system`, so\n// it never appears in the user's session list — in the collection's declared\n// role with its template + a summary of every record, and the worker edits the\n// records itself via the collections io layer. The host stays domain-free:\n// everything stock-specific (or whatever the collection does) is prose in the\n// template.\n//\n// DI seam: the worker is launched through `FeedsHost.spawnWorker`, injected at\n// boot via `configureFeedsHost`, not imported directly — this module ships in a\n// shared package and must not reach into any host's session/routes layer.\n\nimport { listItems, readSkillTemplate, buildCollectionActionSeedPrompt, type LoadedCollection } from \"../../collection/server/index.js\";\nimport { publish as publishNotifier, clear as clearNotifier } from \"../../notifier/index.js\";\nimport { log, requireFeedsHost, type AgentWorkerResult, type AgentWorkerRunner } from \"./host.js\";\nimport { readFeedState, writeFeedState, type FeedState } from \"./state.js\";\nimport type { AgentIngestSpec } from \"../ingestTypes.js\";\nimport type { RefreshResult } from \"./refreshResult.js\";\n\nexport type { AgentWorkerResult, AgentWorkerRunner } from \"./host.js\";\n\n/** The injected worker launcher, or null if the host was never configured.\n * Read non-throwingly so the failure-isolated contract holds (an unconfigured\n * host becomes an `errors` entry, not a thrown exception). */\nfunction workerRunnerOrNull(): AgentWorkerRunner | null {\n try {\n return requireFeedsHost().spawnWorker;\n } catch {\n return null;\n }\n}\n\nfunction result(slug: string, patch: Partial<RefreshResult>): RefreshResult {\n return { slug, written: 0, removed: 0, errors: [], ...patch };\n}\n\n/** Dispatch one agent-ingest refresh: build the seed, launch a worker, and (on a\n * successful launch) stamp `lastFetchedAt` with the DISPATCH time — not\n * completion. That's what gates the due-loop, so a slow worker can't cause a\n * double-dispatch. `opts.hidden` (default true) runs an invisible system worker\n * for SCHEDULED refreshes; a MANUAL Refresh passes `hidden:false` for a visible,\n * debuggable session. Failure-isolated: never throws; cap-miss / template-miss /\n * launch error leave state untouched and report via `errors`. */\nexport async function refreshViaAgent(workspaceRoot: string, collection: LoadedCollection, opts?: { hidden?: boolean }): Promise<RefreshResult> {\n const hidden = opts?.hidden ?? true;\n const { slug } = collection;\n const ingest = collection.schema.ingest as AgentIngestSpec | undefined;\n if (!ingest || ingest.kind !== \"agent\") return result(slug, { errors: [\"collection has no agent ingest config\"] });\n const workerRunner = workerRunnerOrNull();\n if (!workerRunner) return result(slug, { errors: [\"agent ingest worker runner not configured\"] });\n\n const template = await readSkillTemplate(collection.skillDir, ingest.template);\n if (template === null) return result(slug, { errors: [`ingest template '${ingest.template}' could not be read`] });\n\n const items = await listItems(collection.dataDir, { workspaceRoot });\n const message = buildCollectionActionSeedPrompt(items, collection.schema, template);\n\n // The runner is injected, so guard its promise here to honour the\n // failure-isolated contract (a rejection must become an `errors` entry, not\n // escape into the scheduler loop / route handler).\n let launch: AgentWorkerResult;\n try {\n launch = await workerRunner({\n message,\n roleId: ingest.role,\n hidden,\n // A visible manual run is watched directly — only hidden runs get the\n // completion hook (failure bell + consecutiveFailures); `finalizeRun` only\n // fires it for system-origin sessions anyway.\n onComplete: hidden ? (outcome) => recordOutcome(workspaceRoot, collection, outcome.didError) : undefined,\n });\n } catch (err) {\n log.warn(\"feeds\", \"agent ingest worker launch threw\", { slug, error: String(err) });\n return result(slug, { errors: [String(err)], dispatched: false });\n }\n if (!launch.ok) {\n // Cap-miss or launch error: do NOT touch lastFetchedAt — the next due tick\n // (or manual Refresh) redials. Surface it so a manual refresh reads honest.\n log.info(\"feeds\", \"agent ingest dispatch skipped\", { slug, error: launch.error });\n return result(slug, { errors: [launch.error], dispatched: false });\n }\n\n const state = await readFeedState(workspaceRoot, collection);\n await writeFeedState(workspaceRoot, collection, { ...state, lastFetchedAt: new Date().toISOString() });\n log.info(\"feeds\", \"agent ingest dispatched\", { slug, role: ingest.role, chatId: launch.chatId, hidden, items: items.length });\n // Surface the chatId only for a visible run so the client can open it; a hidden\n // session is excluded from every listing and can't be navigated to anyway.\n return result(slug, { dispatched: true, ...(hidden ? {} : { chatId: launch.chatId }) });\n}\n\n/** Completion hook: reconcile failure tracking when a dispatched worker\n * finishes. On error, bump `consecutiveFailures` and raise a single failure\n * bell (deduped via the persisted `failureBellId`). On success, reset the\n * counter and clear any standing bell. Best-effort + failure-isolated — it\n * runs inside the agent run's teardown, so it must never throw. */\nasync function recordOutcome(workspaceRoot: string, collection: LoadedCollection, didError: boolean): Promise<void> {\n const state = await readFeedState(workspaceRoot, collection);\n const next: FeedState = didError ? await onWorkerError(state, collection) : await onWorkerSuccess(state, collection);\n await writeFeedState(workspaceRoot, collection, next);\n}\n\nasync function onWorkerError(state: FeedState, collection: LoadedCollection): Promise<FeedState> {\n const next: FeedState = { ...state, consecutiveFailures: state.consecutiveFailures + 1 };\n log.warn(\"feeds\", \"agent ingest worker failed\", { slug: collection.slug, consecutiveFailures: next.consecutiveFailures });\n // Raise a single bell on the first failure; a standing one isn't piled onto.\n if (!state.failureBellId) {\n try {\n const { id } = await publishNotifier({\n pluginPkg: \"host\",\n severity: \"nudge\",\n lifecycle: \"fyi\",\n title: \"Collection refresh failed\",\n body: `“${collection.schema.title}” (${collection.slug}) couldn't refresh. Open it to retry.`,\n navigateTarget: `/collections/${collection.slug}`,\n });\n next.failureBellId = id;\n } catch (err) {\n log.warn(\"feeds\", \"failed to publish ingest failure bell\", { slug: collection.slug, error: String(err) });\n }\n }\n return next;\n}\n\nasync function onWorkerSuccess(state: FeedState, collection: LoadedCollection): Promise<FeedState> {\n log.info(\"feeds\", \"agent ingest worker completed\", { slug: collection.slug });\n if (state.failureBellId) {\n await clearNotifier(state.failureBellId).catch((err) =>\n log.warn(\"feeds\", \"failed to clear ingest failure bell\", { slug: collection.slug, error: String(err) }),\n );\n }\n const next: FeedState = { ...state, consecutiveFailures: 0 };\n delete next.failureBellId;\n return next;\n}\n","// Retrieval engine: fetch a feed, upsert its records into the\n// collection's data dir (keyed by primaryKey, so re-fetches replace in\n// place / accumulate by id), and persist per-feed state. Per-feed\n// failures are isolated — `refreshOne` never throws; `refreshDue`\n// processes feeds sequentially to stay gentle on remote hosts (the\n// fetch client does no rate-limiting yet).\n\nimport { deleteItem, discoverCollections, listItems, writeItem, type LoadedCollection } from \"../../collection/server/index.js\";\nimport type { CollectionItem, CollectionSchema } from \"../../collection/index.js\";\nimport { log, requireFeedsHost } from \"./host.js\";\nimport { getRetriever } from \"./retrievers/index.js\";\nimport \"./retrievers/registerAll.js\";\nimport { readFeedState, writeFeedState, type FeedState } from \"./state.js\";\nimport { DEFAULT_FEED_MAX_ITEMS, AGENT_INGEST_KIND, type FeedSchedule, type IngestSpec } from \"../ingestTypes.js\";\nimport { refreshViaAgent } from \"./agentIngest.js\";\nimport type { RefreshResult } from \"./refreshResult.js\";\n\nexport type { RefreshResult } from \"./refreshResult.js\";\n\n// Refresh cadence anchors (ms). Inlined — the engine needs only these two and\n// must stay free of host-side time-constant modules.\nconst ONE_HOUR_MS = 3_600_000;\nconst ONE_DAY_MS = 86_400_000;\n\n/** Feed schemas carry the rich `IngestSpec` (validated at discovery —\n * `source === \"feed\"` requires `ingest`), but the canonical\n * `CollectionSchema.ingest` only promises the minimal `CollectionIngest`.\n * Narrow here so the engine can read the retrieval fields type-safely. */\nfunction feedIngest(schema: CollectionSchema): IngestSpec | undefined {\n return schema.ingest as IngestSpec | undefined;\n}\n\nasync function upsertItems(workspaceRoot: string, feed: LoadedCollection, items: CollectionItem[]): Promise<number> {\n let written = 0;\n for (const item of items) {\n const itemId = item[feed.schema.primaryKey];\n if (typeof itemId !== \"string\" || itemId.length === 0) continue;\n const result = await writeItem(feed.dataDir, itemId, item, { refuseOverwrite: false, workspaceRoot, slug: feed.slug });\n if (result.kind === \"ok\") written += 1;\n else log.warn(\"feeds\", \"feed item write skipped\", { slug: feed.slug, itemId, kind: result.kind });\n }\n return written;\n}\n\n/** The schema's first `date` field, used to order records for the\n * maxItems cap. Null when the schema declares none. */\nfunction firstDateField(schema: CollectionSchema): string | null {\n for (const [key, spec] of Object.entries(schema.fields)) {\n if (spec.type === \"date\") return key;\n }\n return null;\n}\n\n// Epoch ms for a record's date value; missing / unparseable sorts oldest.\nfunction recordTime(item: CollectionItem, field: string): number {\n const value = item[field];\n const millis = typeof value === \"string\" ? Date.parse(value) : NaN;\n return Number.isFinite(millis) ? millis : Number.NEGATIVE_INFINITY;\n}\n\n/** Enforce `ingest.maxItems` (default 100): keep the newest N records by\n * the schema's date field, delete the rest. No-op when the cap is 0/absent\n * of a date field, or when under the cap. Returns the number deleted. */\nasync function pruneFeed(workspaceRoot: string, feed: LoadedCollection): Promise<number> {\n const ingest = feedIngest(feed.schema);\n // maxItems is a declarative-feed concept; agent ingest manages its own record\n // set, so it's never pruned here (and `refreshOne` never calls pruneFeed for it).\n const cap = (ingest && ingest.kind !== AGENT_INGEST_KIND ? ingest.maxItems : undefined) ?? DEFAULT_FEED_MAX_ITEMS;\n if (cap <= 0) return 0;\n const dateField = firstDateField(feed.schema);\n if (!dateField) {\n log.warn(\"feeds\", \"maxItems prune skipped: schema has no date field to order by\", { slug: feed.slug });\n return 0;\n }\n const items = await listItems(feed.dataDir, { workspaceRoot });\n if (items.length <= cap) return 0;\n const stale = [...items].sort((left, right) => recordTime(right, dateField) - recordTime(left, dateField)).slice(cap);\n let removed = 0;\n for (const item of stale) {\n const itemId = item[feed.schema.primaryKey];\n if (typeof itemId !== \"string\" || itemId.length === 0) continue;\n if ((await deleteItem(feed.dataDir, itemId, { workspaceRoot, slug: feed.slug })).kind === \"ok\") removed += 1;\n }\n if (removed > 0) log.info(\"feeds\", \"pruned old feed records\", { slug: feed.slug, removed, cap });\n return removed;\n}\n\n/** Fetch one feed now, upsert its records, then enforce the maxItems cap.\n * Failure-isolated: returns an errors array rather than throwing. */\nexport async function refreshOne(workspaceRoot: string, feed: LoadedCollection, opts?: { hidden?: boolean }): Promise<RefreshResult> {\n const { slug } = feed;\n const ingest = feedIngest(feed.schema);\n if (!ingest) return { slug, written: 0, removed: 0, errors: [\"collection has no ingest config\"] };\n // `agent` ingest dispatches a worker instead of fetching: branch off BEFORE\n // the retriever registry (which only knows declarative kinds). `opts.hidden`\n // (manual Refresh passes false) only affects the agent path; declarative\n // feeds ignore it.\n if (ingest.kind === AGENT_INGEST_KIND) return refreshViaAgent(workspaceRoot, feed, opts);\n const retriever = getRetriever(ingest.kind);\n if (!retriever) return { slug, written: 0, removed: 0, errors: [`no retriever registered for kind '${ingest.kind}'`] };\n const state = await readFeedState(workspaceRoot, feed);\n try {\n const result = await retriever(ingest, feed.schema, state);\n const written = await upsertItems(workspaceRoot, feed, result.items);\n await writeFeedState(workspaceRoot, feed, { ...state, lastFetchedAt: new Date().toISOString(), cursor: result.cursor, consecutiveFailures: 0 });\n const removed = await pruneFeed(workspaceRoot, feed);\n log.info(\"feeds\", \"feed refreshed\", { slug, written, removed, fetched: result.items.length });\n return { slug, written, removed, errors: [] };\n } catch (error) {\n await writeFeedState(workspaceRoot, feed, { ...state, consecutiveFailures: state.consecutiveFailures + 1 });\n const message = String(error);\n log.warn(\"feeds\", \"feed refresh failed\", { slug, error: message });\n return { slug, written: 0, removed: 0, errors: [message] };\n }\n}\n\nfunction dueIntervalMs(schedule: FeedSchedule): number {\n switch (schedule) {\n case \"daily\":\n return ONE_DAY_MS;\n case \"weekly\":\n return 7 * ONE_DAY_MS;\n default:\n return ONE_HOUR_MS;\n }\n}\n\n/** True iff a `daily` schedule with a UTC `atHour` anchor is due. The system\n * task ticks hourly, so \"due\" means: we're in the anchor hour AND we haven't\n * already run in roughly the last day. The 23 h floor (not 24 h) tolerates the\n * tick landing a few minutes earlier than the previous run, while staying well\n * above 1 h so a second tick in the same anchor hour can't double-fire. */\nfunction isDailyAtHourDue(now: Date, atHour: number, lastFetchedAt: string | null): boolean {\n if (now.getUTCHours() !== atHour) return false;\n if (!lastFetchedAt) return true;\n const elapsed = now.getTime() - Date.parse(lastFetchedAt);\n if (!Number.isFinite(elapsed)) return true;\n return elapsed >= 23 * ONE_HOUR_MS;\n}\n\n/** True iff a collection is due to refresh given its schedule + last run.\n * `on-demand` is never auto-due; a `daily` schedule with `atHour` anchors to\n * that UTC hour, otherwise cadence is elapsed-based. */\nfunction isFeedDue(feed: LoadedCollection, state: FeedState): boolean {\n const ingest = feedIngest(feed.schema);\n const schedule = ingest?.schedule;\n if (!schedule || schedule === \"on-demand\") return false;\n if (schedule === \"daily\" && typeof ingest?.atHour === \"number\") {\n return isDailyAtHourDue(new Date(), ingest.atHour, state.lastFetchedAt);\n }\n if (!state.lastFetchedAt) return true;\n const elapsed = Date.now() - Date.parse(state.lastFetchedAt);\n if (!Number.isFinite(elapsed)) return true;\n return elapsed >= dueIntervalMs(schedule);\n}\n\n/** Refresh every collection whose ingest schedule says it's due — declarative\n * feeds AND skill-backed collections with `ingest.kind: \"agent\"`. Called by the\n * hourly system task. Sequential + failure-isolated. */\nexport async function refreshDue(workspaceRoot: string = requireFeedsHost().workspaceRoot): Promise<RefreshResult[]> {\n const all = await discoverCollections({ workspaceRoot });\n const withIngest = all.filter((collection) => collection.schema.ingest);\n const results: RefreshResult[] = [];\n for (const collection of withIngest) {\n // Isolate per-collection failures: `readFeedState`/`refreshOne` (esp. the\n // agent path) can throw, and one bad collection must not abort the whole\n // due-loop. Capture it as an errors result and move on.\n try {\n const state = await readFeedState(workspaceRoot, collection);\n if (!isFeedDue(collection, state)) continue;\n results.push(await refreshOne(workspaceRoot, collection));\n } catch (error) {\n log.warn(\"feeds\", \"scheduled refresh failed for collection\", { slug: collection.slug, error: String(error) });\n results.push({ slug: collection.slug, written: 0, removed: 0, errors: [String(error)] });\n }\n }\n return results;\n}\n","// Shareable registration for the hourly \"refresh due collections\" task.\n// Both MulmoClaude (via `initScheduler`, with persistence + catch-up) and a\n// standalone MulmoTerminal/MulmoBooks (via a bare task-manager `registerTask`)\n// register the SAME definition through this factory, so the id/schedule/run\n// can't drift between hosts. See plans/feat-shareable-feed-refresh-registration.md.\nimport { SCHEDULE_TYPES, MISSED_RUN_POLICIES } from \"@receptron/task-scheduler\";\nimport type { SystemTaskDef } from \"../../scheduler/adapter.js\";\nimport { refreshDue } from \"./engine.js\";\n\n// Id kept stable so the scheduler-state row isn't orphaned across hosts/renames.\nexport const FEED_REFRESH_TASK_ID = \"system:feed-refresh\";\n// Single source of truth for the default refresh cadence (one hour). Core has no\n// shared time module — `server/utils/time.ts` is host-only — so the named export\n// IS the constant; hosts override via `feedRefreshTaskDef({ intervalMs })`.\nexport const DEFAULT_FEED_REFRESH_INTERVAL_MS = 60 * 60 * 1000;\n\n/**\n * The hourly \"refresh due collections\" task, shared by every host.\n *\n * Drives ALL scheduled ingest: declarative feeds (RSS / JSON) fetch directly,\n * and skill-backed `ingest.kind: \"agent\"` collections dispatch a hidden worker.\n *\n * Returns the rich `SystemTaskDef` rather than registering it — each host keeps\n * its own registration mechanism and applies its own schedule overrides first.\n * A host driving a bare task-manager can still `registerTask(...)` it; the extra\n * `missedRunPolicy` field is simply ignored there.\n *\n * @param opts.intervalMs per-host interval override (defaults to one hour)\n * @param opts.workspaceRoot defaults to the configured FeedsHost workspace\n */\nexport function feedRefreshTaskDef(opts?: { workspaceRoot?: string; intervalMs?: number }): SystemTaskDef {\n return {\n id: FEED_REFRESH_TASK_ID,\n name: \"Scheduled collection refresh\",\n description: \"Refresh due collections — fetch declarative feeds + dispatch agent-ingest workers\",\n schedule: { type: SCHEDULE_TYPES.interval, intervalMs: opts?.intervalMs ?? DEFAULT_FEED_REFRESH_INTERVAL_MS },\n missedRunPolicy: MISSED_RUN_POLICIES.runOnce,\n run: () => refreshDue(opts?.workspaceRoot).then(() => {}),\n };\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"index.js","names":[],"sources":["../../../src/feeds/server/host.ts","../../../src/feeds/server/registry.ts","../../../src/feeds/server/retrievers/index.ts","../../../src/feeds/server/fetch/httpClient.ts","../../../src/feeds/server/fetch/rssParser.ts","../../../src/feeds/server/pathResolver.ts","../../../src/feeds/server/projectItem.ts","../../../src/feeds/server/retrievers/rss.ts","../../../src/feeds/server/retrievers/httpJson.ts","../../../src/feeds/server/state.ts","../../../src/feeds/server/agentIngest.ts","../../../src/feeds/server/engine.ts","../../../src/feeds/server/scheduledRefresh.ts"],"sourcesContent":["// Host injection seam for the Feeds engine — mirrors `configureCollectionHost`\n// / `configureScheduler`. The engine is host-agnostic: the workspace root, the\n// logger, an atomic file writer, and the hidden/visible agent-ingest worker\n// launcher are injected once at boot via `configureFeedsHost`. Everything else\n// the engine needs (collection IO, the notifier) is a sibling `@mulmoclaude/core`\n// subpath, imported directly. Both MulmoClaude and MulmoTerminal supply their\n// own host shim.\n\n/** Outcome of launching one hidden/visible agent-ingest worker. `chatId` lets\n * the caller register a completion hook so a failed refresh doesn't die\n * silently. */\nexport type AgentWorkerResult = { ok: true; chatId: string } | { ok: false; error: string };\n\n/** Launches a worker chat. Injected at boot to keep the feeds engine from\n * importing a host's routes/session layer. `hidden` chooses an invisible\n * system worker (scheduled refresh) vs a visible session the user can watch\n * (manual Refresh — debuggable). `onComplete` is a one-shot completion hook\n * (only honoured for hidden workers) so the dispatcher learns success/failure.\n * Returns `ok:false` on the concurrency-cap miss or a launch error — the caller\n * leaves state untouched and retries next tick. */\nexport type AgentWorkerRunner = (args: {\n message: string;\n roleId: string;\n hidden: boolean;\n onComplete?: (outcome: { didError: boolean }) => void | Promise<void>;\n}) => Promise<AgentWorkerResult>;\n\n/** Structured logger, `(prefix, msg, data?)` — same shape as `CollectionLogger`. */\nexport interface FeedsLogger {\n error: (prefix: string, message: string, data?: Record<string, unknown>) => void;\n warn: (prefix: string, message: string, data?: Record<string, unknown>) => void;\n info: (prefix: string, message: string, data?: Record<string, unknown>) => void;\n debug: (prefix: string, message: string, data?: Record<string, unknown>) => void;\n}\n\nexport interface FeedsHost {\n /** Absolute workspace root — the default for `refreshDue()` and state paths. */\n workspaceRoot: string;\n /** Host logger. */\n log: FeedsLogger;\n /** Host atomic file writer (state files). */\n writeFileAtomic: (filePath: string, content: string) => Promise<void>;\n /** Launches the agent-ingest worker (was `setAgentWorkerRunner`). */\n spawnWorker: AgentWorkerRunner;\n}\n\nlet current: FeedsHost | null = null;\n\n/** Wire the feeds engine to a host. Call once at startup, before any refresh. */\nexport function configureFeedsHost(host: FeedsHost): void {\n if (current && current !== host) {\n throw new Error(\"@mulmoclaude/core/feeds: configureFeedsHost() was already called with a different host\");\n }\n current = host;\n}\n\n/** The configured host, or throw if `configureFeedsHost` was never called. */\nexport function requireFeedsHost(): FeedsHost {\n if (!current) throw new Error(\"@mulmoclaude/core/feeds: configureFeedsHost() was not called by the host\");\n return current;\n}\n\n/** Test-only: clear the configured host. */\nexport function resetFeedsHostForTesting(): void {\n current = null;\n}\n\n/** Forwarding logger so engine modules can `import { log }` without each\n * reaching for `requireFeedsHost().log`. */\nexport const log: FeedsLogger = {\n error: (prefix, msg, data) => requireFeedsHost().log.error(prefix, msg, data),\n warn: (prefix, msg, data) => requireFeedsHost().log.warn(prefix, msg, data),\n info: (prefix, msg, data) => requireFeedsHost().log.info(prefix, msg, data),\n debug: (prefix, msg, data) => requireFeedsHost().log.debug(prefix, msg, data),\n};\n","// List the registered data-source feeds. Feeds are CREATED / REMOVED by\n// the agent writing / deleting `feeds/<slug>/schema.json` directly (see\n// config/helps/feeds.md) — the host only discovers + retrieves them.\n// icon / dataPath defaults for agent-authored feed schemas are applied in\n// `collection/server/discovery.ts` (source === \"feed\").\n\nimport { rm } from \"node:fs/promises\";\nimport { discoverCollections, resolveDataDir, safeSlugName, type LoadedCollection } from \"../../collection/server/index.js\";\nimport { log, requireFeedsHost } from \"./host.js\";\nimport { feedDir } from \"../paths.js\";\n\n/** Every registered feed, as a discovered collection (carrying its\n * validated schema, `ingest`, and resolved `dataDir`). */\nexport async function listFeeds(workspaceRoot: string = requireFeedsHost().workspaceRoot): Promise<LoadedCollection[]> {\n const all = await discoverCollections({ workspaceRoot });\n return all.filter((collection) => collection.source === \"feed\");\n}\n\n/** Delete a feed entirely: its records AND its `feeds/<slug>/` directory\n * (schema + state). Idempotent. Host-side only (backs the UI delete\n * button); the agent removes a feed by deleting both directories itself.\n *\n * The records dir is derived from the SLUG (`data/feeds/<slug>`), never\n * from the schema's `dataPath` — feeds are forced into that namespace at\n * discovery, so a malformed/hostile `dataPath` can't redirect this delete\n * at another app's data (e.g. `data/wiki`). `resolveDataDir` also rejects\n * any path that escapes the workspace. */\nexport async function removeFeed(workspaceRoot: string, slug: string): Promise<boolean> {\n const safe = safeSlugName(slug);\n if (safe === null) return false;\n const recordsDir = resolveDataDir(`data/feeds/${safe}`, workspaceRoot);\n try {\n if (recordsDir) await rm(recordsDir, { recursive: true, force: true });\n await rm(feedDir(safe, workspaceRoot), { recursive: true, force: true });\n log.info(\"feeds\", \"feed + records removed\", { slug: safe });\n return true;\n } catch (error) {\n log.warn(\"feeds\", \"feed remove failed\", { slug: safe, error: String(error) });\n return false;\n }\n}\n","// Pluggable retriever registry. Each declarative `ingest.kind` maps to one\n// RetrieveFn that fetches the endpoint and returns projected records.\n// Side-effect registration keeps the engine decoupled from the kinds.\n// The `agent` kind is NOT a retriever (it dispatches a hidden worker before\n// the engine consults this registry); a future `code` kind would register here.\n\nimport type { CollectionItem, CollectionSchema } from \"../../../collection/index.js\";\nimport type { DeclarativeIngestSpec } from \"../../ingestTypes.js\";\nimport type { FeedState } from \"../state.js\";\n\nexport interface RetrieveResult {\n /** Projected records, keyed by primaryKey (the engine upserts them). */\n items: CollectionItem[];\n /** Updated retriever cursor to persist (incremental fetches). */\n cursor: Record<string, string>;\n}\n\n// Declarative-only: the engine branches `agent` ingest off BEFORE looking up a\n// retriever, so a RetrieveFn never sees a non-fetch spec (and rss/http-json can\n// read `ingest.url`/`map` without union narrowing).\nexport type RetrieveFn = (ingest: DeclarativeIngestSpec, schema: CollectionSchema, state: FeedState) => Promise<RetrieveResult>;\n\nconst registry = new Map<string, RetrieveFn>();\n\nexport function registerRetriever(kind: string, retriever: RetrieveFn): void {\n registry.set(kind, retriever);\n}\n\nexport function getRetriever(kind: string): RetrieveFn | undefined {\n return registry.get(kind);\n}\n","// Minimal HTTP client for feed retrievers. Feed URLs are model-authored /\n// user-supplied, so beyond a User-Agent + timeout this guards against SSRF:\n// every URL (and every redirect hop) is DNS-resolved and rejected if it\n// points at a loopback / private / link-local / cloud-metadata address.\n// Redirects are followed MANUALLY so a public URL can't 302 to an internal\n// one and bypass the guard. It does NOT do robots.txt / rate limiting (the\n// engine fetches feeds sequentially to stay gentle).\n\nimport { lookup } from \"node:dns/promises\";\nimport { isIP } from \"node:net\";\n\n// Inlined — the client needs only this one time constant and must stay free of\n// host-side time-constant modules.\nconst ONE_SECOND_MS = 1_000;\n\n/** Identifies the bot to site operators. */\nexport const FEED_USER_AGENT = \"MulmoClaude-FeedBot/1.0 (+https://github.com/receptron/mulmoclaude)\";\n\n/** Per-request wall-clock cap so a hung server can't wedge a refresh. */\nexport const DEFAULT_FEED_TIMEOUT_MS = 30 * ONE_SECOND_MS;\n\n/** Cap on redirect hops followed (each re-checked for SSRF). */\nconst MAX_REDIRECTS = 5;\n\n// CIDR blocks we refuse to fetch: unspecified, loopback, RFC1918, CGNAT,\n// and link-local (which also covers the 169.254.169.254 metadata IP).\n/* eslint-disable sonarjs/no-hardcoded-ip -- intentional SSRF deny-list of loopback / private / link-local / CGNAT CIDRs */\nconst BLOCKED_V4_CIDRS: readonly (readonly [string, number])[] = [\n [\"0.0.0.0\", 8],\n [\"10.0.0.0\", 8],\n [\"100.64.0.0\", 10],\n [\"127.0.0.0\", 8],\n [\"169.254.0.0\", 16],\n [\"172.16.0.0\", 12],\n [\"192.168.0.0\", 16],\n];\n/* eslint-enable sonarjs/no-hardcoded-ip */\n\nfunction ipv4ToInt(address: string): number | null {\n const octets = address.split(\".\").map(Number);\n if (octets.length !== 4 || octets.some((part) => !Number.isInteger(part) || part < 0 || part > 255)) return null;\n return ((octets[0] << 24) | (octets[1] << 16) | (octets[2] << 8) | octets[3]) >>> 0;\n}\n\nfunction isBlockedIpv4(address: string): boolean {\n const value = ipv4ToInt(address);\n if (value === null) return true; // malformed → block\n return BLOCKED_V4_CIDRS.some(([base, bits]) => {\n const baseInt = ipv4ToInt(base) ?? 0;\n const mask = bits === 0 ? 0 : (0xffffffff << (32 - bits)) >>> 0;\n return (value & mask) === (baseInt & mask);\n });\n}\n\nfunction isBlockedIpv6(address: string): boolean {\n const lower = address.toLowerCase();\n if (lower === \"::1\" || lower === \"::\") return true; // loopback, unspecified\n if (lower.startsWith(\"fe80\")) return true; // link-local fe80::/10\n if (lower.startsWith(\"fc\") || lower.startsWith(\"fd\")) return true; // ULA fc00::/7\n const mapped = /^::ffff:(\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}\\.\\d{1,3})$/.exec(lower); // IPv4-mapped\n return mapped ? isBlockedIpv4(mapped[1]) : false;\n}\n\n/** True for any address we must not fetch (also blocks non-IP input). */\nfunction isBlockedIp(address: string): boolean {\n const kind = isIP(address);\n if (kind === 4) return isBlockedIpv4(address);\n if (kind === 6) return isBlockedIpv6(address);\n return true;\n}\n\n/** Reject non-http(s) URLs and URLs that resolve to a private/loopback\n * address (SSRF guard). Throws with a clear reason; returns void on pass. */\nasync function assertFetchableUrl(rawUrl: string): Promise<void> {\n if (!/^https?:\\/\\//i.test(rawUrl)) throw new Error(`refusing non-http(s) URL: ${rawUrl}`);\n const { hostname } = new URL(rawUrl);\n // URL.hostname keeps the brackets for IPv6 literals (`[::1]`); strip them\n // so isIP / the block check see the bare address.\n const host = hostname.startsWith(\"[\") && hostname.endsWith(\"]\") ? hostname.slice(1, -1) : hostname;\n if (isIP(host)) {\n if (isBlockedIp(host)) throw new Error(`refusing to fetch a private/loopback address: ${host}`);\n return;\n }\n let addresses: { address: string }[];\n try {\n addresses = await lookup(host, { all: true });\n } catch (err) {\n throw new Error(`could not resolve host '${host}': ${String(err)}`);\n }\n const blocked = addresses.find((entry) => isBlockedIp(entry.address));\n if (blocked) throw new Error(`refusing to fetch '${host}' — resolves to a private/loopback address (${blocked.address})`);\n}\n\nasync function fetchOnce(url: string, timeoutMs: number): Promise<Response> {\n const controller = new AbortController();\n const timer = setTimeout(() => controller.abort(new DOMException(`feed fetch timed out after ${timeoutMs}ms`, \"TimeoutError\")), timeoutMs);\n try {\n return await fetch(url, { headers: { \"User-Agent\": FEED_USER_AGENT }, signal: controller.signal, redirect: \"manual\" });\n } finally {\n clearTimeout(timer);\n }\n}\n\n// Follow redirects manually, re-running the SSRF guard on every hop so a\n// public URL cannot bounce to an internal target.\nasync function fetchGuarded(rawUrl: string, timeoutMs: number): Promise<Response> {\n let current = rawUrl;\n for (let hop = 0; hop <= MAX_REDIRECTS; hop++) {\n await assertFetchableUrl(current);\n const response = await fetchOnce(current, timeoutMs);\n const redirect = response.status >= 300 && response.status < 400 && response.status !== 304 ? response.headers.get(\"location\") : null;\n if (!redirect) return response;\n current = new URL(redirect, current).toString();\n }\n throw new Error(`too many redirects (>${MAX_REDIRECTS}) starting from ${rawUrl}`);\n}\n\n/** Fetch a URL as text, throwing on guard rejection, network error, or non-2xx. */\nexport async function fetchText(url: string, timeoutMs: number = DEFAULT_FEED_TIMEOUT_MS): Promise<string> {\n const response = await fetchGuarded(url, timeoutMs);\n if (!response.ok) throw new Error(`HTTP ${response.status} ${response.statusText} fetching ${url}`);\n return response.text();\n}\n\n/** Fetch a URL as parsed JSON, throwing on guard rejection, network error, or non-2xx. */\nexport async function fetchJson(url: string, timeoutMs: number = DEFAULT_FEED_TIMEOUT_MS): Promise<unknown> {\n const response = await fetchGuarded(url, timeoutMs);\n if (!response.ok) throw new Error(`HTTP ${response.status} ${response.statusText} fetching ${url}`);\n return response.json();\n}\n","// RSS 2.0 + Atom 1.0 + RSS 1.0 (RDF) parser for the Feeds engine.\n//\n// Deliberately NON-normalizing: it locates the feed's items and hands\n// each one back as its RAW parsed XML element. The host hard-codes no\n// per-feed field list — `ingest.map` resolves source paths against the\n// raw item (tags are keys; attributes are prefixed `@_`; namespaced tags\n// keep their prefix), and the caller inspects the feed to decide what to\n// map. Generic value handling (text-node unwrapping, date parsing by the\n// target field's declared type) lives in `projectItem.ts`, not here.\n//\n// Own copy of the XML plumbing — the Feeds tree does not import the\n// legacy `sources` tree. Pure; unit-testable with fixture strings.\n\nimport { XMLParser } from \"fast-xml-parser\";\n\n// Tiny inline type guards (the host's shared `utils/types` is not available in\n// this shared package — these are the only two we need here).\nfunction isRecord(value: unknown): value is Record<string, unknown> {\n return typeof value === \"object\" && value !== null && !Array.isArray(value);\n}\nfunction isNonEmptyString(value: unknown): value is string {\n return typeof value === \"string\" && value.trim().length > 0;\n}\n\nexport interface ParsedFeedItem {\n /** The raw parsed XML <item>/<entry> object, verbatim. */\n raw: Record<string, unknown>;\n}\n\nexport interface ParsedFeed {\n kind: \"rss\" | \"atom\";\n title: string | null;\n items: ParsedFeedItem[];\n}\n\nconst xml = new XMLParser({\n ignoreAttributes: false,\n attributeNamePrefix: \"@_\",\n cdataPropName: \"#cdata\",\n parseTagValue: false,\n parseAttributeValue: false,\n trimValues: true,\n // Only the item containers are forced to arrays (single vs. many).\n // Everything else stays in its natural parsed shape so `ingest.map`\n // resolves paths the way the caller sees them in the feed.\n isArray: (name) => name === \"item\" || name === \"entry\",\n});\n\n/** Parse an RSS/Atom/RDF feed body. Returns null when the input doesn't\n * look like a feed we understand. */\nexport function parseFeed(body: string): ParsedFeed | null {\n const text = stripBom(body);\n if (!text.trim()) return null;\n let parsed: unknown;\n try {\n parsed = xml.parse(text);\n } catch {\n return null;\n }\n if (!isRecord(parsed)) return null;\n if (isRecord(parsed.rss)) return parseRss(parsed.rss);\n if (isRecord(parsed.feed)) return parseAtom(parsed.feed);\n const rdf = parsed[\"rdf:RDF\"] ?? parsed.RDF;\n if (isRecord(rdf)) return parseRss10(rdf);\n return null;\n}\n\n// Collect the raw item objects, skipping anything that isn't a record or\n// carries no <title> (drops feed-level noise / empty entries).\nfunction collectItems(value: unknown): ParsedFeedItem[] {\n const rawItems = Array.isArray(value) ? value : [];\n const items: ParsedFeedItem[] = [];\n for (const raw of rawItems) {\n if (isRecord(raw) && readString(raw.title) !== null) items.push({ raw });\n }\n return items;\n}\n\nfunction parseRss(rss: Record<string, unknown>): ParsedFeed | null {\n const { channel } = rss;\n if (!isRecord(channel)) return null;\n return { kind: \"rss\", title: readString(channel.title), items: collectItems(channel.item) };\n}\n\nfunction parseRss10(rdf: Record<string, unknown>): ParsedFeed | null {\n const channel = isRecord(rdf.channel) ? rdf.channel : null;\n return { kind: \"rss\", title: channel ? readString(channel.title) : null, items: collectItems(rdf.item) };\n}\n\nfunction parseAtom(feed: Record<string, unknown>): ParsedFeed | null {\n return { kind: \"atom\", title: readString(feed.title), items: collectItems(feed.entry) };\n}\n\n// --- helpers ------------------------------------------------------------\n\n// Extract a string from a plain string, a `{ \"#text\" }` / `{ \"#cdata\" }`\n// node, or an array (first non-empty). Used only to read the feed/item\n// title for the channel name + the empty-item filter.\nfunction readString(value: unknown): string | null {\n if (isNonEmptyString(value)) return value;\n if (typeof value === \"string\") return null;\n if (isRecord(value)) return readStringFromRecord(value);\n if (Array.isArray(value)) return readStringFromArray(value);\n return null;\n}\n\nfunction readStringFromRecord(record: Record<string, unknown>): string | null {\n const text = record[\"#text\"];\n if (isNonEmptyString(text)) return text;\n const cdata = record[\"#cdata\"];\n if (isNonEmptyString(cdata)) return cdata;\n return null;\n}\n\nfunction readStringFromArray(array: readonly unknown[]): string | null {\n for (const entry of array) {\n const resolved = readString(entry);\n if (resolved !== null) return resolved;\n }\n return null;\n}\n\nfunction stripBom(text: string): string {\n return text.charCodeAt(0) === 0xfeff ? text.slice(1) : text;\n}\n","// Tiny dot/bracket path resolver used by the declarative `ingest.map`\n// and `ingest.itemsAt`. Pure, no I/O, exhaustively unit-testable.\n//\n// Supported syntax:\n// \"title\" → root.title\n// \"data.name\" → root.data.name\n// \"results[0].id\" → root.results[0].id\n// \"hourly[]\" → root.hourly (trailing [] = \"the array\n// here\"; the marker is a no-op for value reads)\n// \"data.results[]\" → root.data.results\n//\n// Any miss (wrong type, out-of-range index, absent key) yields\n// `undefined` rather than throwing — declarative configs fail soft.\n\ninterface KeyToken {\n kind: \"key\";\n key: string;\n}\ninterface IndexToken {\n kind: \"index\";\n index: number;\n}\ntype PathToken = KeyToken | IndexToken;\n\nconst BRACKET_RE = /\\[(\\d*)\\]/g;\n\n/** Split one dot-segment (`results[0]`, `hourly[]`, `name`) into tokens.\n * An empty `[]` is an array-identity marker and emits no token — the\n * array value is read by the preceding key. */\nfunction parseSegment(segment: string, tokens: PathToken[]): void {\n const bracketStart = segment.indexOf(\"[\");\n const name = bracketStart === -1 ? segment : segment.slice(0, bracketStart);\n if (name.length > 0) tokens.push({ kind: \"key\", key: name });\n if (bracketStart === -1) return;\n for (const match of segment.slice(bracketStart).matchAll(BRACKET_RE)) {\n const [, inner] = match;\n if (inner.length > 0) tokens.push({ kind: \"index\", index: Number(inner) });\n }\n}\n\nfunction tokenize(path: string): PathToken[] {\n const tokens: PathToken[] = [];\n for (const segment of path.split(\".\")) {\n if (segment.length > 0) parseSegment(segment, tokens);\n }\n return tokens;\n}\n\nfunction step(current: unknown, token: PathToken): unknown {\n if (current === null || current === undefined) return undefined;\n if (token.kind === \"key\") {\n if (typeof current !== \"object\" || Array.isArray(current)) return undefined;\n return (current as Record<string, unknown>)[token.key];\n }\n return Array.isArray(current) ? current[token.index] : undefined;\n}\n\n/** Resolve a dot/bracket path against `root`, or `undefined` on any miss. */\nexport function getByPath(root: unknown, path: string): unknown {\n let current: unknown = root;\n for (const token of tokenize(path)) {\n current = step(current, token);\n }\n return current;\n}\n\n/** Locate the array of raw items for a fetch. With `itemsAt` set, walk\n * to it; without it, the response itself must be the array. Non-arrays\n * yield `[]` so a malformed response is a no-op, not a crash. */\nexport function getItemsArray(root: unknown, itemsAt: string | undefined): unknown[] {\n if (!itemsAt) return Array.isArray(root) ? root : [];\n const value = getByPath(root, itemsAt);\n return Array.isArray(value) ? value : [];\n}\n","// Project one raw retrieved item (a parsed RSS entry, or a JSON object)\n// into a CollectionItem whose keys match the schema's fields, using the\n// declarative `ingest.map`. Also derives the record's id.\n//\n// Id rule: a collection record's filename IS its primaryKey value, and\n// that value must be a safe slug. Feed-native keys (RSS guids/URLs,\n// ISO datetimes) usually are NOT slug-safe, so we slugify the natural\n// key into a deterministic, stable id — same natural key → same id, so\n// re-fetches upsert in place. The natural key comes from the mapped\n// primaryKey value, then `ingest.idFrom`, then a content hash.\n\nimport { createHash } from \"node:crypto\";\nimport { getByPath } from \"./pathResolver.js\";\nimport type { CollectionItem, CollectionSchema } from \"../../collection/index.js\";\nimport type { DeclarativeIngestSpec } from \"../ingestTypes.js\";\n\nfunction asKeyString(value: unknown): string | null {\n if (typeof value === \"string\" && value.trim().length > 0) return value.trim();\n if (typeof value === \"number\" && Number.isFinite(value)) return String(value);\n return null;\n}\n\n/** Collapse an XML element object that carries body text alongside\n * attributes (`{ \"#text\": \"v\", \"@_x\": \"...\" }`) or CDATA to its text.\n * Generic — the host knows no field names, only this fast-xml-parser\n * shape — so a tag like `<guid isPermaLink=\"false\">id</guid>` maps to\n * its text without the caller needing to write `.#text`. */\nfunction unwrapTextNode(value: unknown): unknown {\n if (value && typeof value === \"object\" && !Array.isArray(value)) {\n const obj = value as Record<string, unknown>;\n if (typeof obj[\"#text\"] === \"string\") return obj[\"#text\"];\n if (typeof obj[\"#cdata\"] === \"string\") return obj[\"#cdata\"];\n }\n return value;\n}\n\n/** Normalize a mapped value generically — driven by the SCHEMA's declared\n * field type, never by the source field name. Today: unwrap XML text\n * nodes, and coerce anything mapped into a `date` field to a `YYYY-MM-DD`\n * civil date (the collection `date` type + calendar are day-granularity\n * and parse strictly — a full RFC-3339 timestamp would be rejected). */\nfunction normalizeValue(value: unknown, fieldType: string | undefined): unknown {\n const unwrapped = unwrapTextNode(value);\n if (fieldType === \"date\" && typeof unwrapped === \"string\") {\n const millis = Date.parse(unwrapped);\n if (Number.isFinite(millis)) return new Date(millis).toISOString().slice(0, 10);\n }\n return unwrapped;\n}\n\n/** Slugify a natural key into a stable, filename-safe id. Short, safe\n * keys pass through (lowercased); long or unsafe keys collapse to a\n * hash so the filename stays bounded and valid. */\nfunction toSafeId(natural: string): string {\n const collapsed = natural\n .trim()\n .toLowerCase()\n .replace(/[^a-z0-9]+/g, \"-\");\n // eslint-disable-next-line sonarjs/super-linear-regex -- anchored hyphen trim, linear, no catastrophic backtracking\n const slug = collapsed.replace(/^-+|-+$/g, \"\");\n if (slug.length > 0 && slug.length <= 80) return slug;\n const hash = createHash(\"sha256\")\n .update(natural || \"item\", \"utf-8\")\n .digest(\"hex\")\n .slice(0, 16);\n return slug.length > 80 ? `${slug.slice(0, 60)}-${hash}` : `feed-${hash}`;\n}\n\nfunction naturalKey(record: CollectionItem, rawItem: unknown, ingest: DeclarativeIngestSpec, schema: CollectionSchema): string {\n const fromMapped = asKeyString(record[schema.primaryKey]);\n if (fromMapped) return fromMapped;\n if (ingest.idFrom) {\n const fromId = asKeyString(unwrapTextNode(getByPath(rawItem, ingest.idFrom)));\n if (fromId) return fromId;\n }\n return JSON.stringify(record);\n}\n\n/** Build a record from a raw fetched item (a parsed RSS/Atom element or a\n * JSON object) using `ingest.map`. Each source path is resolved against\n * the raw item and normalized per the target field's declared type. The\n * returned record's primaryKey is set to the derived safe id (so it\n * doubles as the filename). */\nexport function projectRecord(rawItem: unknown, ingest: DeclarativeIngestSpec, schema: CollectionSchema): CollectionItem {\n const record: CollectionItem = {};\n for (const [targetField, sourcePath] of Object.entries(ingest.map)) {\n const value = normalizeValue(getByPath(rawItem, sourcePath), schema.fields?.[targetField]?.type);\n if (value !== undefined) record[targetField] = value;\n }\n record[schema.primaryKey] = toSafeId(naturalKey(record, rawItem, ingest, schema));\n return record;\n}\n","// RSS / Atom retriever. Fetches the feed, parses it, and projects each\n// item's RAW parsed XML element through `ingest.map`. The map's source\n// paths are the item's own tags/attributes (e.g. `title`, `pubDate`,\n// `enclosure.@_url`, `itunes:duration`) — the host hard-codes no field\n// list; the caller inspects the feed and maps what it carries.\n\nimport { fetchText } from \"../fetch/httpClient.js\";\nimport { parseFeed } from \"../fetch/rssParser.js\";\nimport { projectRecord } from \"../projectItem.js\";\nimport { registerRetriever, type RetrieveFn } from \"./index.js\";\n\nconst retrieveRss: RetrieveFn = async (ingest, schema) => {\n const body = await fetchText(ingest.url);\n const feed = parseFeed(body);\n if (!feed) return { items: [], cursor: {} };\n const items = feed.items.map((item) => projectRecord(item.raw, ingest, schema));\n return { items, cursor: {} };\n};\n\n// Atom shares the same parser + projection path.\nregisterRetriever(\"rss\", retrieveRss);\nregisterRetriever(\"atom\", retrieveRss);\n\nexport { retrieveRss };\n","// Generic JSON-API retriever. Fetches JSON, walks `ingest.itemsAt` to\n// the array of raw items, and projects each through `ingest.map` (whose\n// source paths are dot/bracket paths into each raw item).\n\nimport { fetchJson } from \"../fetch/httpClient.js\";\nimport { getItemsArray } from \"../pathResolver.js\";\nimport { projectRecord } from \"../projectItem.js\";\nimport { registerRetriever, type RetrieveFn } from \"./index.js\";\n\nconst retrieveHttpJson: RetrieveFn = async (ingest, schema) => {\n const json = await fetchJson(ingest.url);\n const rawItems = getItemsArray(json, ingest.itemsAt);\n const items = rawItems.map((raw) => projectRecord(raw, ingest, schema));\n return { items, cursor: {} };\n};\n\nregisterRetriever(\"http-json\", retrieveHttpJson);\n\nexport { retrieveHttpJson };\n","// Per-collection retrieval state — when we last fetched/dispatched, the\n// retriever's cursor (for incremental fetches), and a consecutive-failure\n// counter. NOT committed to git. Location depends on the collection's source:\n// feeds keep it alongside the schema at `<workspace>/feeds/<slug>/_state.json`;\n// skill-backed collections with `ingest.kind: \"agent\"` store it at\n// `<workspace>/data/ingest-state/<slug>.json` (a schema-less `feeds/<slug>/`\n// dir would confuse feed discovery, and `_state.json` must never live in a\n// collection's dataDir where `listItems` would read it as a record).\n// Deliberately minimal: the legacy `sources` tree carries richer backoff\n// state, but the engine starts simple and grows on real need.\n\nimport { mkdir, readFile } from \"node:fs/promises\";\nimport path from \"node:path\";\nimport type { CollectionSource } from \"../../collection/index.js\";\nimport { log, requireFeedsHost } from \"./host.js\";\nimport { feedStatePath, ingestStatePath } from \"../paths.js\";\n\n/** Minimal shape needed to locate a collection's state file. `LoadedCollection`\n * satisfies it. */\nexport interface StateTarget {\n slug: string;\n source: CollectionSource;\n}\n\n/** Resolve the on-disk state file for a collection, branching on source. */\nfunction stateFilePath(target: StateTarget, workspaceRoot: string): string {\n return target.source === \"feed\" ? feedStatePath(target.slug, workspaceRoot) : ingestStatePath(target.slug, workspaceRoot);\n}\n\nexport interface FeedState {\n slug: string;\n /** ISO timestamp of the last successful fetch (declarative) or dispatch\n * (agent ingest), or null if never. */\n lastFetchedAt: string | null;\n /** Free-form retriever cursor (e.g. last-seen id / etag). */\n cursor: Record<string, string>;\n /** Consecutive failed fetches/runs; reset to 0 on success. */\n consecutiveFailures: number;\n /** Agent ingest only: the notifier entry id of the active \"refresh failed\"\n * bell, so a later success can clear exactly that entry. Absent when no\n * failure bell is showing. */\n failureBellId?: string;\n}\n\nexport function defaultFeedState(slug: string): FeedState {\n return { slug, lastFetchedAt: null, cursor: {}, consecutiveFailures: 0 };\n}\n\nfunction normalizeState(slug: string, parsed: Partial<FeedState>): FeedState {\n const base = defaultFeedState(slug);\n const cursor = parsed.cursor && typeof parsed.cursor === \"object\" ? (parsed.cursor as Record<string, string>) : base.cursor;\n return {\n slug,\n lastFetchedAt: typeof parsed.lastFetchedAt === \"string\" ? parsed.lastFetchedAt : base.lastFetchedAt,\n cursor,\n consecutiveFailures: typeof parsed.consecutiveFailures === \"number\" ? parsed.consecutiveFailures : base.consecutiveFailures,\n ...(typeof parsed.failureBellId === \"string\" ? { failureBellId: parsed.failureBellId } : {}),\n };\n}\n\n/** Read a collection's retrieval state, tolerating a missing file (first run →\n * default). The state path branches on `target.source`. */\nexport async function readFeedState(workspaceRoot: string, target: StateTarget): Promise<FeedState> {\n const { slug } = target;\n try {\n const raw = await readFile(stateFilePath(target, workspaceRoot), \"utf-8\");\n return normalizeState(slug, JSON.parse(raw) as Partial<FeedState>);\n } catch (err) {\n const error = err as { code?: string };\n if (error.code !== \"ENOENT\") {\n log.warn(\"feeds\", \"failed to read feed state, using default\", { slug, error: String(err) });\n }\n return defaultFeedState(slug);\n }\n}\n\n/** Persist a collection's retrieval state atomically (creating the parent dir\n * if needed). The state path branches on `target.source`. */\nexport async function writeFeedState(workspaceRoot: string, target: StateTarget, state: FeedState): Promise<void> {\n const file = stateFilePath(target, workspaceRoot);\n await mkdir(path.dirname(file), { recursive: true });\n await requireFeedsHost().writeFileAtomic(file, `${JSON.stringify(state, null, 2)}\\n`);\n}\n","// Agent-performed ingest (`ingest.kind: \"agent\"`). On schedule (or a manual\n// Refresh), the host seeds a hidden background worker — origin `system`, so\n// it never appears in the user's session list — in the collection's declared\n// role with its template + a summary of every record, and the worker edits the\n// records itself via the collections io layer. The host stays domain-free:\n// everything stock-specific (or whatever the collection does) is prose in the\n// template.\n//\n// DI seam: the worker is launched through `FeedsHost.spawnWorker`, injected at\n// boot via `configureFeedsHost`, not imported directly — this module ships in a\n// shared package and must not reach into any host's session/routes layer.\n\nimport { listItems, readSkillTemplate, buildCollectionActionSeedPrompt, type LoadedCollection } from \"../../collection/server/index.js\";\nimport { publish as publishNotifier, clear as clearNotifier } from \"../../notifier/index.js\";\nimport { log, requireFeedsHost, type AgentWorkerResult, type AgentWorkerRunner } from \"./host.js\";\nimport { readFeedState, writeFeedState, type FeedState } from \"./state.js\";\nimport type { AgentIngestSpec } from \"../ingestTypes.js\";\nimport type { RefreshResult } from \"./refreshResult.js\";\n\nexport type { AgentWorkerResult, AgentWorkerRunner } from \"./host.js\";\n\n/** The injected worker launcher, or null if the host was never configured.\n * Read non-throwingly so the failure-isolated contract holds (an unconfigured\n * host becomes an `errors` entry, not a thrown exception). */\nfunction workerRunnerOrNull(): AgentWorkerRunner | null {\n try {\n return requireFeedsHost().spawnWorker;\n } catch {\n return null;\n }\n}\n\nfunction result(slug: string, patch: Partial<RefreshResult>): RefreshResult {\n return { slug, written: 0, removed: 0, errors: [], ...patch };\n}\n\n/** Dispatch one agent-ingest refresh: build the seed, launch a worker, and (on a\n * successful launch) stamp `lastFetchedAt` with the DISPATCH time — not\n * completion. That's what gates the due-loop, so a slow worker can't cause a\n * double-dispatch. `opts.hidden` (default true) runs an invisible system worker\n * for SCHEDULED refreshes; a MANUAL Refresh passes `hidden:false` for a visible,\n * debuggable session. Failure-isolated: never throws; cap-miss / template-miss /\n * launch error leave state untouched and report via `errors`. */\nexport async function refreshViaAgent(workspaceRoot: string, collection: LoadedCollection, opts?: { hidden?: boolean }): Promise<RefreshResult> {\n const hidden = opts?.hidden ?? true;\n const { slug } = collection;\n const ingest = collection.schema.ingest as AgentIngestSpec | undefined;\n if (!ingest || ingest.kind !== \"agent\") return result(slug, { errors: [\"collection has no agent ingest config\"] });\n const workerRunner = workerRunnerOrNull();\n if (!workerRunner) return result(slug, { errors: [\"agent ingest worker runner not configured\"] });\n\n const template = await readSkillTemplate(collection.skillDir, ingest.template);\n if (template === null) return result(slug, { errors: [`ingest template '${ingest.template}' could not be read`] });\n\n const items = await listItems(collection.dataDir, { workspaceRoot });\n const message = buildCollectionActionSeedPrompt(items, collection.schema, template);\n\n // The runner is injected, so guard its promise here to honour the\n // failure-isolated contract (a rejection must become an `errors` entry, not\n // escape into the scheduler loop / route handler).\n let launch: AgentWorkerResult;\n try {\n launch = await workerRunner({\n message,\n roleId: ingest.role,\n hidden,\n // A visible manual run is watched directly — only hidden runs get the\n // completion hook (failure bell + consecutiveFailures); `finalizeRun` only\n // fires it for system-origin sessions anyway.\n onComplete: hidden ? (outcome) => recordOutcome(workspaceRoot, collection, outcome.didError) : undefined,\n });\n } catch (err) {\n log.warn(\"feeds\", \"agent ingest worker launch threw\", { slug, error: String(err) });\n return result(slug, { errors: [String(err)], dispatched: false });\n }\n if (!launch.ok) {\n // Cap-miss or launch error: do NOT touch lastFetchedAt — the next due tick\n // (or manual Refresh) redials. Surface it so a manual refresh reads honest.\n log.info(\"feeds\", \"agent ingest dispatch skipped\", { slug, error: launch.error });\n return result(slug, { errors: [launch.error], dispatched: false });\n }\n\n const state = await readFeedState(workspaceRoot, collection);\n await writeFeedState(workspaceRoot, collection, { ...state, lastFetchedAt: new Date().toISOString() });\n log.info(\"feeds\", \"agent ingest dispatched\", { slug, role: ingest.role, chatId: launch.chatId, hidden, items: items.length });\n // Surface the chatId only for a visible run so the client can open it; a hidden\n // session is excluded from every listing and can't be navigated to anyway.\n return result(slug, { dispatched: true, ...(hidden ? {} : { chatId: launch.chatId }) });\n}\n\n/** Completion hook: reconcile failure tracking when a dispatched worker\n * finishes. On error, bump `consecutiveFailures` and raise a single failure\n * bell (deduped via the persisted `failureBellId`). On success, reset the\n * counter and clear any standing bell. Best-effort + failure-isolated — it\n * runs inside the agent run's teardown, so it must never throw. */\nasync function recordOutcome(workspaceRoot: string, collection: LoadedCollection, didError: boolean): Promise<void> {\n const state = await readFeedState(workspaceRoot, collection);\n const next: FeedState = didError ? await onWorkerError(state, collection) : await onWorkerSuccess(state, collection);\n await writeFeedState(workspaceRoot, collection, next);\n}\n\nasync function onWorkerError(state: FeedState, collection: LoadedCollection): Promise<FeedState> {\n const next: FeedState = { ...state, consecutiveFailures: state.consecutiveFailures + 1 };\n log.warn(\"feeds\", \"agent ingest worker failed\", { slug: collection.slug, consecutiveFailures: next.consecutiveFailures });\n // Raise a single bell on the first failure; a standing one isn't piled onto.\n if (!state.failureBellId) {\n try {\n const { id } = await publishNotifier({\n pluginPkg: \"host\",\n severity: \"nudge\",\n lifecycle: \"fyi\",\n title: \"Collection refresh failed\",\n body: `“${collection.schema.title}” (${collection.slug}) couldn't refresh. Open it to retry.`,\n navigateTarget: `/collections/${collection.slug}`,\n });\n next.failureBellId = id;\n } catch (err) {\n log.warn(\"feeds\", \"failed to publish ingest failure bell\", { slug: collection.slug, error: String(err) });\n }\n }\n return next;\n}\n\nasync function onWorkerSuccess(state: FeedState, collection: LoadedCollection): Promise<FeedState> {\n log.info(\"feeds\", \"agent ingest worker completed\", { slug: collection.slug });\n if (state.failureBellId) {\n await clearNotifier(state.failureBellId).catch((err) =>\n log.warn(\"feeds\", \"failed to clear ingest failure bell\", { slug: collection.slug, error: String(err) }),\n );\n }\n const next: FeedState = { ...state, consecutiveFailures: 0 };\n delete next.failureBellId;\n return next;\n}\n","// Retrieval engine: fetch a feed, upsert its records into the\n// collection's data dir (keyed by primaryKey, so re-fetches replace in\n// place / accumulate by id), and persist per-feed state. Per-feed\n// failures are isolated — `refreshOne` never throws; `refreshDue`\n// processes feeds sequentially to stay gentle on remote hosts (the\n// fetch client does no rate-limiting yet).\n\nimport { deleteItem, discoverCollections, listItems, writeItem, type LoadedCollection } from \"../../collection/server/index.js\";\nimport type { CollectionItem, CollectionSchema } from \"../../collection/index.js\";\nimport { log, requireFeedsHost } from \"./host.js\";\nimport { getRetriever } from \"./retrievers/index.js\";\nimport \"./retrievers/registerAll.js\";\nimport { readFeedState, writeFeedState, type FeedState } from \"./state.js\";\nimport { DEFAULT_FEED_MAX_ITEMS, AGENT_INGEST_KIND, type FeedSchedule, type IngestSpec } from \"../ingestTypes.js\";\nimport { refreshViaAgent } from \"./agentIngest.js\";\nimport type { RefreshResult } from \"./refreshResult.js\";\n\nexport type { RefreshResult } from \"./refreshResult.js\";\n\n// Refresh cadence anchors (ms). Inlined — the engine needs only these two and\n// must stay free of host-side time-constant modules.\nconst ONE_HOUR_MS = 3_600_000;\nconst ONE_DAY_MS = 86_400_000;\n\n/** Feed schemas carry the rich `IngestSpec` (validated at discovery —\n * `source === \"feed\"` requires `ingest`), but the canonical\n * `CollectionSchema.ingest` only promises the minimal `CollectionIngest`.\n * Narrow here so the engine can read the retrieval fields type-safely. */\nfunction feedIngest(schema: CollectionSchema): IngestSpec | undefined {\n return schema.ingest as IngestSpec | undefined;\n}\n\nasync function upsertItems(workspaceRoot: string, feed: LoadedCollection, items: CollectionItem[]): Promise<number> {\n let written = 0;\n for (const item of items) {\n const itemId = item[feed.schema.primaryKey];\n if (typeof itemId !== \"string\" || itemId.length === 0) continue;\n const result = await writeItem(feed.dataDir, itemId, item, { refuseOverwrite: false, workspaceRoot, slug: feed.slug });\n if (result.kind === \"ok\") written += 1;\n else log.warn(\"feeds\", \"feed item write skipped\", { slug: feed.slug, itemId, kind: result.kind });\n }\n return written;\n}\n\n/** The schema's first `date` field, used to order records for the\n * maxItems cap. Null when the schema declares none. */\nfunction firstDateField(schema: CollectionSchema): string | null {\n for (const [key, spec] of Object.entries(schema.fields)) {\n if (spec.type === \"date\") return key;\n }\n return null;\n}\n\n// Epoch ms for a record's date value; missing / unparseable sorts oldest.\nfunction recordTime(item: CollectionItem, field: string): number {\n const value = item[field];\n const millis = typeof value === \"string\" ? Date.parse(value) : NaN;\n return Number.isFinite(millis) ? millis : Number.NEGATIVE_INFINITY;\n}\n\n/** Enforce `ingest.maxItems` (default 100): keep the newest N records by\n * the schema's date field, delete the rest. No-op when the cap is 0/absent\n * of a date field, or when under the cap. Returns the number deleted. */\nasync function pruneFeed(workspaceRoot: string, feed: LoadedCollection): Promise<number> {\n const ingest = feedIngest(feed.schema);\n // maxItems is a declarative-feed concept; agent ingest manages its own record\n // set, so it's never pruned here (and `refreshOne` never calls pruneFeed for it).\n const cap = (ingest && ingest.kind !== AGENT_INGEST_KIND ? ingest.maxItems : undefined) ?? DEFAULT_FEED_MAX_ITEMS;\n if (cap <= 0) return 0;\n const dateField = firstDateField(feed.schema);\n if (!dateField) {\n log.warn(\"feeds\", \"maxItems prune skipped: schema has no date field to order by\", { slug: feed.slug });\n return 0;\n }\n const items = await listItems(feed.dataDir, { workspaceRoot });\n if (items.length <= cap) return 0;\n const stale = [...items].sort((left, right) => recordTime(right, dateField) - recordTime(left, dateField)).slice(cap);\n let removed = 0;\n for (const item of stale) {\n const itemId = item[feed.schema.primaryKey];\n if (typeof itemId !== \"string\" || itemId.length === 0) continue;\n if ((await deleteItem(feed.dataDir, itemId, { workspaceRoot, slug: feed.slug })).kind === \"ok\") removed += 1;\n }\n if (removed > 0) log.info(\"feeds\", \"pruned old feed records\", { slug: feed.slug, removed, cap });\n return removed;\n}\n\n/** Fetch one feed now, upsert its records, then enforce the maxItems cap.\n * Failure-isolated: returns an errors array rather than throwing. */\nexport async function refreshOne(workspaceRoot: string, feed: LoadedCollection, opts?: { hidden?: boolean }): Promise<RefreshResult> {\n const { slug } = feed;\n const ingest = feedIngest(feed.schema);\n if (!ingest) return { slug, written: 0, removed: 0, errors: [\"collection has no ingest config\"] };\n // `agent` ingest dispatches a worker instead of fetching: branch off BEFORE\n // the retriever registry (which only knows declarative kinds). `opts.hidden`\n // (manual Refresh passes false) only affects the agent path; declarative\n // feeds ignore it.\n if (ingest.kind === AGENT_INGEST_KIND) return refreshViaAgent(workspaceRoot, feed, opts);\n const retriever = getRetriever(ingest.kind);\n if (!retriever) return { slug, written: 0, removed: 0, errors: [`no retriever registered for kind '${ingest.kind}'`] };\n const state = await readFeedState(workspaceRoot, feed);\n try {\n const result = await retriever(ingest, feed.schema, state);\n const written = await upsertItems(workspaceRoot, feed, result.items);\n await writeFeedState(workspaceRoot, feed, { ...state, lastFetchedAt: new Date().toISOString(), cursor: result.cursor, consecutiveFailures: 0 });\n const removed = await pruneFeed(workspaceRoot, feed);\n log.info(\"feeds\", \"feed refreshed\", { slug, written, removed, fetched: result.items.length });\n return { slug, written, removed, errors: [] };\n } catch (error) {\n await writeFeedState(workspaceRoot, feed, { ...state, consecutiveFailures: state.consecutiveFailures + 1 });\n const message = String(error);\n log.warn(\"feeds\", \"feed refresh failed\", { slug, error: message });\n return { slug, written: 0, removed: 0, errors: [message] };\n }\n}\n\nfunction dueIntervalMs(schedule: FeedSchedule): number {\n switch (schedule) {\n case \"daily\":\n return ONE_DAY_MS;\n case \"weekly\":\n return 7 * ONE_DAY_MS;\n default:\n return ONE_HOUR_MS;\n }\n}\n\n/** True iff a `daily` schedule with a UTC `atHour` anchor is due. The system\n * task ticks hourly, so \"due\" means: we're in the anchor hour AND we haven't\n * already run in roughly the last day. The 23 h floor (not 24 h) tolerates the\n * tick landing a few minutes earlier than the previous run, while staying well\n * above 1 h so a second tick in the same anchor hour can't double-fire. */\nfunction isDailyAtHourDue(now: Date, atHour: number, lastFetchedAt: string | null): boolean {\n if (now.getUTCHours() !== atHour) return false;\n if (!lastFetchedAt) return true;\n const elapsed = now.getTime() - Date.parse(lastFetchedAt);\n if (!Number.isFinite(elapsed)) return true;\n return elapsed >= 23 * ONE_HOUR_MS;\n}\n\n/** True iff a collection is due to refresh given its schedule + last run.\n * `on-demand` is never auto-due; a `daily` schedule with `atHour` anchors to\n * that UTC hour, otherwise cadence is elapsed-based. */\nfunction isFeedDue(feed: LoadedCollection, state: FeedState): boolean {\n const ingest = feedIngest(feed.schema);\n const schedule = ingest?.schedule;\n if (!schedule || schedule === \"on-demand\") return false;\n if (schedule === \"daily\" && typeof ingest?.atHour === \"number\") {\n return isDailyAtHourDue(new Date(), ingest.atHour, state.lastFetchedAt);\n }\n if (!state.lastFetchedAt) return true;\n const elapsed = Date.now() - Date.parse(state.lastFetchedAt);\n if (!Number.isFinite(elapsed)) return true;\n return elapsed >= dueIntervalMs(schedule);\n}\n\n/** Refresh every collection whose ingest schedule says it's due — declarative\n * feeds AND skill-backed collections with `ingest.kind: \"agent\"`. Called by the\n * hourly system task. Sequential + failure-isolated. */\nexport async function refreshDue(workspaceRoot: string = requireFeedsHost().workspaceRoot): Promise<RefreshResult[]> {\n const all = await discoverCollections({ workspaceRoot });\n const withIngest = all.filter((collection) => collection.schema.ingest);\n const results: RefreshResult[] = [];\n for (const collection of withIngest) {\n // Isolate per-collection failures: `readFeedState`/`refreshOne` (esp. the\n // agent path) can throw, and one bad collection must not abort the whole\n // due-loop. Capture it as an errors result and move on.\n try {\n const state = await readFeedState(workspaceRoot, collection);\n if (!isFeedDue(collection, state)) continue;\n results.push(await refreshOne(workspaceRoot, collection));\n } catch (error) {\n log.warn(\"feeds\", \"scheduled refresh failed for collection\", { slug: collection.slug, error: String(error) });\n results.push({ slug: collection.slug, written: 0, removed: 0, errors: [String(error)] });\n }\n }\n return results;\n}\n","// Shareable registration for the hourly \"refresh due collections\" task.\n// Both MulmoClaude (via `initScheduler`, with persistence + catch-up) and a\n// standalone MulmoTerminal/MulmoBooks (via a bare task-manager `registerTask`)\n// register the SAME definition through this factory, so the id/schedule/run\n// can't drift between hosts. See plans/done/feat-shareable-feed-refresh-registration.md.\nimport { SCHEDULE_TYPES, MISSED_RUN_POLICIES } from \"@receptron/task-scheduler\";\nimport type { SystemTaskDef } from \"../../scheduler/adapter.js\";\nimport { refreshDue } from \"./engine.js\";\n\n// Id kept stable so the scheduler-state row isn't orphaned across hosts/renames.\nexport const FEED_REFRESH_TASK_ID = \"system:feed-refresh\";\n// Single source of truth for the default refresh cadence (one hour). Core has no\n// shared time module — `server/utils/time.ts` is host-only — so the named export\n// IS the constant; hosts override via `feedRefreshTaskDef({ intervalMs })`.\nexport const DEFAULT_FEED_REFRESH_INTERVAL_MS = 60 * 60 * 1000;\n\n/**\n * The hourly \"refresh due collections\" task, shared by every host.\n *\n * Drives ALL scheduled ingest: declarative feeds (RSS / JSON) fetch directly,\n * and skill-backed `ingest.kind: \"agent\"` collections dispatch a hidden worker.\n *\n * Returns the rich `SystemTaskDef` rather than registering it — each host keeps\n * its own registration mechanism and applies its own schedule overrides first.\n * A host driving a bare task-manager can still `registerTask(...)` it; the extra\n * `missedRunPolicy` field is simply ignored there.\n *\n * @param opts.intervalMs per-host interval override (defaults to one hour)\n * @param opts.workspaceRoot defaults to the configured FeedsHost workspace\n */\nexport function feedRefreshTaskDef(opts?: { workspaceRoot?: string; intervalMs?: number }): SystemTaskDef {\n return {\n id: FEED_REFRESH_TASK_ID,\n name: \"Scheduled collection refresh\",\n description: \"Refresh due collections — fetch declarative feeds + dispatch agent-ingest workers\",\n schedule: { type: SCHEDULE_TYPES.interval, intervalMs: opts?.intervalMs ?? DEFAULT_FEED_REFRESH_INTERVAL_MS },\n missedRunPolicy: MISSED_RUN_POLICIES.runOnce,\n run: () => refreshDue(opts?.workspaceRoot).then(() => {}),\n };\n}\n"],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"notifier-ChpY0XrY.js","names":[],"sources":["../src/notifier/types.ts","../src/notifier/store.ts","../src/notifier/validate.ts","../src/notifier/engine.ts"],"sourcesContent":["// Notifier value types. Kept dependency-free (no node, no fs, no\n// pubsub) so the host's API-route layer and any future browser\n// consumer can validate inbound payloads against the same enum\n// constants the engine accepts without pulling in the engine's I/O.\n\n/** Two notification shapes, distinguished by who fires the close call:\n *\n * `fyi` — informational. The host (bell panel) clears it when the\n * user dismisses the row. No deep-link target.\n * `action` — pending obligation. The plugin clears it when the\n * underlying domain state changes (the user paid the tax,\n * viewed the digest, etc.). The bell row navigates to\n * `navigateTarget` on click.\n *\n * The engine reads `lifecycle` only to enforce two publish-time rules\n * (everything downstream — pubsub fan-out, persistence, history — is\n * lifecycle-blind):\n *\n * 1. `action` requires a non-empty `navigateTarget`. Without one,\n * clicking the row does nothing and the entry is a degraded fyi.\n * 2. `action` cannot use `info` severity. A low-priority obligation\n * is incoherent — fyi if it's a ping, `nudge`/`urgent` if it's a\n * real obligation worth a landing page.\n *\n * Both rules are mirrored in the HTTP layer so plugin-runtime callers\n * and HTTP callers hit the same wall. */\nexport const NOTIFIER_LIFECYCLES = [\"fyi\", \"action\"] as const;\nexport type NotifierLifecycle = (typeof NOTIFIER_LIFECYCLES)[number];\n\n/** Severity drives badge color (gray / amber / red, worst-wins) and\n * in a future iteration channel routing. Mostly stored verbatim by\n * the engine; the one engine-visible interaction is the rule that\n * `action` lifecycle cannot pair with `info` severity (see\n * `NotifierLifecycle` above). */\nexport const NOTIFIER_SEVERITIES = [\"info\", \"nudge\", \"urgent\"] as const;\nexport type NotifierSeverity = (typeof NOTIFIER_SEVERITIES)[number];\n\nexport interface NotifierEntry<TPluginData = unknown> {\n /** Engine-assigned UUID. Generated synchronously inside `publish()`\n * so the caller can use it before persistence completes. */\n id: string;\n /** Plugin namespace (e.g. `\"encore\"`, `\"debug__system\"`). The\n * engine never inspects it — used only for `listFor()` filtering\n * and as a UI grouping key. */\n pluginPkg: string;\n severity: NotifierSeverity;\n lifecycle?: NotifierLifecycle;\n title: string;\n body?: string;\n /** Optional in-app deep-link target (relative URL). The bell popup\n * routes here on row click, with `&notificationId=<id>` appended\n * so the landing page can identify which entry to clear. The\n * engine doesn't read this — it's a UI hint stored on the entry. */\n navigateTarget?: string;\n /** Opaque to the engine. Round-trips through JSON unchanged; only\n * the originating plugin's UI knows the shape. */\n pluginData?: TPluginData;\n /** ISO-8601 timestamp set at `publish()` time. */\n createdAt: string;\n}\n\n/** A history entry — a `NotifierEntry` after it has been cleared or\n * cancelled, with the terminal type and timestamp recorded. The\n * bell popup's \"History\" section renders these read-only. */\nexport interface NotifierHistoryEntry<TPluginData = unknown> extends NotifierEntry<TPluginData> {\n terminalType: \"cleared\" | \"cancelled\";\n terminalAt: string;\n}\n\n/** Caller-supplied input for `publish()`. The engine fills in `id`\n * and `createdAt`; everything else flows through verbatim.\n *\n * Two publish-time rules apply to `action` lifecycle (see\n * `NotifierLifecycle`):\n *\n * - `navigateTarget` MUST be a non-empty string.\n * - `severity` MUST NOT be `\"info\"`.\n *\n * Violations cause `publish()` to throw. Currently expressed as\n * runtime validation rather than a discriminated-union type, so the\n * fields below are all individually optional / loose at the\n * type-level. */\nexport interface PublishInput<TPluginData = unknown> {\n pluginPkg: string;\n severity: NotifierSeverity;\n title: string;\n body?: string;\n lifecycle?: NotifierLifecycle;\n navigateTarget?: string;\n pluginData?: TPluginData;\n}\n\n/** On-disk shape of `~/mulmoclaude/data/notifier/active.json`. Holds\n * only entries that haven't been cleared or cancelled — the file is\n * a snapshot, not an event log. */\nexport interface NotifierFile {\n entries: Record<string, NotifierEntry>;\n}\n\n/** On-disk shape of `~/mulmoclaude/data/notifier/history.json`. Array\n * of terminated entries newest-first, capped at `HISTORY_CAP` with\n * FIFO eviction (push at index 0, slice from the tail). */\nexport interface NotifierHistoryFile {\n entries: NotifierHistoryEntry[];\n}\n\n/** History size cap. The bell popup's History section renders this\n * many entries; older ones fall off when new terminations land. */\nexport const HISTORY_CAP = 50;\n\n/** Pub-sub event published on the host's notifier channel after every\n * successful state change. Discriminated union — subscribers switch\n * on `type` to keep TypeScript narrowing the rest of the payload.\n *\n * `updated` carries the post-mutation entry — the receiver swaps\n * the matching `id` in their local active set. Reserved for in-\n * place edits via `updateForPlugin`; no history record is written\n * because the entry is still active, just with refreshed content. */\nexport type NotifierEvent =\n | { type: \"published\"; entry: NotifierEntry }\n | { type: \"cleared\"; id: string }\n | { type: \"cancelled\"; id: string }\n | { type: \"updated\"; entry: NotifierEntry };\n","// Low-level file I/O for the notifier. Reads use node:fs directly;\n// writes go through an injected atomic-JSON writer (the host owns the\n// rename-based atomic write so it stays single-sourced with its other\n// writers). Kept separate from `engine.ts` so the path can be\n// overridden in tests without monkey-patching.\n\nimport { promises as fsPromises } from \"node:fs\";\nimport type { NotifierFile, NotifierHistoryFile } from \"./types.js\";\n\n/** Injected atomic JSON writer — the host's `writeJsonAtomic`. */\nexport type WriteJson = (filePath: string, data: unknown) => Promise<void>;\n\nfunction isNotFoundError(err: unknown): boolean {\n return typeof err === \"object\" && err !== null && (err as { code?: unknown }).code === \"ENOENT\";\n}\n\n/** Read the active-entries file. Returns an empty store when the file\n * doesn't exist yet (first ever call on a fresh workspace). Any other\n * read or parse failure throws — the caller has to decide whether to\n * surface or recover, since silently treating \"malformed file\" as\n * \"no entries\" would lose data. */\nexport async function loadActive(filePath: string): Promise<NotifierFile> {\n let text: string;\n try {\n text = await fsPromises.readFile(filePath, \"utf-8\");\n } catch (err) {\n if (isNotFoundError(err)) return { entries: {} };\n throw err;\n }\n const parsed: unknown = JSON.parse(text);\n // `typeof null === \"object\"` and `Array.isArray([])` is also true,\n // so a naive `typeof entries !== \"object\"` check would let\n // `{ entries: null }` and `{ entries: [] }` through, which then\n // crash downstream `engine.get` / `list*` mutations. Reject both\n // shapes here at load time so the failure surfaces as a clear\n // \"malformed file\" error.\n if (typeof parsed !== \"object\" || parsed === null || !(\"entries\" in parsed)) {\n throw new Error(`notifier: malformed active.json at ${filePath}`);\n }\n const { entries } = parsed as { entries: unknown };\n if (typeof entries !== \"object\" || entries === null || Array.isArray(entries)) {\n throw new Error(`notifier: malformed active.json at ${filePath}`);\n }\n return parsed as NotifierFile;\n}\n\n/** Write the active-entries file via the injected atomic writer so a\n * half-written file is never visible to readers. The caller serialises\n * writes (engine.ts queues mutations) — this function makes no\n * concurrency guarantees of its own. */\nexport async function saveActive(writeJson: WriteJson, filePath: string, state: NotifierFile): Promise<void> {\n await writeJson(filePath, state);\n}\n\n/** Read the history file. Empty array on first run. Same parse-error\n * policy as `loadActive`. */\nexport async function loadHistory(filePath: string): Promise<NotifierHistoryFile> {\n let text: string;\n try {\n text = await fsPromises.readFile(filePath, \"utf-8\");\n } catch (err) {\n if (isNotFoundError(err)) return { entries: [] };\n throw err;\n }\n const parsed: unknown = JSON.parse(text);\n if (typeof parsed !== \"object\" || parsed === null || !(\"entries\" in parsed) || !Array.isArray((parsed as { entries: unknown }).entries)) {\n throw new Error(`notifier: malformed history.json at ${filePath}`);\n }\n return parsed as NotifierHistoryFile;\n}\n\nexport async function saveHistory(writeJson: WriteJson, filePath: string, state: NotifierHistoryFile): Promise<void> {\n await writeJson(filePath, state);\n}\n","// Publish-input validation — pure, dependency-free. Shared by\n// `engine.publish` (throws on error) and the host's HTTP route\n// (returns 400 on error). Single source of truth so plugin-runtime\n// callers and HTTP callers can't drift.\n\nimport type { PublishInput } from \"./types.js\";\n\n/** Hard caps on publish-input fields. The engine reads each entry on\n * every list/get call (no in-memory cache), so unbounded fields hurt\n * every reader. Caps chosen to be generous for legitimate UX copy\n * while bounding active.json growth: a notification fundamentally is\n * a short blurb, not a document. */\nexport const NOTIFIER_LIMITS = {\n titleMax: 200,\n bodyMax: 4000,\n navigateTargetMax: 1000,\n pluginDataMaxBytes: 16 * 1024,\n} as const;\n\nfunction validateTitle(title: string): string | null {\n if (typeof title !== \"string\" || title.length === 0) return \"title must be a non-empty string\";\n if (title.length > NOTIFIER_LIMITS.titleMax) return `title exceeds max length of ${NOTIFIER_LIMITS.titleMax} chars`;\n return null;\n}\n\nfunction validateBody(body: string | undefined): string | null {\n if (body === undefined) return null;\n if (body.length > NOTIFIER_LIMITS.bodyMax) return `body exceeds max length of ${NOTIFIER_LIMITS.bodyMax} chars`;\n return null;\n}\n\nfunction validateNavigateTarget(target: string | undefined): string | null {\n if (target === undefined) return null;\n if (target.length === 0) return \"navigateTarget must be a non-empty relative path when set\";\n if (target.length > NOTIFIER_LIMITS.navigateTargetMax) {\n return `navigateTarget exceeds max length of ${NOTIFIER_LIMITS.navigateTargetMax} chars`;\n }\n // Must be a same-origin relative path. Reject schemes\n // (`javascript:`, `https://...`) and scheme-relative URLs\n // (`//evil.com/...`, which an `<a href>` would resolve to the\n // attacker's origin). One leading \"/\" only.\n if (!target.startsWith(\"/\") || target.startsWith(\"//\")) {\n return \"navigateTarget must be a relative path beginning with a single '/' (no scheme, no '//')\";\n }\n return null;\n}\n\nfunction validatePluginData(pluginData: unknown): string | null {\n if (pluginData === undefined) return null;\n let serialized: string | undefined;\n try {\n serialized = JSON.stringify(pluginData);\n } catch (err) {\n return `pluginData is not JSON-serialisable: ${String(err)}`;\n }\n // `JSON.stringify` returns `undefined` for non-serialisable roots\n // (e.g. a bare function or symbol). Treat that as a serialisation\n // failure so it doesn't slip through as an empty-string size.\n if (typeof serialized !== \"string\") return \"pluginData is not JSON-serialisable\";\n if (serialized.length > NOTIFIER_LIMITS.pluginDataMaxBytes) {\n return `pluginData JSON exceeds ${NOTIFIER_LIMITS.pluginDataMaxBytes} bytes`;\n }\n return null;\n}\n\nfunction validateActionCoherence(input: PublishInput): string | null {\n if (input.lifecycle !== \"action\") return null;\n if (input.severity === \"info\") {\n return \"action lifecycle is incompatible with info severity (use fyi for low-priority pings)\";\n }\n if (typeof input.navigateTarget !== \"string\" || input.navigateTarget.length === 0) {\n return \"action lifecycle requires a non-empty navigateTarget\";\n }\n return null;\n}\n\n/** Validate a `PublishInput`. Returns `null` if OK, or a\n * human-readable error string. Order matters — shape/size errors are\n * reported before lifecycle/severity coherence errors so the message\n * the caller sees points at the most fundamental problem first. */\nexport function validatePublishInput(input: PublishInput): string | null {\n return (\n validateTitle(input.title) ??\n validateBody(input.body) ??\n validateNavigateTarget(input.navigateTarget) ??\n validatePluginData(input.pluginData) ??\n validateActionCoherence(input)\n );\n}\n","// Notifier engine — single-process, two-file (active + history),\n// single-channel. Host-agnostic: file paths, the atomic JSON writer,\n// the pub-sub event sink, and the logger are all injected via\n// `configureNotifier` + `setNotifierFilePaths` so MulmoClaude and\n// MulmoTerminal share one notification engine over their own\n// workspaces and pub-sub fabrics.\n//\n// API surface: publish / clear / cancel / get / listFor / listAll /\n// listHistory (+ plugin-scoped variants). Mutations queue through a\n// writing-flag + waiter-queue coordinator so concurrent callers can't\n// race on the atomic write's rename. Reads bypass the queue (rename\n// atomicity makes half-reads impossible) and trade strict\n// linearisability for simpler code: the contract is \"after\n// `await publish(x)` resolves, subsequent reads see x\" — which holds\n// because `publish` awaits the persist before returning.\n//\n// `clear` / `cancel` push to history *before* removing from active.\n// History persistence is best-effort: if it fails, the active write\n// still wins and the failure is logged. Active is the source of\n// truth; history is an audit aid.\n\nimport { randomUUID } from \"node:crypto\";\nimport { loadActive, loadHistory, saveActive, saveHistory, type WriteJson } from \"./store.js\";\nimport { validatePublishInput } from \"./validate.js\";\nimport {\n HISTORY_CAP,\n type NotifierEntry,\n type NotifierEvent,\n type NotifierFile,\n type NotifierHistoryEntry,\n type NotifierSeverity,\n type PublishInput,\n} from \"./types.js\";\n\nexport { NOTIFIER_LIMITS, validatePublishInput } from \"./validate.js\";\n\n// ── Dependency injection ──────────────────────────────────────────\n\n/** Minimal logger the engine needs. The host passes its structured\n * logger; absent one, failures are swallowed (the engine never throws\n * on a fan-out/persist-best-effort path). */\nexport interface NotifierLogger {\n warn: (message: string, data?: Record<string, unknown>) => void;\n error: (message: string, data?: Record<string, unknown>) => void;\n}\n\nexport interface NotifierConfig {\n /** Atomic JSON writer (the host's `writeJsonAtomic`). */\n writeJson: WriteJson;\n /** Fan-out sink — the host binds this to `pubsub.publish(channel, event)`. */\n publishEvent: (event: NotifierEvent) => void;\n /** Optional logger. */\n log?: NotifierLogger;\n}\n\nconst NOOP_LOG: NotifierLogger = { warn: () => {}, error: () => {} };\n\nlet config: NotifierConfig | null = null;\nlet activeFilePath = \"\";\nlet historyFilePath = \"\";\n\nfunction logger(): NotifierLogger {\n return config?.log ?? NOOP_LOG;\n}\n\n/** Wire the engine's I/O deps. Call once at startup, before the first\n * mutation. Does NOT set file paths — those are set independently via\n * `setNotifierFilePaths` so a host can bind production paths at module\n * load and a test can override them without re-supplying the deps. */\nexport function configureNotifier(injected: NotifierConfig): void {\n config = injected;\n}\n\n// ── In-process event listeners ────────────────────────────────────\n//\n// Separate from the socket.io pubsub so server-side adapters (macOS\n// push, future Encore) can react to state changes without going\n// through a websocket round-trip. The host's pubsub is fan-out-only\n// with no server-side subscribe, so this listener registry is the\n// in-process equivalent. Listeners run synchronously inside `emit`,\n// before the pubsub fan-out.\n\ntype NotifierEventListener = (event: NotifierEvent) => void;\nconst listeners: NotifierEventListener[] = [];\n\n/** Register an in-process listener for engine events. Returns an\n * unsubscribe function the caller can use during teardown. */\nexport function onEvent(listener: NotifierEventListener): () => void {\n listeners.push(listener);\n return () => {\n const idx = listeners.indexOf(listener);\n if (idx >= 0) listeners.splice(idx, 1);\n };\n}\n\nfunction emit(event: NotifierEvent): void {\n // In-process fan-out first. Each listener is wrapped: a throwing\n // listener must not poison the rest, and must not propagate out of\n // `processBatch` and strand the still-unsettled waiters (their\n // resolve/reject is called *after* this emit loop). Fan-out is\n // best-effort by contract — losing one subscriber must not lose\n // the write that already committed.\n for (const listener of listeners) {\n try {\n listener(event);\n } catch (err) {\n logger().error(\"in-process listener failed\", { type: event.type, error: String(err) });\n }\n }\n if (!config) {\n logger().warn(\"emit before init\", { type: event.type });\n return;\n }\n try {\n config.publishEvent(event);\n } catch (err) {\n logger().error(\"emit failed\", { type: event.type, error: String(err) });\n }\n}\n\n// ── Write coordinator ─────────────────────────────────────────────\n\n/** A mutation function applied to the in-memory state object during\n * drain. Returns either:\n *\n * - `null` — no state change (e.g., `clear` on an unknown id).\n * The drainer skips the disk write and the emit if every\n * mutation in a batch returned `null`.\n * - `{ event, historyEntry? }` — state changed. The drainer emits\n * the event after the active write succeeds, and prepends\n * `historyEntry` to history (best-effort) when present.\n *\n * Mutations MUST NOT modify state when returning `null`. Violating\n * this invariant produces a write skip with stale on-disk state. */\ntype MutationOutcome = { event: NotifierEvent; historyEntry?: NotifierHistoryEntry } | null;\ntype Mutation = (state: NotifierFile) => MutationOutcome;\n\ninterface Waiter {\n mutate: Mutation;\n resolve: () => void;\n reject: (err: unknown) => void;\n}\n\ntype MutationResult = { ok: true; outcome: MutationOutcome } | { ok: false; error: unknown };\n\nlet writing = false;\nlet waiters: Waiter[] = [];\n\n/** Point the engine at its active/history files. Resets the write\n * queue, so callers must not have in-flight mutations. The host calls\n * this once with the workspace paths; tests call it per-case with temp\n * files. */\nexport function setNotifierFilePaths(paths: { active: string; history: string }): void {\n activeFilePath = paths.active;\n historyFilePath = paths.history;\n writing = false;\n waiters = [];\n}\n\n/** Test-only: clear config + queue so each suite starts clean. */\nexport function resetNotifier(): void {\n config = null;\n activeFilePath = \"\";\n historyFilePath = \"\";\n writing = false;\n waiters = [];\n listeners.length = 0;\n}\n\nfunction requireWriteJson(): WriteJson {\n if (!config) throw new Error(\"notifier: configureNotifier() not called\");\n return config.writeJson;\n}\n\nfunction applyBatchMutations(batch: Waiter[], state: NotifierFile): MutationResult[] {\n return batch.map((waiter) => {\n try {\n return { ok: true, outcome: waiter.mutate(state) };\n } catch (err) {\n return { ok: false, error: err };\n }\n });\n}\n\nfunction collectEvents(results: MutationResult[]): NotifierEvent[] {\n const events: NotifierEvent[] = [];\n for (const result of results) {\n if (result.ok && result.outcome !== null) events.push(result.outcome.event);\n }\n return events;\n}\n\nfunction collectHistoryEntries(results: MutationResult[]): NotifierHistoryEntry[] {\n const entries: NotifierHistoryEntry[] = [];\n for (const result of results) {\n if (result.ok && result.outcome !== null && result.outcome.historyEntry) {\n entries.push(result.outcome.historyEntry);\n }\n }\n return entries;\n}\n\nfunction settleBatch(batch: Waiter[], results: MutationResult[]): void {\n // Resolves come AFTER any emits so subscribers see the event\n // before the caller's `await` returns.\n for (let index = 0; index < batch.length; index += 1) {\n const result = results[index];\n if (result.ok) batch[index].resolve();\n else batch[index].reject(result.error);\n }\n}\n\nfunction rejectBatch(batch: Waiter[], err: unknown): void {\n for (const waiter of batch) waiter.reject(err);\n}\n\nasync function persistHistory(newEntries: NotifierHistoryEntry[]): Promise<void> {\n const existing = await loadHistory(historyFilePath);\n // Newest-first ordering: a batch contains terminations in arrival\n // order; we want the last one to land at index 0 of history.\n const merged = [...newEntries.slice().reverse(), ...existing.entries].slice(0, HISTORY_CAP);\n await saveHistory(requireWriteJson(), historyFilePath, { entries: merged });\n}\n\nasync function processBatch(batch: Waiter[]): Promise<void> {\n let state: NotifierFile;\n try {\n state = await loadActive(activeFilePath);\n } catch (err) {\n logger().error(\"load failed\", { error: String(err) });\n rejectBatch(batch, err);\n return;\n }\n const results = applyBatchMutations(batch, state);\n const events = collectEvents(results);\n const historyEntries = collectHistoryEntries(results);\n\n if (events.length > 0) {\n try {\n await saveActive(requireWriteJson(), activeFilePath, state);\n } catch (err) {\n logger().error(\"active write failed\", { error: String(err) });\n rejectBatch(batch, err);\n return;\n }\n if (historyEntries.length > 0) {\n // Best-effort: active is the source of truth, history is an\n // audit aid. A failed history write is logged but doesn't\n // unwind the active commit.\n try {\n await persistHistory(historyEntries);\n } catch (err) {\n logger().error(\"history write failed\", { error: String(err) });\n }\n }\n for (const event of events) emit(event);\n }\n settleBatch(batch, results);\n}\n\nasync function drain(): Promise<void> {\n writing = true;\n try {\n while (waiters.length > 0) {\n const batch = waiters;\n waiters = [];\n await processBatch(batch);\n }\n } finally {\n writing = false;\n }\n}\n\nfunction enqueue(mutate: Mutation): Promise<void> {\n return new Promise<void>((resolve, reject) => {\n waiters.push({ mutate, resolve, reject });\n if (!writing) void drain();\n });\n}\n\nfunction removeEntry(state: NotifierFile, entryId: string): NotifierFile[\"entries\"] {\n // Object-rest excludes the key without invoking `delete`.\n const { [entryId]: __removed, ...remaining } = state.entries;\n return remaining;\n}\n\nfunction buildHistoryEntry(entry: NotifierEntry, terminalType: \"cleared\" | \"cancelled\"): NotifierHistoryEntry {\n return { ...entry, terminalType, terminalAt: new Date().toISOString() };\n}\n\n// ── Public API ────────────────────────────────────────────────────\n\nexport async function publish<TPluginData = unknown>(input: PublishInput<TPluginData>): Promise<{ id: string }> {\n // Validate at the engine boundary so plugin-runtime callers and\n // HTTP callers hit the same wall.\n const validationError = validatePublishInput(input as PublishInput);\n if (validationError) {\n throw new Error(`notifier.publish: ${validationError}`);\n }\n const entryId = randomUUID();\n const entry: NotifierEntry<TPluginData> = {\n id: entryId,\n pluginPkg: input.pluginPkg,\n severity: input.severity,\n lifecycle: input.lifecycle,\n title: input.title,\n body: input.body,\n navigateTarget: input.navigateTarget,\n pluginData: input.pluginData,\n createdAt: new Date().toISOString(),\n };\n await enqueue((state) => {\n state.entries[entryId] = entry as NotifierEntry;\n return { event: { type: \"published\", entry: entry as NotifierEntry } };\n });\n return { id: entryId };\n}\n\nexport async function clear(entryId: string): Promise<void> {\n await enqueue((state) => {\n const entry = state.entries[entryId];\n if (!entry) return null;\n state.entries = removeEntry(state, entryId);\n return {\n event: { type: \"cleared\", id: entryId },\n historyEntry: buildHistoryEntry(entry, \"cleared\"),\n };\n });\n}\n\nexport async function cancel(entryId: string): Promise<void> {\n await enqueue((state) => {\n const entry = state.entries[entryId];\n if (!entry) return null;\n state.entries = removeEntry(state, entryId);\n return {\n event: { type: \"cancelled\", id: entryId },\n historyEntry: buildHistoryEntry(entry, \"cancelled\"),\n };\n });\n}\n\n/** In-place update for an active entry. Only the fields present on\n * `patch` are rewritten; `id`, `pluginPkg`, `lifecycle`, and\n * `createdAt` stay fixed. Emits a single `\"updated\"` event with the\n * post-mutation entry — no history record is written because the\n * entry is still active, just with refreshed content.\n *\n * No-ops (no throw) when the id is unknown, the entry belongs to a\n * different plugin, or the merged shape would violate\n * `validatePublishInput`. The silent skip matches `clearForPlugin`'s\n * isolation semantics; validation failures are logged for diagnosis. */\nexport async function updateForPlugin<TPluginData = unknown>(\n pluginPkg: string,\n entryId: string,\n patch: {\n severity?: NotifierSeverity;\n title?: string;\n body?: string;\n navigateTarget?: string;\n pluginData?: TPluginData;\n },\n): Promise<void> {\n await enqueue((state) => {\n const entry = state.entries[entryId];\n if (!entry) return null;\n if (entry.pluginPkg !== pluginPkg) return null;\n const next: NotifierEntry = {\n ...entry,\n ...(patch.severity !== undefined ? { severity: patch.severity } : {}),\n ...(patch.title !== undefined ? { title: patch.title } : {}),\n ...(patch.body !== undefined ? { body: patch.body } : {}),\n ...(patch.navigateTarget !== undefined ? { navigateTarget: patch.navigateTarget } : {}),\n ...(patch.pluginData !== undefined ? { pluginData: patch.pluginData } : {}),\n };\n // Re-validate the merged shape so an update can't degrade the\n // entry below publish-time invariants.\n const validationError = validatePublishInput({\n pluginPkg: next.pluginPkg,\n severity: next.severity,\n title: next.title,\n body: next.body,\n lifecycle: next.lifecycle,\n navigateTarget: next.navigateTarget,\n pluginData: next.pluginData,\n });\n if (validationError) {\n logger().warn(\"update rejected by validation\", { entryId, pluginPkg, error: validationError });\n return null;\n }\n state.entries[entryId] = next;\n return { event: { type: \"updated\", entry: next } };\n });\n}\n\n/** Plugin-scoped point lookup. Returns the entry by id, but only if it\n * belongs to the caller's plugin; otherwise undefined. Cross-plugin\n * reads return undefined for isolation — same property as\n * `clearForPlugin` / `updateForPlugin`. */\nexport async function getForPlugin(pluginPkg: string, entryId: string): Promise<NotifierEntry | undefined> {\n const state = await loadActive(activeFilePath);\n const entry = state.entries[entryId];\n if (!entry) return undefined;\n if (entry.pluginPkg !== pluginPkg) return undefined;\n return entry;\n}\n\n/** Plugin-scoped clear. Same as `clear` but no-ops if the entry's\n * `pluginPkg` doesn't match the caller's, so a plugin can't dismiss\n * another plugin's notification by guessing or scraping its id. */\nexport async function clearForPlugin(pluginPkg: string, entryId: string): Promise<void> {\n await enqueue((state) => {\n const entry = state.entries[entryId];\n if (!entry) return null;\n if (entry.pluginPkg !== pluginPkg) return null;\n state.entries = removeEntry(state, entryId);\n return {\n event: { type: \"cleared\", id: entryId },\n historyEntry: buildHistoryEntry(entry, \"cleared\"),\n };\n });\n}\n\nexport async function get(entryId: string): Promise<NotifierEntry | undefined> {\n const state = await loadActive(activeFilePath);\n return state.entries[entryId];\n}\n\nexport async function listFor(pluginPkg: string): Promise<NotifierEntry[]> {\n const state = await loadActive(activeFilePath);\n return Object.values(state.entries).filter((entry) => entry.pluginPkg === pluginPkg);\n}\n\nexport async function listAll(): Promise<NotifierEntry[]> {\n const state = await loadActive(activeFilePath);\n return Object.values(state.entries);\n}\n\nexport async function listHistory(): Promise<NotifierHistoryEntry[]> {\n const state = await loadHistory(historyFilePath);\n return state.entries;\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"notifier-ChpY0XrY.js","names":[],"sources":["../src/notifier/types.ts","../src/notifier/store.ts","../src/notifier/validate.ts","../src/notifier/engine.ts"],"sourcesContent":["// Notifier value types. Kept dependency-free (no node, no fs, no\n// pubsub) so the host's API-route layer and any future browser\n// consumer can validate inbound payloads against the same enum\n// constants the engine accepts without pulling in the engine's I/O.\n\n/** Two notification shapes, distinguished by who fires the close call:\n *\n * `fyi` — informational. The host (bell panel) clears it when the\n * user dismisses the row. No deep-link target.\n * `action` — pending obligation. The plugin clears it when the\n * underlying domain state changes (the user paid the tax,\n * viewed the digest, etc.). The bell row navigates to\n * `navigateTarget` on click.\n *\n * The engine reads `lifecycle` only to enforce two publish-time rules\n * (everything downstream — pubsub fan-out, persistence, history — is\n * lifecycle-blind):\n *\n * 1. `action` requires a non-empty `navigateTarget`. Without one,\n * clicking the row does nothing and the entry is a degraded fyi.\n * 2. `action` cannot use `info` severity. A low-priority obligation\n * is incoherent — fyi if it's a ping, `nudge`/`urgent` if it's a\n * real obligation worth a landing page.\n *\n * Both rules are mirrored in the HTTP layer so plugin-runtime callers\n * and HTTP callers hit the same wall. */\nexport const NOTIFIER_LIFECYCLES = [\"fyi\", \"action\"] as const;\nexport type NotifierLifecycle = (typeof NOTIFIER_LIFECYCLES)[number];\n\n/** Severity drives badge color (gray / amber / red, worst-wins) and\n * in a future iteration channel routing. Mostly stored verbatim by\n * the engine; the one engine-visible interaction is the rule that\n * `action` lifecycle cannot pair with `info` severity (see\n * `NotifierLifecycle` above). */\nexport const NOTIFIER_SEVERITIES = [\"info\", \"nudge\", \"urgent\"] as const;\nexport type NotifierSeverity = (typeof NOTIFIER_SEVERITIES)[number];\n\nexport interface NotifierEntry<TPluginData = unknown> {\n /** Engine-assigned UUID. Generated synchronously inside `publish()`\n * so the caller can use it before persistence completes. */\n id: string;\n /** Plugin namespace (e.g. `\"encore\"`, `\"debug__system\"`). The\n * engine never inspects it — used only for `listFor()` filtering\n * and as a UI grouping key. */\n pluginPkg: string;\n severity: NotifierSeverity;\n lifecycle?: NotifierLifecycle;\n title: string;\n body?: string;\n /** Optional in-app deep-link target (relative URL). The bell popup\n * routes here on row click, with `&notificationId=<id>` appended\n * so the landing page can identify which entry to clear. The\n * engine doesn't read this — it's a UI hint stored on the entry. */\n navigateTarget?: string;\n /** Opaque to the engine. Round-trips through JSON unchanged; only\n * the originating plugin's UI knows the shape. */\n pluginData?: TPluginData;\n /** ISO-8601 timestamp set at `publish()` time. */\n createdAt: string;\n}\n\n/** A history entry — a `NotifierEntry` after it has been cleared or\n * cancelled, with the terminal type and timestamp recorded. The\n * bell popup's \"History\" section renders these read-only. */\nexport interface NotifierHistoryEntry<TPluginData = unknown> extends NotifierEntry<TPluginData> {\n terminalType: \"cleared\" | \"cancelled\";\n terminalAt: string;\n}\n\n/** Caller-supplied input for `publish()`. The engine fills in `id`\n * and `createdAt`; everything else flows through verbatim.\n *\n * Two publish-time rules apply to `action` lifecycle (see\n * `NotifierLifecycle`):\n *\n * - `navigateTarget` MUST be a non-empty string.\n * - `severity` MUST NOT be `\"info\"`.\n *\n * Violations cause `publish()` to throw. Currently expressed as\n * runtime validation rather than a discriminated-union type, so the\n * fields below are all individually optional / loose at the\n * type-level. */\nexport interface PublishInput<TPluginData = unknown> {\n pluginPkg: string;\n severity: NotifierSeverity;\n title: string;\n body?: string;\n lifecycle?: NotifierLifecycle;\n navigateTarget?: string;\n pluginData?: TPluginData;\n}\n\n/** On-disk shape of `~/mulmoclaude/data/notifier/active.json`. Holds\n * only entries that haven't been cleared or cancelled — the file is\n * a snapshot, not an event log. */\nexport interface NotifierFile {\n entries: Record<string, NotifierEntry>;\n}\n\n/** On-disk shape of `~/mulmoclaude/data/notifier/history.json`. Array\n * of terminated entries newest-first, capped at `HISTORY_CAP` with\n * FIFO eviction (push at index 0, slice from the tail). */\nexport interface NotifierHistoryFile {\n entries: NotifierHistoryEntry[];\n}\n\n/** History size cap. The bell popup's History section renders this\n * many entries; older ones fall off when new terminations land. */\nexport const HISTORY_CAP = 50;\n\n/** Pub-sub event published on the host's notifier channel after every\n * successful state change. Discriminated union — subscribers switch\n * on `type` to keep TypeScript narrowing the rest of the payload.\n *\n * `updated` carries the post-mutation entry — the receiver swaps\n * the matching `id` in their local active set. Reserved for in-\n * place edits via `updateForPlugin`; no history record is written\n * because the entry is still active, just with refreshed content. */\nexport type NotifierEvent =\n { type: \"published\"; entry: NotifierEntry } | { type: \"cleared\"; id: string } | { type: \"cancelled\"; id: string } | { type: \"updated\"; entry: NotifierEntry };\n","// Low-level file I/O for the notifier. Reads use node:fs directly;\n// writes go through an injected atomic-JSON writer (the host owns the\n// rename-based atomic write so it stays single-sourced with its other\n// writers). Kept separate from `engine.ts` so the path can be\n// overridden in tests without monkey-patching.\n\nimport { promises as fsPromises } from \"node:fs\";\nimport type { NotifierFile, NotifierHistoryFile } from \"./types.js\";\n\n/** Injected atomic JSON writer — the host's `writeJsonAtomic`. */\nexport type WriteJson = (filePath: string, data: unknown) => Promise<void>;\n\nfunction isNotFoundError(err: unknown): boolean {\n return typeof err === \"object\" && err !== null && (err as { code?: unknown }).code === \"ENOENT\";\n}\n\n/** Read the active-entries file. Returns an empty store when the file\n * doesn't exist yet (first ever call on a fresh workspace). Any other\n * read or parse failure throws — the caller has to decide whether to\n * surface or recover, since silently treating \"malformed file\" as\n * \"no entries\" would lose data. */\nexport async function loadActive(filePath: string): Promise<NotifierFile> {\n let text: string;\n try {\n text = await fsPromises.readFile(filePath, \"utf-8\");\n } catch (err) {\n if (isNotFoundError(err)) return { entries: {} };\n throw err;\n }\n const parsed: unknown = JSON.parse(text);\n // `typeof null === \"object\"` and `Array.isArray([])` is also true,\n // so a naive `typeof entries !== \"object\"` check would let\n // `{ entries: null }` and `{ entries: [] }` through, which then\n // crash downstream `engine.get` / `list*` mutations. Reject both\n // shapes here at load time so the failure surfaces as a clear\n // \"malformed file\" error.\n if (typeof parsed !== \"object\" || parsed === null || !(\"entries\" in parsed)) {\n throw new Error(`notifier: malformed active.json at ${filePath}`);\n }\n const { entries } = parsed as { entries: unknown };\n if (typeof entries !== \"object\" || entries === null || Array.isArray(entries)) {\n throw new Error(`notifier: malformed active.json at ${filePath}`);\n }\n return parsed as NotifierFile;\n}\n\n/** Write the active-entries file via the injected atomic writer so a\n * half-written file is never visible to readers. The caller serialises\n * writes (engine.ts queues mutations) — this function makes no\n * concurrency guarantees of its own. */\nexport async function saveActive(writeJson: WriteJson, filePath: string, state: NotifierFile): Promise<void> {\n await writeJson(filePath, state);\n}\n\n/** Read the history file. Empty array on first run. Same parse-error\n * policy as `loadActive`. */\nexport async function loadHistory(filePath: string): Promise<NotifierHistoryFile> {\n let text: string;\n try {\n text = await fsPromises.readFile(filePath, \"utf-8\");\n } catch (err) {\n if (isNotFoundError(err)) return { entries: [] };\n throw err;\n }\n const parsed: unknown = JSON.parse(text);\n if (typeof parsed !== \"object\" || parsed === null || !(\"entries\" in parsed) || !Array.isArray((parsed as { entries: unknown }).entries)) {\n throw new Error(`notifier: malformed history.json at ${filePath}`);\n }\n return parsed as NotifierHistoryFile;\n}\n\nexport async function saveHistory(writeJson: WriteJson, filePath: string, state: NotifierHistoryFile): Promise<void> {\n await writeJson(filePath, state);\n}\n","// Publish-input validation — pure, dependency-free. Shared by\n// `engine.publish` (throws on error) and the host's HTTP route\n// (returns 400 on error). Single source of truth so plugin-runtime\n// callers and HTTP callers can't drift.\n\nimport type { PublishInput } from \"./types.js\";\n\n/** Hard caps on publish-input fields. The engine reads each entry on\n * every list/get call (no in-memory cache), so unbounded fields hurt\n * every reader. Caps chosen to be generous for legitimate UX copy\n * while bounding active.json growth: a notification fundamentally is\n * a short blurb, not a document. */\nexport const NOTIFIER_LIMITS = {\n titleMax: 200,\n bodyMax: 4000,\n navigateTargetMax: 1000,\n pluginDataMaxBytes: 16 * 1024,\n} as const;\n\nfunction validateTitle(title: string): string | null {\n if (typeof title !== \"string\" || title.length === 0) return \"title must be a non-empty string\";\n if (title.length > NOTIFIER_LIMITS.titleMax) return `title exceeds max length of ${NOTIFIER_LIMITS.titleMax} chars`;\n return null;\n}\n\nfunction validateBody(body: string | undefined): string | null {\n if (body === undefined) return null;\n if (body.length > NOTIFIER_LIMITS.bodyMax) return `body exceeds max length of ${NOTIFIER_LIMITS.bodyMax} chars`;\n return null;\n}\n\nfunction validateNavigateTarget(target: string | undefined): string | null {\n if (target === undefined) return null;\n if (target.length === 0) return \"navigateTarget must be a non-empty relative path when set\";\n if (target.length > NOTIFIER_LIMITS.navigateTargetMax) {\n return `navigateTarget exceeds max length of ${NOTIFIER_LIMITS.navigateTargetMax} chars`;\n }\n // Must be a same-origin relative path. Reject schemes\n // (`javascript:`, `https://...`) and scheme-relative URLs\n // (`//evil.com/...`, which an `<a href>` would resolve to the\n // attacker's origin). One leading \"/\" only.\n if (!target.startsWith(\"/\") || target.startsWith(\"//\")) {\n return \"navigateTarget must be a relative path beginning with a single '/' (no scheme, no '//')\";\n }\n return null;\n}\n\nfunction validatePluginData(pluginData: unknown): string | null {\n if (pluginData === undefined) return null;\n let serialized: string | undefined;\n try {\n serialized = JSON.stringify(pluginData);\n } catch (err) {\n return `pluginData is not JSON-serialisable: ${String(err)}`;\n }\n // `JSON.stringify` returns `undefined` for non-serialisable roots\n // (e.g. a bare function or symbol). Treat that as a serialisation\n // failure so it doesn't slip through as an empty-string size.\n if (typeof serialized !== \"string\") return \"pluginData is not JSON-serialisable\";\n if (serialized.length > NOTIFIER_LIMITS.pluginDataMaxBytes) {\n return `pluginData JSON exceeds ${NOTIFIER_LIMITS.pluginDataMaxBytes} bytes`;\n }\n return null;\n}\n\nfunction validateActionCoherence(input: PublishInput): string | null {\n if (input.lifecycle !== \"action\") return null;\n if (input.severity === \"info\") {\n return \"action lifecycle is incompatible with info severity (use fyi for low-priority pings)\";\n }\n if (typeof input.navigateTarget !== \"string\" || input.navigateTarget.length === 0) {\n return \"action lifecycle requires a non-empty navigateTarget\";\n }\n return null;\n}\n\n/** Validate a `PublishInput`. Returns `null` if OK, or a\n * human-readable error string. Order matters — shape/size errors are\n * reported before lifecycle/severity coherence errors so the message\n * the caller sees points at the most fundamental problem first. */\nexport function validatePublishInput(input: PublishInput): string | null {\n return (\n validateTitle(input.title) ??\n validateBody(input.body) ??\n validateNavigateTarget(input.navigateTarget) ??\n validatePluginData(input.pluginData) ??\n validateActionCoherence(input)\n );\n}\n","// Notifier engine — single-process, two-file (active + history),\n// single-channel. Host-agnostic: file paths, the atomic JSON writer,\n// the pub-sub event sink, and the logger are all injected via\n// `configureNotifier` + `setNotifierFilePaths` so MulmoClaude and\n// MulmoTerminal share one notification engine over their own\n// workspaces and pub-sub fabrics.\n//\n// API surface: publish / clear / cancel / get / listFor / listAll /\n// listHistory (+ plugin-scoped variants). Mutations queue through a\n// writing-flag + waiter-queue coordinator so concurrent callers can't\n// race on the atomic write's rename. Reads bypass the queue (rename\n// atomicity makes half-reads impossible) and trade strict\n// linearisability for simpler code: the contract is \"after\n// `await publish(x)` resolves, subsequent reads see x\" — which holds\n// because `publish` awaits the persist before returning.\n//\n// `clear` / `cancel` push to history *before* removing from active.\n// History persistence is best-effort: if it fails, the active write\n// still wins and the failure is logged. Active is the source of\n// truth; history is an audit aid.\n\nimport { randomUUID } from \"node:crypto\";\nimport { loadActive, loadHistory, saveActive, saveHistory, type WriteJson } from \"./store.js\";\nimport { validatePublishInput } from \"./validate.js\";\nimport {\n HISTORY_CAP,\n type NotifierEntry,\n type NotifierEvent,\n type NotifierFile,\n type NotifierHistoryEntry,\n type NotifierSeverity,\n type PublishInput,\n} from \"./types.js\";\n\nexport { NOTIFIER_LIMITS, validatePublishInput } from \"./validate.js\";\n\n// ── Dependency injection ──────────────────────────────────────────\n\n/** Minimal logger the engine needs. The host passes its structured\n * logger; absent one, failures are swallowed (the engine never throws\n * on a fan-out/persist-best-effort path). */\nexport interface NotifierLogger {\n warn: (message: string, data?: Record<string, unknown>) => void;\n error: (message: string, data?: Record<string, unknown>) => void;\n}\n\nexport interface NotifierConfig {\n /** Atomic JSON writer (the host's `writeJsonAtomic`). */\n writeJson: WriteJson;\n /** Fan-out sink — the host binds this to `pubsub.publish(channel, event)`. */\n publishEvent: (event: NotifierEvent) => void;\n /** Optional logger. */\n log?: NotifierLogger;\n}\n\nconst NOOP_LOG: NotifierLogger = { warn: () => {}, error: () => {} };\n\nlet config: NotifierConfig | null = null;\nlet activeFilePath = \"\";\nlet historyFilePath = \"\";\n\nfunction logger(): NotifierLogger {\n return config?.log ?? NOOP_LOG;\n}\n\n/** Wire the engine's I/O deps. Call once at startup, before the first\n * mutation. Does NOT set file paths — those are set independently via\n * `setNotifierFilePaths` so a host can bind production paths at module\n * load and a test can override them without re-supplying the deps. */\nexport function configureNotifier(injected: NotifierConfig): void {\n config = injected;\n}\n\n// ── In-process event listeners ────────────────────────────────────\n//\n// Separate from the socket.io pubsub so server-side adapters (macOS\n// push, future Encore) can react to state changes without going\n// through a websocket round-trip. The host's pubsub is fan-out-only\n// with no server-side subscribe, so this listener registry is the\n// in-process equivalent. Listeners run synchronously inside `emit`,\n// before the pubsub fan-out.\n\ntype NotifierEventListener = (event: NotifierEvent) => void;\nconst listeners: NotifierEventListener[] = [];\n\n/** Register an in-process listener for engine events. Returns an\n * unsubscribe function the caller can use during teardown. */\nexport function onEvent(listener: NotifierEventListener): () => void {\n listeners.push(listener);\n return () => {\n const idx = listeners.indexOf(listener);\n if (idx >= 0) listeners.splice(idx, 1);\n };\n}\n\nfunction emit(event: NotifierEvent): void {\n // In-process fan-out first. Each listener is wrapped: a throwing\n // listener must not poison the rest, and must not propagate out of\n // `processBatch` and strand the still-unsettled waiters (their\n // resolve/reject is called *after* this emit loop). Fan-out is\n // best-effort by contract — losing one subscriber must not lose\n // the write that already committed.\n for (const listener of listeners) {\n try {\n listener(event);\n } catch (err) {\n logger().error(\"in-process listener failed\", { type: event.type, error: String(err) });\n }\n }\n if (!config) {\n logger().warn(\"emit before init\", { type: event.type });\n return;\n }\n try {\n config.publishEvent(event);\n } catch (err) {\n logger().error(\"emit failed\", { type: event.type, error: String(err) });\n }\n}\n\n// ── Write coordinator ─────────────────────────────────────────────\n\n/** A mutation function applied to the in-memory state object during\n * drain. Returns either:\n *\n * - `null` — no state change (e.g., `clear` on an unknown id).\n * The drainer skips the disk write and the emit if every\n * mutation in a batch returned `null`.\n * - `{ event, historyEntry? }` — state changed. The drainer emits\n * the event after the active write succeeds, and prepends\n * `historyEntry` to history (best-effort) when present.\n *\n * Mutations MUST NOT modify state when returning `null`. Violating\n * this invariant produces a write skip with stale on-disk state. */\ntype MutationOutcome = { event: NotifierEvent; historyEntry?: NotifierHistoryEntry } | null;\ntype Mutation = (state: NotifierFile) => MutationOutcome;\n\ninterface Waiter {\n mutate: Mutation;\n resolve: () => void;\n reject: (err: unknown) => void;\n}\n\ntype MutationResult = { ok: true; outcome: MutationOutcome } | { ok: false; error: unknown };\n\nlet writing = false;\nlet waiters: Waiter[] = [];\n\n/** Point the engine at its active/history files. Resets the write\n * queue, so callers must not have in-flight mutations. The host calls\n * this once with the workspace paths; tests call it per-case with temp\n * files. */\nexport function setNotifierFilePaths(paths: { active: string; history: string }): void {\n activeFilePath = paths.active;\n historyFilePath = paths.history;\n writing = false;\n waiters = [];\n}\n\n/** Test-only: clear config + queue so each suite starts clean. */\nexport function resetNotifier(): void {\n config = null;\n activeFilePath = \"\";\n historyFilePath = \"\";\n writing = false;\n waiters = [];\n listeners.length = 0;\n}\n\nfunction requireWriteJson(): WriteJson {\n if (!config) throw new Error(\"notifier: configureNotifier() not called\");\n return config.writeJson;\n}\n\nfunction applyBatchMutations(batch: Waiter[], state: NotifierFile): MutationResult[] {\n return batch.map((waiter) => {\n try {\n return { ok: true, outcome: waiter.mutate(state) };\n } catch (err) {\n return { ok: false, error: err };\n }\n });\n}\n\nfunction collectEvents(results: MutationResult[]): NotifierEvent[] {\n const events: NotifierEvent[] = [];\n for (const result of results) {\n if (result.ok && result.outcome !== null) events.push(result.outcome.event);\n }\n return events;\n}\n\nfunction collectHistoryEntries(results: MutationResult[]): NotifierHistoryEntry[] {\n const entries: NotifierHistoryEntry[] = [];\n for (const result of results) {\n if (result.ok && result.outcome !== null && result.outcome.historyEntry) {\n entries.push(result.outcome.historyEntry);\n }\n }\n return entries;\n}\n\nfunction settleBatch(batch: Waiter[], results: MutationResult[]): void {\n // Resolves come AFTER any emits so subscribers see the event\n // before the caller's `await` returns.\n for (let index = 0; index < batch.length; index += 1) {\n const result = results[index];\n if (result.ok) batch[index].resolve();\n else batch[index].reject(result.error);\n }\n}\n\nfunction rejectBatch(batch: Waiter[], err: unknown): void {\n for (const waiter of batch) waiter.reject(err);\n}\n\nasync function persistHistory(newEntries: NotifierHistoryEntry[]): Promise<void> {\n const existing = await loadHistory(historyFilePath);\n // Newest-first ordering: a batch contains terminations in arrival\n // order; we want the last one to land at index 0 of history.\n const merged = [...newEntries.slice().reverse(), ...existing.entries].slice(0, HISTORY_CAP);\n await saveHistory(requireWriteJson(), historyFilePath, { entries: merged });\n}\n\nasync function processBatch(batch: Waiter[]): Promise<void> {\n let state: NotifierFile;\n try {\n state = await loadActive(activeFilePath);\n } catch (err) {\n logger().error(\"load failed\", { error: String(err) });\n rejectBatch(batch, err);\n return;\n }\n const results = applyBatchMutations(batch, state);\n const events = collectEvents(results);\n const historyEntries = collectHistoryEntries(results);\n\n if (events.length > 0) {\n try {\n await saveActive(requireWriteJson(), activeFilePath, state);\n } catch (err) {\n logger().error(\"active write failed\", { error: String(err) });\n rejectBatch(batch, err);\n return;\n }\n if (historyEntries.length > 0) {\n // Best-effort: active is the source of truth, history is an\n // audit aid. A failed history write is logged but doesn't\n // unwind the active commit.\n try {\n await persistHistory(historyEntries);\n } catch (err) {\n logger().error(\"history write failed\", { error: String(err) });\n }\n }\n for (const event of events) emit(event);\n }\n settleBatch(batch, results);\n}\n\nasync function drain(): Promise<void> {\n writing = true;\n try {\n while (waiters.length > 0) {\n const batch = waiters;\n waiters = [];\n await processBatch(batch);\n }\n } finally {\n writing = false;\n }\n}\n\nfunction enqueue(mutate: Mutation): Promise<void> {\n return new Promise<void>((resolve, reject) => {\n waiters.push({ mutate, resolve, reject });\n if (!writing) void drain();\n });\n}\n\nfunction removeEntry(state: NotifierFile, entryId: string): NotifierFile[\"entries\"] {\n // Object-rest excludes the key without invoking `delete`.\n const { [entryId]: __removed, ...remaining } = state.entries;\n return remaining;\n}\n\nfunction buildHistoryEntry(entry: NotifierEntry, terminalType: \"cleared\" | \"cancelled\"): NotifierHistoryEntry {\n return { ...entry, terminalType, terminalAt: new Date().toISOString() };\n}\n\n// ── Public API ────────────────────────────────────────────────────\n\nexport async function publish<TPluginData = unknown>(input: PublishInput<TPluginData>): Promise<{ id: string }> {\n // Validate at the engine boundary so plugin-runtime callers and\n // HTTP callers hit the same wall.\n const validationError = validatePublishInput(input as PublishInput);\n if (validationError) {\n throw new Error(`notifier.publish: ${validationError}`);\n }\n const entryId = randomUUID();\n const entry: NotifierEntry<TPluginData> = {\n id: entryId,\n pluginPkg: input.pluginPkg,\n severity: input.severity,\n lifecycle: input.lifecycle,\n title: input.title,\n body: input.body,\n navigateTarget: input.navigateTarget,\n pluginData: input.pluginData,\n createdAt: new Date().toISOString(),\n };\n await enqueue((state) => {\n state.entries[entryId] = entry as NotifierEntry;\n return { event: { type: \"published\", entry: entry as NotifierEntry } };\n });\n return { id: entryId };\n}\n\nexport async function clear(entryId: string): Promise<void> {\n await enqueue((state) => {\n const entry = state.entries[entryId];\n if (!entry) return null;\n state.entries = removeEntry(state, entryId);\n return {\n event: { type: \"cleared\", id: entryId },\n historyEntry: buildHistoryEntry(entry, \"cleared\"),\n };\n });\n}\n\nexport async function cancel(entryId: string): Promise<void> {\n await enqueue((state) => {\n const entry = state.entries[entryId];\n if (!entry) return null;\n state.entries = removeEntry(state, entryId);\n return {\n event: { type: \"cancelled\", id: entryId },\n historyEntry: buildHistoryEntry(entry, \"cancelled\"),\n };\n });\n}\n\n/** In-place update for an active entry. Only the fields present on\n * `patch` are rewritten; `id`, `pluginPkg`, `lifecycle`, and\n * `createdAt` stay fixed. Emits a single `\"updated\"` event with the\n * post-mutation entry — no history record is written because the\n * entry is still active, just with refreshed content.\n *\n * No-ops (no throw) when the id is unknown, the entry belongs to a\n * different plugin, or the merged shape would violate\n * `validatePublishInput`. The silent skip matches `clearForPlugin`'s\n * isolation semantics; validation failures are logged for diagnosis. */\nexport async function updateForPlugin<TPluginData = unknown>(\n pluginPkg: string,\n entryId: string,\n patch: {\n severity?: NotifierSeverity;\n title?: string;\n body?: string;\n navigateTarget?: string;\n pluginData?: TPluginData;\n },\n): Promise<void> {\n await enqueue((state) => {\n const entry = state.entries[entryId];\n if (!entry) return null;\n if (entry.pluginPkg !== pluginPkg) return null;\n const next: NotifierEntry = {\n ...entry,\n ...(patch.severity !== undefined ? { severity: patch.severity } : {}),\n ...(patch.title !== undefined ? { title: patch.title } : {}),\n ...(patch.body !== undefined ? { body: patch.body } : {}),\n ...(patch.navigateTarget !== undefined ? { navigateTarget: patch.navigateTarget } : {}),\n ...(patch.pluginData !== undefined ? { pluginData: patch.pluginData } : {}),\n };\n // Re-validate the merged shape so an update can't degrade the\n // entry below publish-time invariants.\n const validationError = validatePublishInput({\n pluginPkg: next.pluginPkg,\n severity: next.severity,\n title: next.title,\n body: next.body,\n lifecycle: next.lifecycle,\n navigateTarget: next.navigateTarget,\n pluginData: next.pluginData,\n });\n if (validationError) {\n logger().warn(\"update rejected by validation\", { entryId, pluginPkg, error: validationError });\n return null;\n }\n state.entries[entryId] = next;\n return { event: { type: \"updated\", entry: next } };\n });\n}\n\n/** Plugin-scoped point lookup. Returns the entry by id, but only if it\n * belongs to the caller's plugin; otherwise undefined. Cross-plugin\n * reads return undefined for isolation — same property as\n * `clearForPlugin` / `updateForPlugin`. */\nexport async function getForPlugin(pluginPkg: string, entryId: string): Promise<NotifierEntry | undefined> {\n const state = await loadActive(activeFilePath);\n const entry = state.entries[entryId];\n if (!entry) return undefined;\n if (entry.pluginPkg !== pluginPkg) return undefined;\n return entry;\n}\n\n/** Plugin-scoped clear. Same as `clear` but no-ops if the entry's\n * `pluginPkg` doesn't match the caller's, so a plugin can't dismiss\n * another plugin's notification by guessing or scraping its id. */\nexport async function clearForPlugin(pluginPkg: string, entryId: string): Promise<void> {\n await enqueue((state) => {\n const entry = state.entries[entryId];\n if (!entry) return null;\n if (entry.pluginPkg !== pluginPkg) return null;\n state.entries = removeEntry(state, entryId);\n return {\n event: { type: \"cleared\", id: entryId },\n historyEntry: buildHistoryEntry(entry, \"cleared\"),\n };\n });\n}\n\nexport async function get(entryId: string): Promise<NotifierEntry | undefined> {\n const state = await loadActive(activeFilePath);\n return state.entries[entryId];\n}\n\nexport async function listFor(pluginPkg: string): Promise<NotifierEntry[]> {\n const state = await loadActive(activeFilePath);\n return Object.values(state.entries).filter((entry) => entry.pluginPkg === pluginPkg);\n}\n\nexport async function listAll(): Promise<NotifierEntry[]> {\n const state = await loadActive(activeFilePath);\n return Object.values(state.entries);\n}\n\nexport async function listHistory(): Promise<NotifierHistoryEntry[]> {\n const state = await loadHistory(historyFilePath);\n return state.entries;\n}\n"],"mappings":"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"}