@remnic/connector-granola 9.69.26 → 9.69.28

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
@@ -1,4 +1,5 @@
1
1
  import { WearableConversation, WearableConnectorFactoryOptions, WearableSourceConnector, WearableConnectorRegistration } from '@remnic/core';
2
+ import { ConnectorApiError } from '@remnic/core/http-retry';
2
3
 
3
4
  /**
4
5
  * Minimal Granola public API client (raw fetch, no SDK).
@@ -21,6 +22,7 @@ import { WearableConversation, WearableConnectorFactoryOptions, WearableSourceCo
21
22
  * A non-2xx or network failure throws GranolaApiError (a backend failure); an
22
23
  * empty `notes` array is a real empty result, never conflated (AGENTS.md §22).
23
24
  */
25
+
24
26
  declare const GRANOLA_DEFAULT_BASE_URL = "https://public-api.granola.ai";
25
27
  /** Hard API maximum for `page_size` on the notes list. */
26
28
  declare const NOTES_MAX_PAGE_SIZE = 30;
@@ -66,9 +68,8 @@ interface GranolaClientOptions {
66
68
  timeoutMs?: number;
67
69
  sleep?: (ms: number) => Promise<void>;
68
70
  }
69
- declare class GranolaApiError extends Error {
70
- readonly status?: number | undefined;
71
- constructor(message: string, status?: number | undefined);
71
+ declare class GranolaApiError extends ConnectorApiError {
72
+ constructor(message: string, status?: number);
72
73
  }
73
74
  declare class GranolaClient {
74
75
  private readonly apiKey;
package/dist/index.js CHANGED
@@ -6,18 +6,21 @@ import {
6
6
  } from "@remnic/core";
7
7
 
8
8
  // src/client.ts
9
+ import {
10
+ ConnectorApiError,
11
+ describeNetworkError,
12
+ discardResponseBody,
13
+ retryingFetch,
14
+ stripTrailingSlashes
15
+ } from "@remnic/core/http-retry";
9
16
  var GRANOLA_DEFAULT_BASE_URL = "https://public-api.granola.ai";
10
17
  var NOTES_MAX_PAGE_SIZE = 30;
11
18
  var DEFAULT_TIMEOUT_MS = 3e4;
12
- var MAX_RETRIES = 3;
13
- var MAX_RETRY_DELAY_MS = 3e4;
14
- var GranolaApiError = class extends Error {
19
+ var GranolaApiError = class extends ConnectorApiError {
15
20
  constructor(message, status) {
16
- super(message);
17
- this.status = status;
21
+ super(message, status);
18
22
  this.name = "GranolaApiError";
19
23
  }
20
- status;
21
24
  };
22
25
  function isRecord(value) {
23
26
  return typeof value === "object" && value !== null;
@@ -38,7 +41,7 @@ var GranolaClient = class {
38
41
  this.baseUrl = stripTrailingSlashes(options.baseUrl ?? GRANOLA_DEFAULT_BASE_URL);
39
42
  this.fetchImpl = options.fetchImpl ?? fetch;
40
43
  this.timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS;
41
- this.sleep = options.sleep ?? defaultSleep;
44
+ this.sleep = options.sleep;
42
45
  }
43
46
  /** One page of note summaries in the half-open [createdAfter, createdBefore) window. */
44
47
  async listNotes(params) {
@@ -96,96 +99,37 @@ var GranolaClient = class {
96
99
  }
97
100
  }
98
101
  async requestJson(pathAndQuery, signal) {
99
- let lastError;
100
- for (let attempt = 0; attempt <= MAX_RETRIES; attempt++) {
101
- signal?.throwIfAborted();
102
- const timeoutSignal = AbortSignal.timeout(this.timeoutMs);
103
- const combined = signal ? AbortSignal.any([signal, timeoutSignal]) : timeoutSignal;
104
- let response;
105
- try {
106
- response = await this.fetchImpl(`${this.baseUrl}${pathAndQuery}`, {
107
- method: "GET",
108
- headers: {
109
- Authorization: `Bearer ${this.apiKey}`,
110
- Accept: "application/json"
111
- },
112
- signal: combined
113
- });
114
- } catch (err) {
115
- if (signal?.aborted) throw err;
116
- lastError = err;
117
- if (attempt < MAX_RETRIES) {
118
- await this.sleep(backoffMs(attempt));
119
- continue;
120
- }
121
- throw new GranolaApiError(
122
- `Granola API request failed after ${MAX_RETRIES + 1} attempts: ${describeNetworkError(err)}`
123
- );
124
- }
125
- if (response.status === 429 || response.status >= 500) {
126
- discardBody(response);
127
- lastError = new GranolaApiError(`Granola API responded ${response.status}`, response.status);
128
- if (attempt < MAX_RETRIES) {
129
- await this.sleep(await retryDelayMs(response, attempt));
130
- continue;
102
+ const response = await retryingFetch(`${this.baseUrl}${pathAndQuery}`, {
103
+ init: {
104
+ method: "GET",
105
+ headers: {
106
+ Authorization: `Bearer ${this.apiKey}`,
107
+ Accept: "application/json"
131
108
  }
132
- throw lastError;
133
- }
134
- if (!response.ok) {
135
- discardBody(response);
136
- throw new GranolaApiError(
137
- `Granola API responded ${response.status} for ${pathAndQuery.split("?")[0]}`,
138
- response.status
139
- );
140
- }
141
- try {
142
- return await response.json();
143
- } catch {
144
- throw new GranolaApiError("Granola API returned a non-JSON body");
145
- }
146
- }
147
- throw lastError instanceof Error ? lastError : new GranolaApiError("Granola API request failed");
148
- }
149
- };
150
- function defaultSleep(ms) {
151
- const { promise, resolve } = Promise.withResolvers();
152
- setTimeout(resolve, ms);
153
- return promise;
154
- }
155
- function discardBody(response) {
156
- void response.body?.cancel().catch(() => {
157
- });
158
- }
159
- function describeNetworkError(err) {
160
- if (isRecord(err)) {
161
- const name = typeof err.name === "string" ? err.name : "Error";
162
- const code = err.code;
163
- return typeof code === "string" ? `${name} (${code})` : name;
164
- }
165
- return "network error";
166
- }
167
- function stripTrailingSlashes(value) {
168
- let end = value.length;
169
- while (end > 0 && value.charCodeAt(end - 1) === 47) end--;
170
- return value.slice(0, end);
171
- }
172
- function backoffMs(attempt) {
173
- return Math.min(MAX_RETRY_DELAY_MS, 1e3 * 2 ** attempt);
174
- }
175
- async function retryDelayMs(response, attempt) {
176
- const header = response.headers.get("retry-after");
177
- if (header) {
178
- const seconds = Number(header);
179
- if (Number.isFinite(seconds) && seconds >= 0) {
180
- return Math.min(MAX_RETRY_DELAY_MS, seconds * 1e3);
109
+ },
110
+ fetchImpl: this.fetchImpl,
111
+ sleep: this.sleep,
112
+ signal,
113
+ timeoutMs: this.timeoutMs,
114
+ networkError: (err, attempts) => new GranolaApiError(
115
+ `Granola API request failed after ${attempts} attempts: ${describeNetworkError(err)}`
116
+ ),
117
+ retryableError: (retryable) => new GranolaApiError(`Granola API responded ${retryable.status}`, retryable.status)
118
+ });
119
+ if (!response.ok) {
120
+ discardResponseBody(response);
121
+ throw new GranolaApiError(
122
+ `Granola API responded ${response.status} for ${pathAndQuery.split("?")[0]}`,
123
+ response.status
124
+ );
181
125
  }
182
- const when = Date.parse(header);
183
- if (Number.isFinite(when)) {
184
- return Math.min(MAX_RETRY_DELAY_MS, Math.max(0, when - Date.now()));
126
+ try {
127
+ return await response.json();
128
+ } catch {
129
+ throw new GranolaApiError("Granola API returned a non-JSON body");
185
130
  }
186
131
  }
187
- return backoffMs(attempt);
188
- }
132
+ };
189
133
 
190
134
  // src/normalize.ts
191
135
  import {
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 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"]}
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\nimport {\n ConnectorApiError,\n describeNetworkError,\n discardResponseBody,\n retryingFetch,\n stripTrailingSlashes,\n} from \"@remnic/core/http-retry\";\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;\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 ConnectorApiError {\n constructor(\n message: string,\n status?: number,\n ) {\n super(message, status);\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>) | undefined;\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;\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 const response = await retryingFetch(`${this.baseUrl}${pathAndQuery}`, {\n init: {\n method: \"GET\",\n headers: {\n Authorization: `Bearer ${this.apiKey}`,\n Accept: \"application/json\",\n },\n },\n fetchImpl: this.fetchImpl,\n sleep: this.sleep,\n signal,\n timeoutMs: this.timeoutMs,\n networkError: (err, attempts) =>\n new GranolaApiError(\n `Granola API request failed after ${attempts} attempts: ${describeNetworkError(err)}`,\n ),\n retryableError: (retryable) =>\n new GranolaApiError(`Granola API responded ${retryable.status}`, retryable.status),\n });\n if (!response.ok) {\n discardResponseBody(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}\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.26",
3
+ "version": "9.69.28",
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.26"
22
+ "@remnic/core": "^9.69.28"
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.26"
28
+ "@remnic/core": "9.69.28"
29
29
  },
30
30
  "license": "MIT",
31
31
  "repository": {