/** * @fileoverview HN API service — Firebase and Algolia HTTP clients with HTML stripping utilities. * @module services/hn/hn-service */ import type { Context } from '@cyanheads/mcp-ts-core'; import { McpError } from '@cyanheads/mcp-ts-core/errors'; import type { AlgoliaResponse, HnFeedType, HnItem, HnUser } from './types.js'; /** Decode numeric and named HTML entities. An undeclared name stays as typed. */ export declare function decodeHtmlEntities(text: string): string; /** * Strip an HN HTML body (`text`, `about`, and Algolia's `comment_text` / * `story_text`) to plain text: `

` becomes a blank line, code blocks are kept * verbatim, each link renders once as its full URL — or as `text (url)` when * the text is something else — other tags are removed, and entities are * decoded exactly once. Titles are not HTML, though some arrive entity-encoded: * they take {@link decodeHtmlEntities} alone and never pass through here. */ export declare function stripHtml(html: string): string; /** * {@link stripHtml} for an Algolia highlight snippet, which is the field's HTML * with `` inserted around matched terms: the `…` markers survive, * including inside a link collapsed to its href. */ export declare function stripHtmlPreservingEm(html: string): string; /** Normalize empty URL strings to undefined. */ export declare function normalizeUrl(url?: string | null): string | undefined; /** * Extract the bare hostname (lowercased, leading `www.` stripped) from a URL. * Returns undefined for missing or unparseable URLs. */ export declare function extractDomain(url: string | undefined): string | undefined; /** * Convert a validated ISO 8601 date bound to its exact instant in Unix * milliseconds. A date-time without a `Z` or `±hh:mm` offset is read as UTC — * the same as a date-only value, and unlike JS's own parsing, which reads it in * the host's local time zone. */ export declare function dateBoundToEpochMs(bound: string): number; /** * Convert a validated ISO 8601 date bound to Unix seconds, read as * {@link dateBoundToEpochMs} reads it. * * A fractional second rounds down by default, which suits an exclusive start. * An exclusive end over whole-second timestamps passes `'ceil'`: an end at * `10:00:00.5` must still admit an item created at `10:00:00`. */ export declare function dateBoundToEpochSeconds(bound: string, rounding?: 'floor' | 'ceil'): number; /** * One slot of a {@link HnService.fetchItems} batch, in input order. * * - `item` — HN returned the item. It may be dead or deleted; callers filter. * - `absent` — HN answered `null`: no item exists under this ID. * - `failed` — the fetch failed after retries, or never started because an * earlier slot was rate-limited. `error` is the classified failure. A later * call may succeed, so a caller reports the ID rather than dropping it. */ export type ItemSlot = { kind: 'item'; id: number; item: HnItem; } | { kind: 'absent'; id: number; } | { kind: 'failed'; id: number; error: unknown; }; /** * Settle one page of a batch for the paged tools: the live items in input * order, and the IDs whose fetch failed. Dead, deleted, and absent slots drop * out of both. When every slot failed, throws the first failure's classified * error, since an empty success would read as a page with nothing on it. */ export declare function settlePage(slots: readonly ItemSlot[]): { items: HnItem[]; failedIds: number[]; }; /** True for the classified 429 `getJson` throws once retries are spent or the wait outlasts them. */ export declare function isRateLimited(err: unknown): err is McpError; export declare class HnService { private readonly concurrencyLimit; constructor(concurrencyLimit: number); /** * GET a JSON document from an upstream, with retries around the full * fetch-and-parse pipeline and both failure shapes — non-2xx status and * HTML-instead-of-JSON — classified onto a contract reason. */ private getJson; /** Fetch a single item by ID. Returns null when HN reports the item is missing. */ fetchItem(id: number, ctx: Context): Promise; /** * Fetch a user profile by username. Returns null when the user does not exist. * The username is percent-encoded as a single path segment — an unencoded * value containing `/` or dot-segments would otherwise escape the `/user/` * route and resolve to a different Firebase resource. */ fetchUser(username: string, ctx: Context): Promise; /** Fetch a feed's ID array. Throws on upstream failure after retries. */ fetchFeed(type: HnFeedType, ctx: Context): Promise; /** * Batch-fetch items with concurrency limiting: one {@link ItemSlot} per ID, * in input order. A per-item failure after exhausted retries fills a * `failed` slot, so one bad item does not fail the batch. Two failures are * not per-item: * * - A caller abort rethrows, ending the batch instead of spending the rest of * the ID list on fetches nobody is waiting for. * - An `upstream_rate_limited` failure stops the batch. Fetches already in * flight settle, and every slot not yet started is reported `failed` with * that error, rather than each running its own retry ladder against a * throttled upstream. */ fetchItems(ids: readonly number[], ctx: Context): Promise; /** * Search HN via Algolia. Throws on upstream failure after retries. An omitted * `query` sends no `query` param, making a filter-only search; the tags are * comma-joined, which Algolia ANDs. Both date bounds are exclusive. */ search(params: { query?: string | undefined; tags?: string | undefined; author?: string | undefined; storyId?: number | undefined; sort: 'relevance' | 'date'; dateRange?: { start?: string | undefined; end?: string | undefined; } | undefined; minPoints?: number | undefined; count: number; page: number; }, ctx: Context): Promise; } export declare function initHnService(): void; export declare function getHnService(): HnService; //# sourceMappingURL=hn-service.d.ts.map