@remnic/connector-granola 9.69.24 → 9.69.26

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.
package/dist/index.d.ts CHANGED
@@ -96,7 +96,7 @@ declare class GranolaClient {
96
96
 
97
97
  /**
98
98
  * Normalize Granola notes into Remnic's provider-agnostic
99
- * `WearableConversation` shape, plus the timezone-aware day-window helpers the
99
+ * `WearableConversation` shape, plus the timezone-aware day-window helper the
100
100
  * Granola `created_after`/`created_before` filters need.
101
101
  *
102
102
  * Granola transcript items carry absolute `start_time`/`end_time` (ISO) and a
@@ -110,7 +110,8 @@ declare class GranolaClient {
110
110
  declare const GRANOLA_SOURCE_ID = "granola";
111
111
  /**
112
112
  * Half-open [createdAfter, createdBefore) UTC ISO bounds of a local day — the
113
- * window the Granola notes list filters on.
113
+ * window the Granola notes list filters on. Field names map to the Granola
114
+ * API; the DST-aware window math is core's `activityDayWindow`.
114
115
  */
115
116
  declare function granolaDayWindow(date: string, timezone: string): {
116
117
  createdAfter: string;
package/dist/index.js CHANGED
@@ -188,49 +188,13 @@ async function retryDelayMs(response, attempt) {
188
188
  }
189
189
 
190
190
  // src/normalize.ts
191
+ import {
192
+ activityDayWindow
193
+ } from "@remnic/core";
191
194
  var GRANOLA_SOURCE_ID = "granola";
192
- function timezoneOffsetIso(instant, timezone) {
193
- try {
194
- const parts = new Intl.DateTimeFormat("en-US", {
195
- timeZone: timezone,
196
- timeZoneName: "longOffset"
197
- }).formatToParts(instant);
198
- const name = parts.find((part) => part.type === "timeZoneName")?.value ?? "GMT";
199
- const match = name.match(/GMT([+-]\d{2}:\d{2})?/);
200
- return match?.[1] ?? "+00:00";
201
- } catch {
202
- return "+00:00";
203
- }
204
- }
205
- function zonedDayStartIso(date, timezone) {
206
- let offset = timezoneOffsetIso(/* @__PURE__ */ new Date(`${date}T12:00:00Z`), timezone);
207
- const candidate = /* @__PURE__ */ new Date(`${date}T00:00:00${offset}`);
208
- const refined = timezoneOffsetIso(candidate, timezone);
209
- if (refined !== offset) {
210
- offset = refined;
211
- }
212
- return `${date}T00:00:00${offset}`;
213
- }
214
- function nextIsoDate(date) {
215
- const parsed = /* @__PURE__ */ new Date(`${date}T00:00:00Z`);
216
- parsed.setUTCDate(parsed.getUTCDate() + 1);
217
- return parsed.toISOString().slice(0, 10);
218
- }
219
- function assertValidTimezone(timezone) {
220
- try {
221
- new Intl.DateTimeFormat("en-US", { timeZone: timezone });
222
- } catch {
223
- throw new RangeError(
224
- `Invalid IANA timezone "${timezone}" for the Granola connector \u2014 check wearables.timezone.`
225
- );
226
- }
227
- }
228
195
  function granolaDayWindow(date, timezone) {
229
- assertValidTimezone(timezone);
230
- return {
231
- createdAfter: new Date(zonedDayStartIso(date, timezone)).toISOString(),
232
- createdBefore: new Date(zonedDayStartIso(nextIsoDate(date), timezone)).toISOString()
233
- };
196
+ const { startUtc, endUtc } = activityDayWindow(date, timezone);
197
+ return { createdAfter: startUtc, createdBefore: endUtc };
234
198
  }
235
199
  function trimmed(value) {
236
200
  return typeof value === "string" && value.trim().length > 0 ? value.trim() : void 0;
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/index.ts","../src/client.ts","../src/normalize.ts"],"sourcesContent":["/**\n * @remnic/connector-granola — Granola meeting-notes connector.\n *\n * À-la-carte optional companion of @remnic/core: installing core alone\n * never pulls this in; core discovers it at runtime via a\n * computed-specifier dynamic import (see wearables/registry.ts) or via\n * a direct import of this module, which self-registers idempotently.\n *\n * API key: `wearables.sources.granola.apiKey`, else the\n * `REMNIC_GRANOLA_API_KEY` / `GRANOLA_API_KEY` environment variables\n * (checked in that order). Create a key under Settings → Connectors →\n * API keys in the Granola app (Business/Enterprise plans).\n */\n\nimport {\n selfRegisterWearableConnector,\n type WearableConnectorFactoryOptions,\n type WearableConnectorRegistration,\n type WearableConversation,\n type WearableFetchOptions,\n type WearableFetchPage,\n type WearableSourceConnector,\n} from \"@remnic/core\";\n\nimport { GranolaApiError, GranolaClient, type GranolaNote } from \"./client.js\";\nimport { GRANOLA_SOURCE_ID, granolaDayWindow, noteToConversation } from \"./normalize.js\";\n\nexport {\n GranolaClient,\n GranolaApiError,\n GRANOLA_DEFAULT_BASE_URL,\n NOTES_MAX_PAGE_SIZE,\n} from \"./client.js\";\nexport type {\n GranolaNote,\n GranolaTranscriptItem,\n GranolaSpeaker,\n GranolaCalendarEvent,\n GranolaUser,\n NotesPage,\n GranolaClientOptions,\n} from \"./client.js\";\nexport { GRANOLA_SOURCE_ID, granolaDayWindow, noteToConversation } from \"./normalize.js\";\n\nconst GRANOLA_DISPLAY_NAME = \"Granola\";\n\nexport function resolveGranolaApiKey(\n configuredKey: string | undefined,\n env: NodeJS.ProcessEnv = process.env,\n): string | undefined {\n if (typeof configuredKey === \"string\" && configuredKey.trim().length > 0) {\n return configuredKey.trim();\n }\n for (const name of [\"REMNIC_GRANOLA_API_KEY\", \"GRANOLA_API_KEY\"]) {\n const value = env[name];\n if (typeof value === \"string\" && value.trim().length > 0) {\n return value.trim();\n }\n }\n return undefined;\n}\n\nexport function createGranolaConnector(\n options: WearableConnectorFactoryOptions,\n): WearableSourceConnector {\n let client: GranolaClient | null = null;\n const getClient = (): GranolaClient => {\n if (!client) {\n client = new GranolaClient({\n apiKey: resolveGranolaApiKey(options.settings.apiKey) ?? \"\",\n baseUrl: options.settings.baseUrl,\n });\n }\n return client;\n };\n\n return {\n id: GRANOLA_SOURCE_ID,\n displayName: GRANOLA_DISPLAY_NAME,\n async verifyAuth(signal?: AbortSignal) {\n return getClient().verifyAuth(signal);\n },\n async fetchConversations(opts: WearableFetchOptions): Promise<WearableFetchPage> {\n const { createdAfter, createdBefore } = granolaDayWindow(opts.date, opts.timezone);\n const activeClient = getClient();\n const page = await activeClient.listNotes({\n createdAfter,\n createdBefore,\n cursor: opts.cursor,\n signal: opts.signal,\n });\n // The list returns summaries only; each note's transcript/summary/timing\n // needs a per-note fetch. Fetch sequentially to respect the 5 req/s rate\n // limit (the client also backs off on 429).\n const conversations: WearableConversation[] = [];\n for (const summary of page.notes) {\n let full: GranolaNote;\n try {\n full = await activeClient.getNote(summary.id, opts.signal);\n } catch (err) {\n // A note that 404s (deleted / no longer summarized) is gone, not a\n // backend outage — skip it. Any other failure is real; rethrow so it\n // reads as a source failure, not a quiet day (AGENTS.md §22).\n if (err instanceof GranolaApiError && err.status === 404) continue;\n throw err;\n }\n const conversation = noteToConversation(full);\n // Drop a record with no resolvable start rather than emit an empty\n // startIso that fails downstream parsing silently.\n if (conversation.startIso !== \"\") conversations.push(conversation);\n }\n return { conversations, nextCursor: page.nextCursor };\n },\n };\n}\n\nexport const wearableConnectorRegistration: WearableConnectorRegistration = {\n id: GRANOLA_SOURCE_ID,\n displayName: GRANOLA_DISPLAY_NAME,\n factory: createGranolaConnector,\n};\n\n/**\n * Idempotently register the connector with the core registry. Importing\n * this module registers it as a side effect; calling this again is safe\n * (returns false when already registered).\n */\nexport const ensureGranolaConnectorRegistered =\n selfRegisterWearableConnector(wearableConnectorRegistration);\n","/**\n * Minimal Granola public API client (raw fetch, no SDK).\n *\n * API verified against the official OpenAPI at\n * https://docs.granola.ai/api-reference/list-notes and\n * https://docs.granola.ai/api-reference/get-note (fetched 2026-07-21):\n * - Base URL: https://public-api.granola.ai\n * - Auth: `Authorization: Bearer grn_<key>`\n * - `GET /v1/notes?created_after=&created_before=&cursor=&page_size=` →\n * `{ notes: NoteSummary[], hasMore, cursor }` (page_size 1..30, default 10);\n * `created_after`/`created_before` accept date or date-time. The list only\n * returns notes that already have an AI summary + transcript.\n * - `GET /v1/notes/{id}?include=transcript` → full Note with `calendar_event`\n * (`scheduled_start_time`/`scheduled_end_time`), `attendees`, `summary_text`,\n * `summary_markdown`, and `transcript` items\n * `{ speaker: { source, diarization_label? }, text, start_time, end_time }`.\n * - Rate limits: 5 req/s sustained, 25 burst → 429 on excess.\n *\n * A non-2xx or network failure throws GranolaApiError (a backend failure); an\n * empty `notes` array is a real empty result, never conflated (AGENTS.md §22).\n */\n\nexport const GRANOLA_DEFAULT_BASE_URL = \"https://public-api.granola.ai\";\n\n/** Hard API maximum for `page_size` on the notes list. */\nexport const NOTES_MAX_PAGE_SIZE = 30;\n\nconst DEFAULT_TIMEOUT_MS = 30_000;\nconst MAX_RETRIES = 3;\nconst MAX_RETRY_DELAY_MS = 30_000;\n\nexport interface GranolaSpeaker {\n source?: string | null;\n diarization_label?: string | null;\n}\n\nexport interface GranolaTranscriptItem {\n speaker?: GranolaSpeaker | null;\n text?: string | null;\n start_time?: string | null;\n end_time?: string | null;\n}\n\nexport interface GranolaCalendarEvent {\n event_title?: string | null;\n organiser?: string | null;\n scheduled_start_time?: string | null;\n scheduled_end_time?: string | null;\n}\n\nexport interface GranolaUser {\n name?: string | null;\n email?: string | null;\n}\n\nexport interface GranolaNote {\n id: string;\n title?: string | null;\n created_at?: string | null;\n updated_at?: string | null;\n calendar_event?: GranolaCalendarEvent | null;\n attendees?: GranolaUser[] | null;\n summary_text?: string | null;\n summary_markdown?: string | null;\n transcript?: GranolaTranscriptItem[] | null;\n}\n\nexport interface NotesPage {\n notes: GranolaNote[];\n nextCursor: string | null;\n}\n\nexport interface GranolaClientOptions {\n apiKey: string;\n baseUrl?: string;\n fetchImpl?: typeof fetch;\n timeoutMs?: number;\n sleep?: (ms: number) => Promise<void>;\n}\n\nexport class GranolaApiError extends Error {\n constructor(\n message: string,\n readonly status?: number,\n ) {\n super(message);\n this.name = \"GranolaApiError\";\n }\n}\n\n/** Narrow unknown JSON to a plain object so field reads are actually checked. */\nfunction isRecord(value: unknown): value is Record<string, unknown> {\n return typeof value === \"object\" && value !== null;\n}\n\nexport class GranolaClient {\n private readonly apiKey: string;\n private readonly baseUrl: string;\n private readonly fetchImpl: typeof fetch;\n private readonly timeoutMs: number;\n private readonly sleep: (ms: number) => Promise<void>;\n\n constructor(options: GranolaClientOptions) {\n if (typeof options.apiKey !== \"string\" || options.apiKey.trim().length === 0) {\n throw new GranolaApiError(\n \"Granola API key is missing. Set wearables.sources.granola.apiKey \" +\n \"or the REMNIC_GRANOLA_API_KEY / GRANOLA_API_KEY environment variable \" +\n \"(create a key under Settings → Connectors → API keys in the Granola app; \" +\n \"requires a Business/Enterprise plan).\",\n );\n }\n this.apiKey = options.apiKey.trim();\n this.baseUrl = stripTrailingSlashes(options.baseUrl ?? GRANOLA_DEFAULT_BASE_URL);\n this.fetchImpl = options.fetchImpl ?? fetch;\n this.timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS;\n this.sleep = options.sleep ?? defaultSleep;\n }\n\n /** One page of note summaries in the half-open [createdAfter, createdBefore) window. */\n async listNotes(params: {\n createdAfter: string;\n createdBefore: string;\n cursor?: string | null;\n signal?: AbortSignal;\n }): Promise<NotesPage> {\n const search = new URLSearchParams({\n created_after: params.createdAfter,\n created_before: params.createdBefore,\n page_size: String(NOTES_MAX_PAGE_SIZE),\n });\n if (typeof params.cursor === \"string\" && params.cursor.length > 0) {\n search.set(\"cursor\", params.cursor);\n }\n const payload = await this.requestJson(`/v1/notes?${search.toString()}`, params.signal);\n const notesRaw = isRecord(payload) ? payload.notes : undefined;\n if (!Array.isArray(notesRaw)) {\n throw new GranolaApiError(\"Granola API returned an unexpected /v1/notes shape (missing notes array)\");\n }\n // `id` is required by the List Notes schema; a row missing it is a\n // backend/schema failure. Reject the page rather than silently dropping\n // rows and advancing the cursor over an incomplete day (§22).\n const notes: GranolaNote[] = [];\n for (const entry of notesRaw) {\n if (!isRecord(entry) || typeof entry.id !== \"string\") {\n throw new GranolaApiError(\"Granola API returned a note row without a string id\");\n }\n notes.push(entry as unknown as GranolaNote);\n }\n const hasMore = isRecord(payload) && payload.hasMore === true;\n const cursor = isRecord(payload) && typeof payload.cursor === \"string\" ? payload.cursor : null;\n if (hasMore && (cursor === null || cursor.length === 0)) {\n // A `hasMore: true` page with no usable cursor is a malformed pagination\n // response; fail loudly rather than silently sync a partial day (§22).\n throw new GranolaApiError(\"Granola reported hasMore=true but returned no pagination cursor\");\n }\n return { notes, nextCursor: hasMore ? cursor : null };\n }\n\n /** A single note with its transcript, calendar event, summary, and attendees. */\n async getNote(id: string, signal?: AbortSignal): Promise<GranolaNote> {\n const payload = await this.requestJson(\n `/v1/notes/${encodeURIComponent(id)}?include=transcript`,\n signal,\n );\n if (!isRecord(payload) || typeof payload.id !== \"string\") {\n throw new GranolaApiError(\"Granola API returned an unexpected note shape (missing id)\");\n }\n // Validated boundary: `id` is confirmed a string above; every other field\n // is optional and the normalizer re-checks each. Cast through `unknown`\n // because the external JSON shape can't be proven structurally here.\n return payload as unknown as GranolaNote;\n }\n\n /** Cheap auth probe. */\n async verifyAuth(signal?: AbortSignal): Promise<{ ok: boolean; detail?: string }> {\n try {\n await this.requestJson(\"/v1/notes?page_size=1\", signal);\n return { ok: true };\n } catch (err) {\n if (err instanceof GranolaApiError && (err.status === 401 || err.status === 403)) {\n return {\n ok: false,\n detail: \"Granola rejected the API key (401/403) — create a new key under Settings → Connectors → API keys\",\n };\n }\n return { ok: false, detail: err instanceof Error ? err.message : String(err) };\n }\n }\n\n private async requestJson(pathAndQuery: string, signal?: AbortSignal): Promise<unknown> {\n let lastError: unknown;\n for (let attempt = 0; attempt <= MAX_RETRIES; attempt++) {\n signal?.throwIfAborted();\n const timeoutSignal = AbortSignal.timeout(this.timeoutMs);\n const combined = signal ? AbortSignal.any([signal, timeoutSignal]) : timeoutSignal;\n let response: Response;\n try {\n response = await this.fetchImpl(`${this.baseUrl}${pathAndQuery}`, {\n method: \"GET\",\n headers: {\n Authorization: `Bearer ${this.apiKey}`,\n Accept: \"application/json\",\n },\n signal: combined,\n });\n } catch (err) {\n if (signal?.aborted) throw err;\n lastError = err;\n if (attempt < MAX_RETRIES) {\n await this.sleep(backoffMs(attempt));\n continue;\n }\n throw new GranolaApiError(\n `Granola API request failed after ${MAX_RETRIES + 1} attempts: ${describeNetworkError(err)}`,\n );\n }\n\n if (response.status === 429 || response.status >= 500) {\n discardBody(response);\n lastError = new GranolaApiError(`Granola API responded ${response.status}`, response.status);\n if (attempt < MAX_RETRIES) {\n await this.sleep(await retryDelayMs(response, attempt));\n continue;\n }\n throw lastError;\n }\n if (!response.ok) {\n discardBody(response);\n throw new GranolaApiError(\n `Granola API responded ${response.status} for ${pathAndQuery.split(\"?\")[0]}`,\n response.status,\n );\n }\n try {\n return await response.json();\n } catch {\n throw new GranolaApiError(\"Granola API returned a non-JSON body\");\n }\n }\n throw lastError instanceof Error ? lastError : new GranolaApiError(\"Granola API request failed\");\n }\n}\n\nfunction defaultSleep(ms: number): Promise<void> {\n const { promise, resolve } = Promise.withResolvers<void>();\n setTimeout(resolve, ms);\n return promise;\n}\n\n/**\n * Release the socket back to the pool on error paths: an unconsumed response\n * body pins the undici connection (stability review note).\n */\nfunction discardBody(response: Response): void {\n void response.body?.cancel().catch(() => {});\n}\n\nfunction describeNetworkError(err: unknown): string {\n if (isRecord(err)) {\n const name = typeof err.name === \"string\" ? err.name : \"Error\";\n const code = err.code;\n return typeof code === \"string\" ? `${name} (${code})` : name;\n }\n return \"network error\";\n}\n\n/** Loop instead of `/\\/+$/` — CodeQL js/polynomial-redos on user-set URLs. */\nfunction stripTrailingSlashes(value: string): string {\n let end = value.length;\n while (end > 0 && value.charCodeAt(end - 1) === 47) end--;\n return value.slice(0, end);\n}\n\nfunction backoffMs(attempt: number): number {\n return Math.min(MAX_RETRY_DELAY_MS, 1_000 * 2 ** attempt);\n}\n\nasync function retryDelayMs(response: Response, attempt: number): Promise<number> {\n const header = response.headers.get(\"retry-after\");\n if (header) {\n const seconds = Number(header);\n if (Number.isFinite(seconds) && seconds >= 0) {\n return Math.min(MAX_RETRY_DELAY_MS, seconds * 1_000);\n }\n const when = Date.parse(header);\n if (Number.isFinite(when)) {\n return Math.min(MAX_RETRY_DELAY_MS, Math.max(0, when - Date.now()));\n }\n }\n return backoffMs(attempt);\n}\n","/**\n * Normalize Granola notes into Remnic's provider-agnostic\n * `WearableConversation` shape, plus the timezone-aware day-window helpers the\n * Granola `created_after`/`created_before` filters need.\n *\n * Granola transcript items carry absolute `start_time`/`end_time` (ISO) and a\n * `speaker.source` of `microphone` (the wearer's own captured audio) or\n * `speaker` (other meeting audio); iOS adds a `diarization_label`\n * (`Speaker A/B/...`). The wearables speaker registry owns final naming.\n * Meeting timing prefers the linked calendar event; notes with a summary but no\n * transcript degrade to a single `note` segment.\n */\n\nimport type {\n WearableConversation,\n WearableTranscriptSegment,\n} from \"@remnic/core\";\n\nimport type { GranolaNote } from \"./client.js\";\n\nexport const GRANOLA_SOURCE_ID = \"granola\";\n\n/** \"GMT+05:30\" → \"+05:30\"; plain \"GMT\" → \"+00:00\". */\nexport function timezoneOffsetIso(instant: Date, timezone: string): string {\n try {\n const parts = new Intl.DateTimeFormat(\"en-US\", {\n timeZone: timezone,\n timeZoneName: \"longOffset\",\n }).formatToParts(instant);\n const name = parts.find((part) => part.type === \"timeZoneName\")?.value ?? \"GMT\";\n const match = name.match(/GMT([+-]\\d{2}:\\d{2})?/);\n return match?.[1] ?? \"+00:00\";\n } catch {\n return \"+00:00\";\n }\n}\n\n/** ISO instant for local midnight of `date` in `timezone`. */\nexport function zonedDayStartIso(date: string, timezone: string): string {\n let offset = timezoneOffsetIso(new Date(`${date}T12:00:00Z`), timezone);\n const candidate = new Date(`${date}T00:00:00${offset}`);\n const refined = timezoneOffsetIso(candidate, timezone);\n if (refined !== offset) {\n offset = refined;\n }\n return `${date}T00:00:00${offset}`;\n}\n\nexport function nextIsoDate(date: string): string {\n const parsed = new Date(`${date}T00:00:00Z`);\n parsed.setUTCDate(parsed.getUTCDate() + 1);\n return parsed.toISOString().slice(0, 10);\n}\n\n/**\n * Reject an invalid/unsupported IANA timezone loudly instead of silently\n * coercing to UTC and shifting every meeting's day window (AGENTS.md §39).\n */\nfunction assertValidTimezone(timezone: string): void {\n try {\n new Intl.DateTimeFormat(\"en-US\", { timeZone: timezone });\n } catch {\n throw new RangeError(\n `Invalid IANA timezone \"${timezone}\" for the Granola connector — check wearables.timezone.`,\n );\n }\n}\n\n/**\n * Half-open [createdAfter, createdBefore) UTC ISO bounds of a local day — the\n * window the Granola notes list filters on.\n */\nexport function granolaDayWindow(\n date: string,\n timezone: string,\n): { createdAfter: string; createdBefore: string } {\n assertValidTimezone(timezone);\n return {\n createdAfter: new Date(zonedDayStartIso(date, timezone)).toISOString(),\n createdBefore: new Date(zonedDayStartIso(nextIsoDate(date), timezone)).toISOString(),\n };\n}\n\nfunction trimmed(value: string | null | undefined): string | undefined {\n return typeof value === \"string\" && value.trim().length > 0 ? value.trim() : undefined;\n}\n\nfunction isoOrUndefined(value: string | null | undefined): string | undefined {\n if (typeof value !== \"string\" || value.trim().length === 0) return undefined;\n const ms = Date.parse(value);\n return Number.isFinite(ms) ? new Date(ms).toISOString() : undefined;\n}\n\nexport function noteToConversation(note: GranolaNote): WearableConversation {\n const event = note.calendar_event ?? undefined;\n const items = note.transcript ?? [];\n\n const segments: WearableTranscriptSegment[] = [];\n for (const item of items) {\n const text = trimmed(item.text);\n if (text === undefined) continue;\n const source = trimmed(item.speaker?.source);\n const label = trimmed(item.speaker?.diarization_label);\n const startIso = isoOrUndefined(item.start_time);\n const endIso = isoOrUndefined(item.end_time);\n // macOS marks the wearer's own audio as `microphone` (vs `speaker` for\n // other meeting audio). When a diarization_label is present (iOS single\n // stream) the source no longer distinguishes the wearer, so don't guess.\n const isWearer = source === \"microphone\" && label === undefined;\n segments.push({\n text,\n speakerKey: label ?? source ?? \"unknown\",\n ...(isWearer ? { isWearer: true } : {}),\n ...(startIso !== undefined ? { startIso } : {}),\n ...(endIso !== undefined ? { endIso } : {}),\n });\n }\n\n const summary = trimmed(note.summary_text) ?? trimmed(note.summary_markdown);\n\n // Note-only fallback: a summarized meeting with no transcript still anchors\n // recall for the day.\n if (segments.length === 0 && summary !== undefined) {\n segments.push({ text: summary, speakerKey: \"note\" });\n }\n\n const title = trimmed(event?.event_title) ?? trimmed(note.title);\n const startIso =\n isoOrUndefined(event?.scheduled_start_time) ??\n segments.find((segment) => segment.startIso !== undefined)?.startIso ??\n isoOrUndefined(note.created_at) ??\n \"\";\n const endIso =\n isoOrUndefined(event?.scheduled_end_time) ??\n [...segments].reverse().find((segment) => segment.endIso !== undefined)?.endIso;\n\n return {\n id: note.id,\n source: GRANOLA_SOURCE_ID,\n ...(title !== undefined ? { title } : {}),\n ...(summary !== undefined ? { summary } : {}),\n startIso,\n ...(endIso !== undefined ? { endIso } : {}),\n segments,\n };\n}\n"],"mappings":"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","names":["startIso","endIso"]}
1
+ {"version":3,"sources":["../src/index.ts","../src/client.ts","../src/normalize.ts"],"sourcesContent":["/**\n * @remnic/connector-granola — Granola meeting-notes connector.\n *\n * À-la-carte optional companion of @remnic/core: installing core alone\n * never pulls this in; core discovers it at runtime via a\n * computed-specifier dynamic import (see wearables/registry.ts) or via\n * a direct import of this module, which self-registers idempotently.\n *\n * API key: `wearables.sources.granola.apiKey`, else the\n * `REMNIC_GRANOLA_API_KEY` / `GRANOLA_API_KEY` environment variables\n * (checked in that order). Create a key under Settings → Connectors →\n * API keys in the Granola app (Business/Enterprise plans).\n */\n\nimport {\n selfRegisterWearableConnector,\n type WearableConnectorFactoryOptions,\n type WearableConnectorRegistration,\n type WearableConversation,\n type WearableFetchOptions,\n type WearableFetchPage,\n type WearableSourceConnector,\n} from \"@remnic/core\";\n\nimport { GranolaApiError, GranolaClient, type GranolaNote } from \"./client.js\";\nimport { GRANOLA_SOURCE_ID, granolaDayWindow, noteToConversation } from \"./normalize.js\";\n\nexport {\n GranolaClient,\n GranolaApiError,\n GRANOLA_DEFAULT_BASE_URL,\n NOTES_MAX_PAGE_SIZE,\n} from \"./client.js\";\nexport type {\n GranolaNote,\n GranolaTranscriptItem,\n GranolaSpeaker,\n GranolaCalendarEvent,\n GranolaUser,\n NotesPage,\n GranolaClientOptions,\n} from \"./client.js\";\nexport { GRANOLA_SOURCE_ID, granolaDayWindow, noteToConversation } from \"./normalize.js\";\n\nconst GRANOLA_DISPLAY_NAME = \"Granola\";\n\nexport function resolveGranolaApiKey(\n configuredKey: string | undefined,\n env: NodeJS.ProcessEnv = process.env,\n): string | undefined {\n if (typeof configuredKey === \"string\" && configuredKey.trim().length > 0) {\n return configuredKey.trim();\n }\n for (const name of [\"REMNIC_GRANOLA_API_KEY\", \"GRANOLA_API_KEY\"]) {\n const value = env[name];\n if (typeof value === \"string\" && value.trim().length > 0) {\n return value.trim();\n }\n }\n return undefined;\n}\n\nexport function createGranolaConnector(\n options: WearableConnectorFactoryOptions,\n): WearableSourceConnector {\n let client: GranolaClient | null = null;\n const getClient = (): GranolaClient => {\n if (!client) {\n client = new GranolaClient({\n apiKey: resolveGranolaApiKey(options.settings.apiKey) ?? \"\",\n baseUrl: options.settings.baseUrl,\n });\n }\n return client;\n };\n\n return {\n id: GRANOLA_SOURCE_ID,\n displayName: GRANOLA_DISPLAY_NAME,\n async verifyAuth(signal?: AbortSignal) {\n return getClient().verifyAuth(signal);\n },\n async fetchConversations(opts: WearableFetchOptions): Promise<WearableFetchPage> {\n const { createdAfter, createdBefore } = granolaDayWindow(opts.date, opts.timezone);\n const activeClient = getClient();\n const page = await activeClient.listNotes({\n createdAfter,\n createdBefore,\n cursor: opts.cursor,\n signal: opts.signal,\n });\n // The list returns summaries only; each note's transcript/summary/timing\n // needs a per-note fetch. Fetch sequentially to respect the 5 req/s rate\n // limit (the client also backs off on 429).\n const conversations: WearableConversation[] = [];\n for (const summary of page.notes) {\n let full: GranolaNote;\n try {\n full = await activeClient.getNote(summary.id, opts.signal);\n } catch (err) {\n // A note that 404s (deleted / no longer summarized) is gone, not a\n // backend outage — skip it. Any other failure is real; rethrow so it\n // reads as a source failure, not a quiet day (AGENTS.md §22).\n if (err instanceof GranolaApiError && err.status === 404) continue;\n throw err;\n }\n const conversation = noteToConversation(full);\n // Drop a record with no resolvable start rather than emit an empty\n // startIso that fails downstream parsing silently.\n if (conversation.startIso !== \"\") conversations.push(conversation);\n }\n return { conversations, nextCursor: page.nextCursor };\n },\n };\n}\n\nexport const wearableConnectorRegistration: WearableConnectorRegistration = {\n id: GRANOLA_SOURCE_ID,\n displayName: GRANOLA_DISPLAY_NAME,\n factory: createGranolaConnector,\n};\n\n/**\n * Idempotently register the connector with the core registry. Importing\n * this module registers it as a side effect; calling this again is safe\n * (returns false when already registered).\n */\nexport const ensureGranolaConnectorRegistered =\n selfRegisterWearableConnector(wearableConnectorRegistration);\n","/**\n * Minimal Granola public API client (raw fetch, no SDK).\n *\n * API verified against the official OpenAPI at\n * https://docs.granola.ai/api-reference/list-notes and\n * https://docs.granola.ai/api-reference/get-note (fetched 2026-07-21):\n * - Base URL: https://public-api.granola.ai\n * - Auth: `Authorization: Bearer grn_<key>`\n * - `GET /v1/notes?created_after=&created_before=&cursor=&page_size=` →\n * `{ notes: NoteSummary[], hasMore, cursor }` (page_size 1..30, default 10);\n * `created_after`/`created_before` accept date or date-time. The list only\n * returns notes that already have an AI summary + transcript.\n * - `GET /v1/notes/{id}?include=transcript` → full Note with `calendar_event`\n * (`scheduled_start_time`/`scheduled_end_time`), `attendees`, `summary_text`,\n * `summary_markdown`, and `transcript` items\n * `{ speaker: { source, diarization_label? }, text, start_time, end_time }`.\n * - Rate limits: 5 req/s sustained, 25 burst → 429 on excess.\n *\n * A non-2xx or network failure throws GranolaApiError (a backend failure); an\n * empty `notes` array is a real empty result, never conflated (AGENTS.md §22).\n */\n\nexport const GRANOLA_DEFAULT_BASE_URL = \"https://public-api.granola.ai\";\n\n/** Hard API maximum for `page_size` on the notes list. */\nexport const NOTES_MAX_PAGE_SIZE = 30;\n\nconst DEFAULT_TIMEOUT_MS = 30_000;\nconst MAX_RETRIES = 3;\nconst MAX_RETRY_DELAY_MS = 30_000;\n\nexport interface GranolaSpeaker {\n source?: string | null;\n diarization_label?: string | null;\n}\n\nexport interface GranolaTranscriptItem {\n speaker?: GranolaSpeaker | null;\n text?: string | null;\n start_time?: string | null;\n end_time?: string | null;\n}\n\nexport interface GranolaCalendarEvent {\n event_title?: string | null;\n organiser?: string | null;\n scheduled_start_time?: string | null;\n scheduled_end_time?: string | null;\n}\n\nexport interface GranolaUser {\n name?: string | null;\n email?: string | null;\n}\n\nexport interface GranolaNote {\n id: string;\n title?: string | null;\n created_at?: string | null;\n updated_at?: string | null;\n calendar_event?: GranolaCalendarEvent | null;\n attendees?: GranolaUser[] | null;\n summary_text?: string | null;\n summary_markdown?: string | null;\n transcript?: GranolaTranscriptItem[] | null;\n}\n\nexport interface NotesPage {\n notes: GranolaNote[];\n nextCursor: string | null;\n}\n\nexport interface GranolaClientOptions {\n apiKey: string;\n baseUrl?: string;\n fetchImpl?: typeof fetch;\n timeoutMs?: number;\n sleep?: (ms: number) => Promise<void>;\n}\n\nexport class GranolaApiError extends Error {\n constructor(\n message: string,\n readonly status?: number,\n ) {\n super(message);\n this.name = \"GranolaApiError\";\n }\n}\n\n/** Narrow unknown JSON to a plain object so field reads are actually checked. */\nfunction isRecord(value: unknown): value is Record<string, unknown> {\n return typeof value === \"object\" && value !== null;\n}\n\nexport class GranolaClient {\n private readonly apiKey: string;\n private readonly baseUrl: string;\n private readonly fetchImpl: typeof fetch;\n private readonly timeoutMs: number;\n private readonly sleep: (ms: number) => Promise<void>;\n\n constructor(options: GranolaClientOptions) {\n if (typeof options.apiKey !== \"string\" || options.apiKey.trim().length === 0) {\n throw new GranolaApiError(\n \"Granola API key is missing. Set wearables.sources.granola.apiKey \" +\n \"or the REMNIC_GRANOLA_API_KEY / GRANOLA_API_KEY environment variable \" +\n \"(create a key under Settings → Connectors → API keys in the Granola app; \" +\n \"requires a Business/Enterprise plan).\",\n );\n }\n this.apiKey = options.apiKey.trim();\n this.baseUrl = stripTrailingSlashes(options.baseUrl ?? GRANOLA_DEFAULT_BASE_URL);\n this.fetchImpl = options.fetchImpl ?? fetch;\n this.timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS;\n this.sleep = options.sleep ?? defaultSleep;\n }\n\n /** One page of note summaries in the half-open [createdAfter, createdBefore) window. */\n async listNotes(params: {\n createdAfter: string;\n createdBefore: string;\n cursor?: string | null;\n signal?: AbortSignal;\n }): Promise<NotesPage> {\n const search = new URLSearchParams({\n created_after: params.createdAfter,\n created_before: params.createdBefore,\n page_size: String(NOTES_MAX_PAGE_SIZE),\n });\n if (typeof params.cursor === \"string\" && params.cursor.length > 0) {\n search.set(\"cursor\", params.cursor);\n }\n const payload = await this.requestJson(`/v1/notes?${search.toString()}`, params.signal);\n const notesRaw = isRecord(payload) ? payload.notes : undefined;\n if (!Array.isArray(notesRaw)) {\n throw new GranolaApiError(\"Granola API returned an unexpected /v1/notes shape (missing notes array)\");\n }\n // `id` is required by the List Notes schema; a row missing it is a\n // backend/schema failure. Reject the page rather than silently dropping\n // rows and advancing the cursor over an incomplete day (§22).\n const notes: GranolaNote[] = [];\n for (const entry of notesRaw) {\n if (!isRecord(entry) || typeof entry.id !== \"string\") {\n throw new GranolaApiError(\"Granola API returned a note row without a string id\");\n }\n notes.push(entry as unknown as GranolaNote);\n }\n const hasMore = isRecord(payload) && payload.hasMore === true;\n const cursor = isRecord(payload) && typeof payload.cursor === \"string\" ? payload.cursor : null;\n if (hasMore && (cursor === null || cursor.length === 0)) {\n // A `hasMore: true` page with no usable cursor is a malformed pagination\n // response; fail loudly rather than silently sync a partial day (§22).\n throw new GranolaApiError(\"Granola reported hasMore=true but returned no pagination cursor\");\n }\n return { notes, nextCursor: hasMore ? cursor : null };\n }\n\n /** A single note with its transcript, calendar event, summary, and attendees. */\n async getNote(id: string, signal?: AbortSignal): Promise<GranolaNote> {\n const payload = await this.requestJson(\n `/v1/notes/${encodeURIComponent(id)}?include=transcript`,\n signal,\n );\n if (!isRecord(payload) || typeof payload.id !== \"string\") {\n throw new GranolaApiError(\"Granola API returned an unexpected note shape (missing id)\");\n }\n // Validated boundary: `id` is confirmed a string above; every other field\n // is optional and the normalizer re-checks each. Cast through `unknown`\n // because the external JSON shape can't be proven structurally here.\n return payload as unknown as GranolaNote;\n }\n\n /** Cheap auth probe. */\n async verifyAuth(signal?: AbortSignal): Promise<{ ok: boolean; detail?: string }> {\n try {\n await this.requestJson(\"/v1/notes?page_size=1\", signal);\n return { ok: true };\n } catch (err) {\n if (err instanceof GranolaApiError && (err.status === 401 || err.status === 403)) {\n return {\n ok: false,\n detail: \"Granola rejected the API key (401/403) — create a new key under Settings → Connectors → API keys\",\n };\n }\n return { ok: false, detail: err instanceof Error ? err.message : String(err) };\n }\n }\n\n private async requestJson(pathAndQuery: string, signal?: AbortSignal): Promise<unknown> {\n let lastError: unknown;\n for (let attempt = 0; attempt <= MAX_RETRIES; attempt++) {\n signal?.throwIfAborted();\n const timeoutSignal = AbortSignal.timeout(this.timeoutMs);\n const combined = signal ? AbortSignal.any([signal, timeoutSignal]) : timeoutSignal;\n let response: Response;\n try {\n response = await this.fetchImpl(`${this.baseUrl}${pathAndQuery}`, {\n method: \"GET\",\n headers: {\n Authorization: `Bearer ${this.apiKey}`,\n Accept: \"application/json\",\n },\n signal: combined,\n });\n } catch (err) {\n if (signal?.aborted) throw err;\n lastError = err;\n if (attempt < MAX_RETRIES) {\n await this.sleep(backoffMs(attempt));\n continue;\n }\n throw new GranolaApiError(\n `Granola API request failed after ${MAX_RETRIES + 1} attempts: ${describeNetworkError(err)}`,\n );\n }\n\n if (response.status === 429 || response.status >= 500) {\n discardBody(response);\n lastError = new GranolaApiError(`Granola API responded ${response.status}`, response.status);\n if (attempt < MAX_RETRIES) {\n await this.sleep(await retryDelayMs(response, attempt));\n continue;\n }\n throw lastError;\n }\n if (!response.ok) {\n discardBody(response);\n throw new GranolaApiError(\n `Granola API responded ${response.status} for ${pathAndQuery.split(\"?\")[0]}`,\n response.status,\n );\n }\n try {\n return await response.json();\n } catch {\n throw new GranolaApiError(\"Granola API returned a non-JSON body\");\n }\n }\n throw lastError instanceof Error ? lastError : new GranolaApiError(\"Granola API request failed\");\n }\n}\n\nfunction defaultSleep(ms: number): Promise<void> {\n const { promise, resolve } = Promise.withResolvers<void>();\n setTimeout(resolve, ms);\n return promise;\n}\n\n/**\n * Release the socket back to the pool on error paths: an unconsumed response\n * body pins the undici connection (stability review note).\n */\nfunction discardBody(response: Response): void {\n void response.body?.cancel().catch(() => {});\n}\n\nfunction describeNetworkError(err: unknown): string {\n if (isRecord(err)) {\n const name = typeof err.name === \"string\" ? err.name : \"Error\";\n const code = err.code;\n return typeof code === \"string\" ? `${name} (${code})` : name;\n }\n return \"network error\";\n}\n\n/** Loop instead of `/\\/+$/` — CodeQL js/polynomial-redos on user-set URLs. */\nfunction stripTrailingSlashes(value: string): string {\n let end = value.length;\n while (end > 0 && value.charCodeAt(end - 1) === 47) end--;\n return value.slice(0, end);\n}\n\nfunction backoffMs(attempt: number): number {\n return Math.min(MAX_RETRY_DELAY_MS, 1_000 * 2 ** attempt);\n}\n\nasync function retryDelayMs(response: Response, attempt: number): Promise<number> {\n const header = response.headers.get(\"retry-after\");\n if (header) {\n const seconds = Number(header);\n if (Number.isFinite(seconds) && seconds >= 0) {\n return Math.min(MAX_RETRY_DELAY_MS, seconds * 1_000);\n }\n const when = Date.parse(header);\n if (Number.isFinite(when)) {\n return Math.min(MAX_RETRY_DELAY_MS, Math.max(0, when - Date.now()));\n }\n }\n return backoffMs(attempt);\n}\n","/**\n * Normalize Granola notes into Remnic's provider-agnostic\n * `WearableConversation` shape, plus the timezone-aware day-window helper the\n * Granola `created_after`/`created_before` filters need.\n *\n * Granola transcript items carry absolute `start_time`/`end_time` (ISO) and a\n * `speaker.source` of `microphone` (the wearer's own captured audio) or\n * `speaker` (other meeting audio); iOS adds a `diarization_label`\n * (`Speaker A/B/...`). The wearables speaker registry owns final naming.\n * Meeting timing prefers the linked calendar event; notes with a summary but no\n * transcript degrade to a single `note` segment.\n */\n\nimport {\n activityDayWindow,\n type WearableConversation,\n type WearableTranscriptSegment,\n} from \"@remnic/core\";\n\nimport type { GranolaNote } from \"./client.js\";\n\nexport const GRANOLA_SOURCE_ID = \"granola\";\n\n/**\n * Half-open [createdAfter, createdBefore) UTC ISO bounds of a local day — the\n * window the Granola notes list filters on. Field names map to the Granola\n * API; the DST-aware window math is core's `activityDayWindow`.\n */\nexport function granolaDayWindow(\n date: string,\n timezone: string,\n): { createdAfter: string; createdBefore: string } {\n const { startUtc, endUtc } = activityDayWindow(date, timezone);\n return { createdAfter: startUtc, createdBefore: endUtc };\n}\n\nfunction trimmed(value: string | null | undefined): string | undefined {\n return typeof value === \"string\" && value.trim().length > 0 ? value.trim() : undefined;\n}\n\nfunction isoOrUndefined(value: string | null | undefined): string | undefined {\n if (typeof value !== \"string\" || value.trim().length === 0) return undefined;\n const ms = Date.parse(value);\n return Number.isFinite(ms) ? new Date(ms).toISOString() : undefined;\n}\n\nexport function noteToConversation(note: GranolaNote): WearableConversation {\n const event = note.calendar_event ?? undefined;\n const items = note.transcript ?? [];\n\n const segments: WearableTranscriptSegment[] = [];\n for (const item of items) {\n const text = trimmed(item.text);\n if (text === undefined) continue;\n const source = trimmed(item.speaker?.source);\n const label = trimmed(item.speaker?.diarization_label);\n const startIso = isoOrUndefined(item.start_time);\n const endIso = isoOrUndefined(item.end_time);\n // macOS marks the wearer's own audio as `microphone` (vs `speaker` for\n // other meeting audio). When a diarization_label is present (iOS single\n // stream) the source no longer distinguishes the wearer, so don't guess.\n const isWearer = source === \"microphone\" && label === undefined;\n segments.push({\n text,\n speakerKey: label ?? source ?? \"unknown\",\n ...(isWearer ? { isWearer: true } : {}),\n ...(startIso !== undefined ? { startIso } : {}),\n ...(endIso !== undefined ? { endIso } : {}),\n });\n }\n\n const summary = trimmed(note.summary_text) ?? trimmed(note.summary_markdown);\n\n // Note-only fallback: a summarized meeting with no transcript still anchors\n // recall for the day.\n if (segments.length === 0 && summary !== undefined) {\n segments.push({ text: summary, speakerKey: \"note\" });\n }\n\n const title = trimmed(event?.event_title) ?? trimmed(note.title);\n const startIso =\n isoOrUndefined(event?.scheduled_start_time) ??\n segments.find((segment) => segment.startIso !== undefined)?.startIso ??\n isoOrUndefined(note.created_at) ??\n \"\";\n const endIso =\n isoOrUndefined(event?.scheduled_end_time) ??\n [...segments].reverse().find((segment) => segment.endIso !== undefined)?.endIso;\n\n return {\n id: note.id,\n source: GRANOLA_SOURCE_ID,\n ...(title !== undefined ? { title } : {}),\n ...(summary !== undefined ? { summary } : {}),\n startIso,\n ...(endIso !== undefined ? { endIso } : {}),\n segments,\n };\n}\n"],"mappings":"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","names":["startIso","endIso"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@remnic/connector-granola",
3
- "version": "9.69.24",
3
+ "version": "9.69.26",
4
4
  "description": "Granola meeting-notes connector for Remnic — pull, clean, and remember meeting transcripts and notes",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -19,13 +19,13 @@
19
19
  "provenance": true
20
20
  },
21
21
  "peerDependencies": {
22
- "@remnic/core": "^9.69.24"
22
+ "@remnic/core": "^9.69.26"
23
23
  },
24
24
  "devDependencies": {
25
25
  "tsup": "^8.0.0",
26
26
  "typescript": "^5.7.0",
27
27
  "tsx": "^4.0.0",
28
- "@remnic/core": "9.69.24"
28
+ "@remnic/core": "9.69.26"
29
29
  },
30
30
  "license": "MIT",
31
31
  "repository": {