granola-toolkit 0.11.0 → 0.12.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 (3) hide show
  1. package/README.md +1 -0
  2. package/dist/cli.js +71 -17
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -214,6 +214,7 @@ Supported environment variables:
214
214
  - `TIMEOUT`
215
215
  - `CACHE_FILE`
216
216
  - `TRANSCRIPT_OUTPUT`
217
+ - `GRANOLA_CLIENT_VERSION`
217
218
 
218
219
  ## Development Checks
219
220
 
package/dist/cli.js CHANGED
@@ -540,13 +540,23 @@ function parseDocument(value) {
540
540
  }
541
541
  //#endregion
542
542
  //#region src/client/granola.ts
543
- const USER_AGENT = "Granola/5.354.0";
544
- const CLIENT_VERSION = "5.354.0";
543
+ const DEFAULT_CLIENT_VERSION = "5.354.0";
545
544
  const DOCUMENTS_URL = "https://api.granola.ai/v2/get-documents";
545
+ function resolveClientVersion(value) {
546
+ return value?.trim() || process.env.GRANOLA_CLIENT_VERSION?.trim() || DEFAULT_CLIENT_VERSION;
547
+ }
546
548
  var GranolaApiClient = class {
547
- constructor(httpClient, documentsUrl = DOCUMENTS_URL) {
549
+ clientVersion;
550
+ documentsUrl;
551
+ constructor(httpClient, options = DOCUMENTS_URL) {
548
552
  this.httpClient = httpClient;
549
- this.documentsUrl = documentsUrl;
553
+ if (typeof options === "string") {
554
+ this.documentsUrl = options;
555
+ this.clientVersion = resolveClientVersion();
556
+ return;
557
+ }
558
+ this.documentsUrl = options.documentsUrl ?? DOCUMENTS_URL;
559
+ this.clientVersion = resolveClientVersion(options.clientVersion);
550
560
  }
551
561
  async listDocuments(options) {
552
562
  const documents = [];
@@ -559,8 +569,8 @@ var GranolaApiClient = class {
559
569
  offset
560
570
  }, {
561
571
  headers: {
562
- "User-Agent": USER_AGENT,
563
- "X-Client-Version": CLIENT_VERSION
572
+ "User-Agent": `Granola/${this.clientVersion}`,
573
+ "X-Client-Version": this.clientVersion
564
574
  },
565
575
  timeoutMs: options.timeoutMs
566
576
  });
@@ -580,35 +590,79 @@ var GranolaApiClient = class {
580
590
  };
581
591
  //#endregion
582
592
  //#region src/client/http.ts
593
+ const RETRYABLE_STATUS_CODES = new Set([
594
+ 429,
595
+ 500,
596
+ 502,
597
+ 503,
598
+ 504
599
+ ]);
600
+ function sleep(delayMs) {
601
+ return new Promise((resolve) => {
602
+ setTimeout(resolve, delayMs);
603
+ });
604
+ }
605
+ function parseRetryAfter(headerValue) {
606
+ if (!headerValue?.trim()) return;
607
+ if (/^\d+$/.test(headerValue.trim())) return Number(headerValue.trim()) * 1e3;
608
+ const retryAt = Date.parse(headerValue);
609
+ if (Number.isNaN(retryAt)) return;
610
+ return Math.max(0, retryAt - Date.now());
611
+ }
583
612
  var AuthenticatedHttpClient = class {
584
613
  fetchImpl;
585
614
  constructor(options) {
586
615
  this.fetchImpl = options.fetchImpl ?? fetch;
587
616
  this.logger = options.logger;
617
+ this.maxRetries = options.maxRetries ?? 2;
618
+ this.retryBaseDelayMs = options.retryBaseDelayMs ?? 500;
619
+ this.retryMaxDelayMs = options.retryMaxDelayMs ?? 5e3;
620
+ this.sleepImpl = options.sleepImpl ?? sleep;
588
621
  this.tokenProvider = options.tokenProvider;
589
622
  }
590
623
  logger;
624
+ maxRetries;
625
+ retryBaseDelayMs;
626
+ retryMaxDelayMs;
627
+ sleepImpl;
591
628
  tokenProvider;
592
- async request(options) {
629
+ async retry(options, attempt, reason, response) {
630
+ const retryAfterMs = parseRetryAfter(response?.headers.get("retry-after") ?? null);
631
+ const delayMs = Math.min(retryAfterMs ?? this.retryBaseDelayMs * 2 ** attempt, this.retryMaxDelayMs);
632
+ this.logger?.warn?.(`${reason}; retrying in ${delayMs}ms (${attempt + 1}/${this.maxRetries})`);
633
+ await this.sleepImpl(delayMs);
634
+ return this.request(options, attempt + 1);
635
+ }
636
+ async request(options, attempt = 0) {
593
637
  const { retryOnUnauthorized = true, timeoutMs, url } = options;
594
638
  const accessToken = await this.tokenProvider.getAccessToken();
595
- const response = await this.fetchImpl(url, {
596
- body: options.body,
597
- headers: {
598
- ...options.headers,
599
- Authorization: `Bearer ${accessToken}`
600
- },
601
- method: options.method ?? "GET",
602
- signal: AbortSignal.timeout(timeoutMs)
603
- });
639
+ let response;
640
+ try {
641
+ response = await this.fetchImpl(url, {
642
+ body: options.body,
643
+ headers: {
644
+ ...options.headers,
645
+ Authorization: `Bearer ${accessToken}`
646
+ },
647
+ method: options.method ?? "GET",
648
+ signal: AbortSignal.timeout(timeoutMs)
649
+ });
650
+ } catch (error) {
651
+ if (attempt < this.maxRetries) {
652
+ const message = error instanceof Error ? error.message : String(error);
653
+ return this.retry(options, attempt, `request failed: ${message}`);
654
+ }
655
+ throw error;
656
+ }
604
657
  if (response.status === 401 && retryOnUnauthorized) {
605
658
  this.logger?.warn?.("request returned 401; invalidating token provider and retrying once");
606
659
  await this.tokenProvider.invalidate();
607
660
  return this.request({
608
661
  ...options,
609
662
  retryOnUnauthorized: false
610
- });
663
+ }, attempt);
611
664
  }
665
+ if (RETRYABLE_STATUS_CODES.has(response.status) && attempt < this.maxRetries) return this.retry(options, attempt, `request returned ${response.status} ${response.statusText || ""}`.trim(), response);
612
666
  return response;
613
667
  }
614
668
  async postJson(url, body, options = { timeoutMs: 3e4 }) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "granola-toolkit",
3
- "version": "0.11.0",
3
+ "version": "0.12.0",
4
4
  "description": "CLI toolkit for exporting and working with Granola notes and transcripts",
5
5
  "keywords": [
6
6
  "cli",