granola-toolkit 0.11.0 → 0.13.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 +123 -23
  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 }) {
@@ -885,6 +939,9 @@ function toJson(value) {
885
939
  function repeatIndent(level) {
886
940
  return " ".repeat(level);
887
941
  }
942
+ function escapeMarkdownText(text) {
943
+ return text.replace(/\\/g, "\\\\").replace(/([*_`[\]])/g, "\\$1");
944
+ }
888
945
  function renderInline(nodes = []) {
889
946
  return nodes.map((node) => renderInlineNode(node)).join("");
890
947
  }
@@ -895,6 +952,9 @@ function applyMarks(text, marks = []) {
895
952
  case "em": return `*${current}*`;
896
953
  case "code": return `\`${current}\``;
897
954
  case "strike": return `~~${current}~~`;
955
+ case "underline": return `<u>${current}</u>`;
956
+ case "subscript": return `<sub>${current}</sub>`;
957
+ case "superscript": return `<sup>${current}</sup>`;
898
958
  case "link": {
899
959
  const href = typeof mark.attrs?.href === "string" ? mark.attrs.href : void 0;
900
960
  return href ? `[${current}](${href})` : current;
@@ -905,8 +965,9 @@ function applyMarks(text, marks = []) {
905
965
  }
906
966
  function renderInlineNode(node) {
907
967
  switch (node.type) {
908
- case "text": return applyMarks(node.text ?? "", node.marks);
968
+ case "text": return applyMarks(escapeMarkdownText(node.text ?? ""), node.marks);
909
969
  case "hardBreak": return " \n";
970
+ case "mention": return applyMarks(escapeMarkdownText(typeof node.attrs?.label === "string" ? node.attrs.label : typeof node.attrs?.text === "string" ? node.attrs.text : typeof node.attrs?.name === "string" ? node.attrs.name : renderInline(node.content)), node.marks);
910
971
  default: return applyMarks(renderInline(node.content), node.marks);
911
972
  }
912
973
  }
@@ -914,21 +975,45 @@ function indentLines(value, level) {
914
975
  const indent = repeatIndent(level);
915
976
  return value.split("\n").map((line) => line.length === 0 ? line : `${indent}${line}`).join("\n");
916
977
  }
917
- function renderList(items, ordered, indentLevel) {
918
- return items.map((item, index) => renderListItem(item, ordered ? `${index + 1}.` : "-", indentLevel)).join("\n");
978
+ function renderList(items, ordered, indentLevel, start = 1) {
979
+ return items.map((item, index) => renderListItem(item, ordered ? `${start + index}.` : "-", indentLevel)).join("\n");
919
980
  }
920
981
  function renderListItem(node, marker, indentLevel) {
921
982
  const children = node.content ?? [];
922
983
  const blockChildren = children.filter((child) => child.type !== "bulletList" && child.type !== "orderedList");
923
984
  const nestedLists = children.filter((child) => child.type === "bulletList" || child.type === "orderedList");
924
985
  const mainText = blockChildren.map((child) => renderBlock(child, indentLevel + 1)).filter(Boolean).join("\n").trim();
925
- let output = `${`${repeatIndent(indentLevel)}${marker} `}${mainText || ""}`.trimEnd();
986
+ const prefix = `${repeatIndent(indentLevel)}${marker} `;
987
+ const continuationIndent = `${repeatIndent(indentLevel)}${" ".repeat(marker.length + 1)}`;
988
+ let output = `${prefix}${mainText.split("\n").map((line, index) => index === 0 ? line : `${continuationIndent}${line}`).join("\n") || ""}`.trimEnd();
926
989
  if (nestedLists.length > 0) {
927
990
  const nestedText = nestedLists.map((child) => renderBlock(child, indentLevel + 1)).filter(Boolean).map((value) => indentLines(value, 0)).join("\n");
928
991
  output = `${output}\n${nestedText}`;
929
992
  }
930
993
  return output;
931
994
  }
995
+ function renderTaskList(items, indentLevel) {
996
+ return items.map((item) => renderTaskItem(item, indentLevel)).join("\n");
997
+ }
998
+ function renderTaskItem(node, indentLevel) {
999
+ return renderListItem(node, node.attrs?.checked === true ? "[x]" : "[ ]", indentLevel);
1000
+ }
1001
+ function renderTableCell(node) {
1002
+ return renderBlocks(node.content ?? [], 0).replace(/\n+/g, " <br> ").replace(/\|/g, "\\|").trim();
1003
+ }
1004
+ function renderTable(node) {
1005
+ const rows = (node.content ?? []).map((row) => (row.content ?? []).map((cell) => renderTableCell(cell))).filter((row) => row.length > 0);
1006
+ if (rows.length === 0) return "";
1007
+ const header = rows[0];
1008
+ const body = rows.slice(1);
1009
+ const separator = header.map(() => "---");
1010
+ const lines = [`| ${header.map((cell) => cell || " ").join(" | ")} |`, `| ${separator.join(" | ")} |`];
1011
+ for (const row of body) {
1012
+ const padded = header.map((_, index) => row[index] ?? " ");
1013
+ lines.push(`| ${padded.join(" | ")} |`);
1014
+ }
1015
+ return lines.join("\n");
1016
+ }
932
1017
  function renderBlock(node, indentLevel) {
933
1018
  switch (node.type) {
934
1019
  case "heading": {
@@ -937,10 +1022,25 @@ function renderBlock(node, indentLevel) {
937
1022
  }
938
1023
  case "paragraph": return renderInline(node.content).trim();
939
1024
  case "bulletList": return renderList(node.content ?? [], false, indentLevel);
940
- case "orderedList": return renderList(node.content ?? [], true, indentLevel);
1025
+ case "orderedList": {
1026
+ const start = typeof node.attrs?.start === "number" ? node.attrs.start : 1;
1027
+ return renderList(node.content ?? [], true, indentLevel, start);
1028
+ }
941
1029
  case "listItem": return renderListItem(node, "-", indentLevel);
1030
+ case "taskList": return renderTaskList(node.content ?? [], indentLevel);
1031
+ case "taskItem": return renderTaskItem(node, indentLevel);
1032
+ case "table": return renderTable(node);
1033
+ case "tableRow": return (node.content ?? []).map((cell) => renderTableCell(cell)).join(" | ");
1034
+ case "tableCell":
1035
+ case "tableHeader": return renderTableCell(node);
942
1036
  case "blockquote": return renderBlocks(node.content ?? [], indentLevel).split("\n").map((line) => line ? `> ${line}` : ">").join("\n").trim();
943
- case "codeBlock": return `\`\`\`\n${extractPlainText(node).trimEnd()}\n\`\`\``;
1037
+ case "codeBlock": {
1038
+ const text = extractPlainText({
1039
+ type: "doc",
1040
+ content: node.content
1041
+ }).trimEnd();
1042
+ return `\`\`\`${typeof node.attrs?.language === "string" ? node.attrs.language.trim() : typeof node.attrs?.params === "string" ? node.attrs.params.trim() : ""}\n${text}\n\`\`\``;
1043
+ }
944
1044
  case "horizontalRule": return "---";
945
1045
  case "hardBreak": return "";
946
1046
  case "text": return renderInlineNode(node);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "granola-toolkit",
3
- "version": "0.11.0",
3
+ "version": "0.13.0",
4
4
  "description": "CLI toolkit for exporting and working with Granola notes and transcripts",
5
5
  "keywords": [
6
6
  "cli",