granola-toolkit 0.2.0 → 0.3.0

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.
Files changed (2) hide show
  1. package/dist/cli.js +132 -47
  2. package/package.json +5 -2
package/dist/cli.js CHANGED
@@ -171,10 +171,56 @@ function transcriptSpeakerLabel(segment) {
171
171
  return segment.source === "microphone" ? "You" : "System";
172
172
  }
173
173
  //#endregion
174
- //#region src/api.ts
175
- const USER_AGENT = "Granola/5.354.0";
176
- const CLIENT_VERSION = "5.354.0";
177
- const DOCUMENTS_URL = "https://api.granola.ai/v2/get-documents";
174
+ //#region src/client/auth.ts
175
+ function getAccessTokenFromSupabaseContents(supabaseContents) {
176
+ const wrapper = parseJsonString(supabaseContents);
177
+ if (!wrapper) throw new Error("failed to parse supabase.json");
178
+ const workosTokens = wrapper.workos_tokens;
179
+ let tokenPayload;
180
+ if (typeof workosTokens === "string") tokenPayload = parseJsonString(workosTokens);
181
+ else tokenPayload = asRecord(workosTokens);
182
+ const accessToken = tokenPayload ? stringValue(tokenPayload.access_token) : "";
183
+ if (!accessToken.trim()) throw new Error("access token not found in supabase.json");
184
+ return accessToken;
185
+ }
186
+ var SupabaseFileTokenSource = class {
187
+ constructor(filePath) {
188
+ this.filePath = filePath;
189
+ }
190
+ async loadAccessToken() {
191
+ return getAccessTokenFromSupabaseContents(await readFile(this.filePath, "utf8"));
192
+ }
193
+ };
194
+ var NoopTokenStore = class {
195
+ async clearToken() {}
196
+ async readToken() {}
197
+ async writeToken(_token) {}
198
+ };
199
+ var CachedTokenProvider = class {
200
+ #token;
201
+ constructor(source, store = new NoopTokenStore()) {
202
+ this.source = source;
203
+ this.store = store;
204
+ }
205
+ async getAccessToken() {
206
+ if (this.#token) return this.#token;
207
+ const storedToken = await this.store.readToken();
208
+ if (storedToken?.trim()) {
209
+ this.#token = storedToken;
210
+ return storedToken;
211
+ }
212
+ const token = await this.source.loadAccessToken();
213
+ this.#token = token;
214
+ await this.store.writeToken(token);
215
+ return token;
216
+ }
217
+ async invalidate() {
218
+ this.#token = void 0;
219
+ await this.store.clearToken();
220
+ }
221
+ };
222
+ //#endregion
223
+ //#region src/client/parsers.ts
178
224
  function parseProseMirrorDoc(value, options = {}) {
179
225
  if (value == null) return;
180
226
  if (typeof value === "string") {
@@ -209,17 +255,6 @@ function parseLastViewedPanel(value) {
209
255
  updatedAt: stringValue(panel.updated_at)
210
256
  };
211
257
  }
212
- function getAccessToken(supabaseContents) {
213
- const wrapper = parseJsonString(supabaseContents);
214
- if (!wrapper) throw new Error("failed to parse supabase.json");
215
- const workosTokens = wrapper.workos_tokens;
216
- let tokenPayload;
217
- if (typeof workosTokens === "string") tokenPayload = parseJsonString(workosTokens);
218
- else tokenPayload = asRecord(workosTokens);
219
- const accessToken = tokenPayload ? stringValue(tokenPayload.access_token) : "";
220
- if (!accessToken.trim()) throw new Error("access token not found in supabase.json");
221
- return accessToken;
222
- }
223
258
  function parseDocument(value) {
224
259
  const record = asRecord(value);
225
260
  if (!record) throw new Error("document payload is not an object");
@@ -235,44 +270,93 @@ function parseDocument(value) {
235
270
  updatedAt: stringValue(record.updated_at)
236
271
  };
237
272
  }
238
- async function fetchDocuments(options) {
239
- const fetchImpl = options.fetchImpl ?? fetch;
240
- const accessToken = getAccessToken(options.supabaseContents);
241
- const documents = [];
242
- const url = options.url ?? DOCUMENTS_URL;
243
- const limit = 100;
244
- let offset = 0;
245
- for (;;) {
246
- const signal = AbortSignal.timeout(options.timeoutMs);
247
- const response = await fetchImpl(url, {
248
- body: JSON.stringify({
273
+ //#endregion
274
+ //#region src/client/granola.ts
275
+ const USER_AGENT = "Granola/5.354.0";
276
+ const CLIENT_VERSION = "5.354.0";
277
+ const DOCUMENTS_URL = "https://api.granola.ai/v2/get-documents";
278
+ var GranolaApiClient = class {
279
+ constructor(httpClient, documentsUrl = DOCUMENTS_URL) {
280
+ this.httpClient = httpClient;
281
+ this.documentsUrl = documentsUrl;
282
+ }
283
+ async listDocuments(options) {
284
+ const documents = [];
285
+ const limit = options.limit ?? 100;
286
+ let offset = 0;
287
+ for (;;) {
288
+ const response = await this.httpClient.postJson(this.documentsUrl, {
249
289
  include_last_viewed_panel: true,
250
290
  limit,
251
291
  offset
252
- }),
292
+ }, {
293
+ headers: {
294
+ "User-Agent": USER_AGENT,
295
+ "X-Client-Version": CLIENT_VERSION
296
+ },
297
+ timeoutMs: options.timeoutMs
298
+ });
299
+ if (!response.ok) {
300
+ const body = (await response.text()).slice(0, 500);
301
+ throw new Error(`failed to get documents: ${response.status} ${response.statusText}${body ? `: ${body}` : ""}`);
302
+ }
303
+ const payload = await response.json();
304
+ if (!Array.isArray(payload.docs)) throw new Error("failed to parse documents response");
305
+ const page = payload.docs.map(parseDocument);
306
+ documents.push(...page);
307
+ if (page.length < limit) break;
308
+ offset += limit;
309
+ }
310
+ return documents;
311
+ }
312
+ };
313
+ //#endregion
314
+ //#region src/client/http.ts
315
+ var AuthenticatedHttpClient = class {
316
+ fetchImpl;
317
+ constructor(options) {
318
+ this.fetchImpl = options.fetchImpl ?? fetch;
319
+ this.logger = options.logger;
320
+ this.tokenProvider = options.tokenProvider;
321
+ }
322
+ logger;
323
+ tokenProvider;
324
+ async request(options) {
325
+ const { retryOnUnauthorized = true, timeoutMs, url } = options;
326
+ const accessToken = await this.tokenProvider.getAccessToken();
327
+ const response = await this.fetchImpl(url, {
328
+ body: options.body,
329
+ headers: {
330
+ ...options.headers,
331
+ Authorization: `Bearer ${accessToken}`
332
+ },
333
+ method: options.method ?? "GET",
334
+ signal: AbortSignal.timeout(timeoutMs)
335
+ });
336
+ if (response.status === 401 && retryOnUnauthorized) {
337
+ this.logger?.warn?.("request returned 401; invalidating token provider and retrying once");
338
+ await this.tokenProvider.invalidate();
339
+ return this.request({
340
+ ...options,
341
+ retryOnUnauthorized: false
342
+ });
343
+ }
344
+ return response;
345
+ }
346
+ async postJson(url, body, options = { timeoutMs: 3e4 }) {
347
+ return this.request({
348
+ ...options,
349
+ body: JSON.stringify(body),
253
350
  headers: {
254
351
  Accept: "*/*",
255
- Authorization: `Bearer ${accessToken}`,
256
352
  "Content-Type": "application/json",
257
- "User-Agent": USER_AGENT,
258
- "X-Client-Version": CLIENT_VERSION
353
+ ...options.headers
259
354
  },
260
355
  method: "POST",
261
- signal
356
+ url
262
357
  });
263
- if (!response.ok) {
264
- const body = (await response.text()).slice(0, 500);
265
- throw new Error(`failed to get documents: ${response.status} ${response.statusText}${body ? `: ${body}` : ""}`);
266
- }
267
- const payload = await response.json();
268
- if (!Array.isArray(payload.docs)) throw new Error("failed to parse documents response");
269
- const page = payload.docs.map(parseDocument);
270
- documents.push(...page);
271
- if (page.length < limit) break;
272
- offset += limit;
273
358
  }
274
- return documents;
275
- }
359
+ };
276
360
  //#endregion
277
361
  //#region src/config.ts
278
362
  function pickString(value) {
@@ -516,10 +600,11 @@ const notesCommand = {
516
600
  debug(config.debug, "timeoutMs", config.notes.timeoutMs);
517
601
  debug(config.debug, "output", config.notes.output);
518
602
  console.log("Fetching documents from Granola API...");
519
- const documents = await fetchDocuments({
520
- supabaseContents: await readFile(config.supabase, "utf8"),
521
- timeoutMs: config.notes.timeoutMs
522
- });
603
+ const tokenProvider = new CachedTokenProvider(new SupabaseFileTokenSource(config.supabase), new NoopTokenStore());
604
+ const documents = await new GranolaApiClient(new AuthenticatedHttpClient({
605
+ logger: console,
606
+ tokenProvider
607
+ })).listDocuments({ timeoutMs: config.notes.timeoutMs });
523
608
  console.log(`Exporting ${documents.length} notes to ${config.notes.output}...`);
524
609
  const written = await writeNotes(documents, config.notes.output);
525
610
  console.log("✓ Export completed successfully");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "granola-toolkit",
3
- "version": "0.2.0",
3
+ "version": "0.3.0",
4
4
  "description": "CLI toolkit for exporting and working with Granola notes and transcripts",
5
5
  "keywords": [
6
6
  "cli",
@@ -37,8 +37,11 @@
37
37
  "fmt": "vp fmt",
38
38
  "lint": "vp lint",
39
39
  "pack:dry-run": "npm pack --dry-run",
40
- "prepublishOnly": "vp pack",
40
+ "prepublishOnly": "node scripts/prepublish.mjs",
41
41
  "release": "node scripts/release.mjs",
42
+ "release:major": "node scripts/release.mjs major",
43
+ "release:minor": "node scripts/release.mjs minor",
44
+ "release:patch": "node scripts/release.mjs patch",
42
45
  "start": "node dist/cli.js",
43
46
  "notes": "node dist/cli.js notes",
44
47
  "transcripts": "node dist/cli.js transcripts",