/** * @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