export interface ZoomCredentials { account_id: string; client_id: string; client_secret: string; } /** One entry from GET /meetings/meeting_summaries. Only the fields the port * actually reads are typed; Zoom sends more. */ export interface MeetingSummary { meeting_uuid?: string; meeting_id?: number | string; meeting_topic?: string; meeting_start_time?: string; summary_start_time?: string; [key: string]: unknown; } /** GET /meetings/{uuid}/meeting_summary. Two shapes: the modern single * markdown blob, and a deprecated split. See renderSummary. */ export interface MeetingDetail { summary_content?: string; summary_overview?: string; summary_details?: Array<{ label?: string; summary?: string; }>; next_steps?: unknown[]; [key: string]: unknown; } export interface ZoomRoute { /** Decibel project id */ id: string; /** Lowercased topic needles, longest first (see loadRoutes) */ match: string[]; /** Absolute output directory */ out: string; } export interface SyncOptions { from?: string; to?: string; days?: number; /** Single output directory. Ignored when route is true. */ out?: string; /** Fan out across the project registry instead of one directory. */ route?: boolean; /** Only topics containing this substring (case-insensitive). */ match?: string; force?: boolean; dryRun?: boolean; status?: string; noFrontmatter?: boolean; } export interface SyncResult { from: string; to: string; returned: number; in_range: number; written: number; skipped: number; empty: number; unrouted: number; reclaimed: number; dry_run: boolean; routes?: Array<{ id: string; match: string[]; out: string; }>; files: Array<{ name: string; dest: string; project: string | null; action: string; }>; notes: string[]; } export declare class ZoomError extends Error { code: string; hint?: string; constructor(code: string, message: string, hint?: string); } /** * Resolution order: environment, then the config.yaml `zoom` block, then the * standalone JSON file, then the legacy path. * * The JSON file is where plasiv's credentials actually live today (mode 600), * so it stays supported rather than being migrated out from under a working * setup. config.yaml is the location EPIC-0036 asks for and is preferred for * anything new — it is the same file the daemon already reads secrets from. * * Environment wins only when ALL THREE values are present. A half-set * environment falling through to a complete file beats erroring on a partial * override, and mixing the two sources produces an account_id from one Zoom app * with a secret from another, which fails at the token call with a message that * blames the wrong thing. */ export declare function loadCredentials(): ZoomCredentials; /** * Server-to-Server OAuth. The original fetched one token per run and never * looked at it again; S2S tokens last an hour, so a backfill long enough to * matter outlives its own credential and dies partway through with a 401 that * looks like a permissions problem. This caches with an expiry and refetches. */ export declare function getToken(creds: ZoomCredentials, now?: number): Promise; /** Test seam, and the escape hatch after a credential change mid-process. */ export declare function clearTokenCache(): void; export interface ApiGetDeps { fetchImpl?: typeof fetch; sleep?: (ms: number) => Promise; } /** * The original had no retry, no backoff, no 429 handling and no concurrency * cap — a serial loop issuing one detail call per meeting. Its clean log proves * nothing, because it has only ever run at hand-driven volume; an account-wide * backfill is the first time this would meet a rate limit. * * Retries 429 and 5xx with exponential backoff, honouring Retry-After when Zoom * sends one. 4xx other than 429 is not retried — a 401 or a 404 does not get * better by asking again. */ export declare function apiGet(urlPath: string, token: string, params?: Record, deps?: ApiGetDeps): Promise; /** * Zoom requires meeting UUIDs to be DOUBLE URL-encoded when they start with "/" * or contain "//", and single-encoded otherwise. * * UNVERIFIED AGAINST REAL DATA: none of the 26 files on the plasiv disk carry a * uuid that triggers the double branch, so that path comes from Zoom's docs * rather than from a case anyone here has seen. The single branch is confirmed — * it produces the same %3D%3D that appears inside the task permalinks Zoom * embeds in its own summary markdown. */ export declare function encodeMeetingUuid(uuid: string): string; /** * The list envelope is documented as `summaries`. When that key is absent this * falls back to whatever list-of-dicts the payload does carry and says so, * because the failure mode being defended against is a silent zero-result run: * an envelope rename would otherwise read as "no meetings", which is * indistinguishable from a quiet week. */ export declare function extractSummaryList(payload: Record, notes?: string[]): MeetingSummary[]; export declare function listSummaries(token: string, from: string, to: string, notes?: string[], deps?: ApiGetDeps): Promise; /** * Zoom returns the whole summary as markdown in `summary_content` on modern * accounts. The split fields are the deprecated shape. * * UNVERIFIED AGAINST REAL DATA: the split branch has never been exercised here. * Every one of the 26 files on the plasiv disk orders its sections Quick recap, * Next steps, per-person groups, Collaboration, Summary — and the split branch * structurally cannot put next_steps second, because it appends them last. That * ordering is the proof the account is on summary_content. The split branch * below is written from Zoom's field names, not from anything observed, and is * kept only so an older account degrades into readable output instead of an * empty file. */ export declare function renderSummary(detail: MeetingDetail): string; export declare function startOf(summary: MeetingSummary): string; /** * Zoom ignores from/to on the summaries list endpoint, so the window has to be * enforced here or it is a lie. An entry with no start time is kept rather than * dropped — a missing timestamp is a reason to look at it, not to hide it. */ export declare function inRange(summary: MeetingSummary, from: string, to: string): boolean; /** Five keys, same order, same spelling as the Python. The dedup index reads * meeting_uuid back out of this block, so the format is load-bearing. */ export declare function buildFrontmatter(summary: MeetingSummary, status: string): string; /** * "{topic} {YYYY-MM-DD HH_MM}Z.md", topic stripped of characters that are * illegal in a filename on some filesystem or other. Verified against the real * file in the unrouted bucket: this produces "Ben - Pete 2026-08-12 17_04Z.md". */ export declare function filenameFor(summary: MeetingSummary): string; export declare function unroutedDir(): string; /** * Projects carrying a `zoom` block in the registry: * * "zoom": { "match": ["plasiv"], "out": "meetings/raw" } * * Aliases are deliberately NOT matched. They are tool-call addresses — "dt", * "dv" — and a two-letter needle matches almost any meeting title. * * Needles are sorted longest-first so routing is deterministic by specificity. * The original took the first rule that matched in registry order, which means * a project whose needle is a substring of another project's needle silently * stole its meetings depending on file ordering. */ export declare function loadRoutes(): ZoomRoute[]; export declare function routeFor(summary: MeetingSummary, routes: ZoomRoute[]): ZoomRoute | null; export interface KnownMeeting { file: string; uuid: string; } /** * Index meeting_uuid out of the first 600 bytes of every .md in the given * directories, keyed on uuid AND start time. * * Two deliberate departures from the original: * * 1. UUID ALONE IS NOT THE KEY. If Zoom reuses a uuid across occurrences of a * recurring meeting, a uuid-keyed index silently skips the second occurrence * as already-seen. Nobody has observed that here and the Python does not * defend against it; keying on uuid+start costs nothing and closes it. * * 2. THE UNROUTED BUCKET IS NOT INDEXED HERE. See reclaim handling in * syncMeetings — an unrouted entry is UNCLAIMED, not SEEN. */ export declare function indexKnownMeetings(dirs: string[]): Map; export declare function dedupKey(uuid: string, start: string): string; /** The stub left in the unrouted bucket. Carries enough to see what is waiting * — and nothing else. * * ISS-0123: a routed run does not merely READ non-client meetings, it WRITES * them. The bucket on the plasiv machine holds a personal two-person meeting * with its full body on disk, put there by a single hand-run. Storing a stub * keeps the "never silently dropped" guarantee without spilling the content of * every meeting in the account onto the filesystem as a side effect of pulling * client ones. * * This is only safe BECAUSE the unrouted bucket is excluded from the dedup * index. A stub in a seen-index would satisfy dedup forever and the real body * would never arrive. Privacy stub and dedup fix are one change, not two. */ export declare function buildUnroutedStub(summary: MeetingSummary): string; export interface SyncDeps extends ApiGetDeps { credentials?: ZoomCredentials; token?: string; routes?: ZoomRoute[]; now?: Date; } /** * Pull summaries and write them where they belong. * * The routed path is the interesting one and the least exercised — one manual * run in its life — so it is written here from the spec rather than trusted * from the source. */ export declare function syncMeetings(opts?: SyncOptions, deps?: SyncDeps): Promise; //# sourceMappingURL=zoom.d.ts.map