/** * Tap — shared types. * * `TapProgress` is the seam of the whole chapter: every long-running function * takes an `onProgress` and emits these events. `done`/`total` are present on * EVERY event (not just `page`) because the consumer draws a bar from them and * a bar that only moves on some events stutters. */ type TapPhase = "discover" | "fetch" | "extract" | "push" | "delete" | "reindex"; type TapEvent = "start" | "page" | "done" | "error"; interface TapProgress { phase: TapPhase; event: TapEvent; url?: string; path?: string; done: number; total: number; message?: string; } type OnProgress = (ev: TapProgress) => void; /** How the URL list was found. */ type DiscoverySource = "sitemap" | "links"; /** * planTap — the preview that touches nothing. * * The whole point of a plan is that it is safe to run: it discovers, fetches * and extracts, and then hands back a table the caller can show a human before * anything is written anywhere. There is not a single knowledge-base call in * this module, by design — `tap` is the verb that writes. * * Two decisions worth knowing about: * * - **Excluded pages are marked, not dropped.** A preview that silently omits * half a site cannot be checked. They carry `excluded: true` and are never * fetched, so an exclusion also saves the requests. * - **A bad page is a row, not an exception.** One 404 in a hundred URLs must * not lose the other ninety-nine, so failures land as pages with `error`. */ interface TapPage { url: string; /** Knowledge-base document path: `/news/foo` -> `news__foo.md`. */ path: string; title: string; words: number; /** Rough estimate (characters / 4) — enough to budget an index. */ tokens: number; thin: boolean; needsJs: boolean; /** Hash of the extracted markdown; empty for excluded or failed pages. */ hash: string; /** Filtered out by `include`/`exclude` — listed, never fetched. */ excluded?: true; /** Why this page has no content. Its presence is what "failed" means. */ error?: string; /** * Only present when `keepContent` was requested. See {@link PlanTapOptions.keepContent}. */ markdown?: string; } interface TapPlanTotals { pages: number; included: number; excluded: number; failed: number; thin: number; needsJs: number; words: number; tokens: number; } interface TapPlan { startUrl: string; source: DiscoverySource; pages: TapPage[]; totals: TapPlanTotals; } interface PlanTapOptions { /** Maximum pages to consider. Politeness default: 100. */ limit?: number; /** In-flight requests. Politeness default: 4 — a browser's worth of load. */ concurrency?: number; /** If given, a URL must match one of these to be included. */ include?: RegExp[]; /** A URL matching any of these is excluded, even if `include` matched. */ exclude?: RegExp[]; /** Preferred content language, passed to fetch and extraction. */ language?: string; /** Per-page timeout override, in milliseconds. */ timeoutMs?: number; /** * Retain each page's markdown on the returned plan. * * Off by default and deliberately so: a 200-page site is several megabytes * of prose, and a preview that a UI holds while a human reads it should not * pin that. Turn it on when the caller is about to `tap` the plan straight * away and wants to reuse the text instead of re-fetching the site. */ keepContent?: boolean; onProgress?: OnProgress; } /** * Knowledge-base document path for a URL: the pathname with `/` as separator * flattened to `__`, `index` for the root, and a `.md` suffix. Flat because a * knowledge base is a flat namespace, and reversible enough to read. */ declare function docPath(url: string): string; /** `include` is an allow-list when present; `exclude` always wins. */ declare function isExcluded(url: string, include?: readonly RegExp[], exclude?: readonly RegExp[]): boolean; /** * Preview what tapping a site would index — without writing anything. * * Discovers the URL list (sitemap first, one hop of links as fallback), then * fetches and extracts every page that survived the include/exclude filter, * with a bounded worker pool. Progress is emitted for discovery, fetch and * extract with monotonic `done`/`total`. * * A site whose robots.txt refuses crawlers yields a plan with no pages rather * than an exception — "this site refuses crawlers" is an answer, not a crash. */ declare function planTap(startUrl: string, opts?: PlanTapOptions): Promise; /** * Base error type. * * Lives in kernel/ rather than client.ts so the domain layer (Call, Agent) can * throw it without importing the client — client.ts already imports the domain, * and the cycle would bite at module-evaluation time. * * `client.ts` re-exports it, so `import { PinecallError } from "@pinecall/sdk"` * keeps working exactly as before. */ declare class PinecallError extends Error { code?: string | undefined; constructor(message: string, code?: string | undefined); } /** * Knowledge base (RAG) REST client — the documents an agent can look things up in. * * Knowledge bases live on the PLAYGROUND API (the management plane), not on * the voice server: creating a KB, pushing docs and rebuilding the index are * account operations, and they happen whether or not any agent is online. * That is why this module takes its own `playgroundUrl` instead of the * `apiUrl` the rest of the SDK talks to. * * Knowledge bases are a paid feature. The server answers HTTP 402 for orgs on * a plan without them, and that arrives here as a typed * `KnowledgeApiError` with `code === "UPGRADE_REQUIRED"` — catchable, so a * consumer can offer the upgrade instead of parsing a message. */ interface KnowledgeApiOptions { apiKey: string; /** * Management API base. Defaults to `PINECALL_PLAYGROUND_URL` and then to * https://playground.pinecall.io. Trailing slashes are stripped, so * "http://localhost:3000/" and "http://localhost:3000" are the same host. */ playgroundUrl?: string; } /** * tap / syncTap — the verbs that write. * * `planTap` looks; these two pour. The whole incremental story hangs on ONE * artefact: a document stored inside the knowledge base itself, at the path * `_tap-manifest.json`, mapping every doc path to the URL it came from and the * hash of the markdown that was pushed. Keeping it in the KB (instead of on * the caller's disk) is what makes a sync work from any machine, any process, * any CI job — the knowledge base carries its own provenance. * * Two rules the manifest buys us, and both are acceptance criteria: * * - **An unchanged page is not re-pushed.** Same hash ⇒ skipped, not written. * - **Zero delta ⇒ no reindex at all.** Re-indexing is the expensive half of * the operation; a sync that found nothing must cost nothing. */ /** Where the manifest lives, inside the knowledge base it describes. */ declare const MANIFEST_PATH = "_tap-manifest.json"; interface TapManifestEntry { url: string; hash: string; } /** * The crawl options a tap ran with, in a shape that survives JSON. * * `include`/`exclude` are stored as **RegExp sources** (`re.source`) because a * `RegExp` does not serialize — `JSON.stringify(/a/)` is `{}` — and they are * rebuilt with `new RegExp(s)` on read. Flags are deliberately not kept: the * filters are matched against URLs, where case matters. */ interface TapCrawlOptions { limit?: number; /** RegExp sources, not patterns with delimiters: `\\/docs\\/`, not `/docs/`. */ include?: string[]; exclude?: string[]; } interface TapManifest { version: 1; startUrl: string; source: DiscoverySource; tappedAt: string; /** * The crawl options the tap that wrote this manifest actually used, so a * later `syncTap` re-plans the same slice of the site instead of the whole * of it. * * **Optional on read, and that is not a version bump.** A manifest written * before this field existed simply has none, and syncs with the library * defaults (limit 100, no include/exclude) — exactly the behaviour it had * when it was written. Manifest `version` stays `1`. */ options?: TapCrawlOptions; /** Keyed by knowledge-base document path. */ pages: Record; } interface TapFailure { path: string; error: string; } interface TapReport { /** Documents whose path was not in the previous manifest. */ pushed: number; /** Documents that existed with a different hash. */ updated: number; /** Documents whose hash was unchanged — never sent. */ skipped: number; failed: TapFailure[]; /** Documents removed because the site no longer serves them. */ deleted: number; reindexed: boolean; } interface TapOptions { /** * Maximum pages to consider. Politeness default: 100. * * Used when `tap` plans the site itself; when it is handed a prebuilt plan * the plan already decided, and this is recorded in the manifest so the * next `syncTap` re-plans with the same bound. */ limit?: number; include?: RegExp[]; exclude?: RegExp[]; onProgress?: OnProgress; /** Rebuild the index at the end when something moved. Default: true. */ reindex?: boolean; } interface SyncTapOptions { /** * Override the limit stored in the manifest. Omitted, the stored one is * used; given, it wins and is written back on the next manifest write. */ limit?: number; /** Override the manifest's stored `include`. Same rule as {@link SyncTapOptions.limit}. */ include?: RegExp[]; /** Override the manifest's stored `exclude`. Same rule as {@link SyncTapOptions.limit}. */ exclude?: RegExp[]; onProgress?: OnProgress; reindex?: boolean; } /** Thrown by `syncTap` when the knowledge base carries no manifest. */ declare class TapSyncError extends PinecallError { constructor(message: string, code: string); } /** * Read the manifest out of the knowledge base. Returns null both when there is * no manifest document and when the one there cannot be parsed — a corrupt * manifest is treated as "never tapped", which re-pushes rather than deletes. */ declare function readManifest(auth: KnowledgeApiOptions, kbId: string): Promise<{ manifest: TapManifest | null; docId: string | null; }>; /** * Pour a plan into a knowledge base. * * Give it a URL and it plans the site first; give it a plan a human already * approved and it uses exactly that. Unchanged pages (same hash as the last * tap) are skipped without a request, and the manifest is rewritten at the end * so the next `syncTap` knows what this run left behind. */ declare function tap(auth: KnowledgeApiOptions, kbId: string, plan: TapPlan | string, opts?: TapOptions): Promise; /** * Re-tap a knowledge base from its own manifest. * * Needs no arguments beyond the KB because the manifest already says which * site this is and what was in it. Pages that changed are pushed, pages the * site stopped serving are deleted, and everything else is left alone — so a * sync over a site that did not move is a handful of GETs and nothing else. */ declare function syncTap(auth: KnowledgeApiOptions, kbId: string, opts?: SyncTapOptions): Promise; /** * Which URLs make up "a website". * * Sitemap first — it is authoritative, costs one request and needs no HTML * parsing. The link crawl is the fallback, and it is deliberately one hop: a * preview that takes a minute to compute is a preview nobody waits for. * * Normalization is the other half of the job. Without it the same page lands * in the knowledge base once per tracking parameter, and a re-tap never * matches what the previous one pushed. */ declare const DEFAULT_PAGE_LIMIT = 100; declare const DEFAULT_CONCURRENCY = 4; /** * Plain HTTP fetching — the cheap path that covers every server-rendered site. * * No browser here by design: headless rendering is out of scope for tap, and a * page that needs it is reported honestly (`needsJs`) rather than rendered. * What this module does owe the site is politeness: an identifying user-agent, * a hard per-page timeout, and a refusal to download anything that is not HTML. */ declare const USER_AGENT: string; /** A page must answer within this, or it is not worth the crawl budget. */ declare const DEFAULT_TIMEOUT_MS = 15000; declare class TapFetchError extends Error { readonly url: string; readonly status?: number; constructor(message: string, info: { url: string; status?: number; }); } /** * HTML -> clean Markdown. * * Defuddle does both halves of the job: it isolates the main content (the * Readability role) and standardizes code blocks, math and footnotes before * converting — which is exactly where readability+turndown produces garbage. * * The two thresholds below were calibrated against real sites; see each one. */ /** A page that yields less than this is short — not necessarily broken. */ declare const THIN_CONTENT_WORDS = 120; /** * Visible-text-to-HTML ratio below which the page is almost certainly rendered * by JavaScript. Measured: vercel.com 0.0055 and stripe.com/docs/api 0.0065 * (both client-rendered) against hono.dev 0.023 and bernardocastro.dev 0.195 * (both server-rendered, one of them genuinely short). Word count alone * confuses "short page" with "empty shell"; this ratio separates them. */ declare const SPA_TEXT_RATIO = 0.012; export { DEFAULT_CONCURRENCY, DEFAULT_PAGE_LIMIT, DEFAULT_TIMEOUT_MS, type DiscoverySource, MANIFEST_PATH, type OnProgress, type PlanTapOptions, SPA_TEXT_RATIO, type SyncTapOptions, THIN_CONTENT_WORDS, type TapCrawlOptions, type TapEvent, type TapFailure, TapFetchError, type TapManifest, type TapManifestEntry, type TapOptions, type TapPage, type TapPhase, type TapPlan, type TapPlanTotals, type TapProgress, type TapReport, TapSyncError, USER_AGENT, docPath, isExcluded, planTap, readManifest, syncTap, tap };