granola-toolkit 0.12.0 → 0.14.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.
- package/README.md +8 -1
- package/dist/cli.js +133 -18
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -81,6 +81,8 @@ Inspect individual meetings:
|
|
|
81
81
|
granola meeting list --limit 10
|
|
82
82
|
granola meeting list --search planning
|
|
83
83
|
granola meeting view 1234abcd
|
|
84
|
+
granola meeting notes 1234abcd
|
|
85
|
+
granola meeting transcript 1234abcd --format json
|
|
84
86
|
granola meeting export 1234abcd --format yaml
|
|
85
87
|
```
|
|
86
88
|
|
|
@@ -143,7 +145,7 @@ The flow is:
|
|
|
143
145
|
2. fetch documents from Granola's API
|
|
144
146
|
3. optionally load the local cache for transcript data
|
|
145
147
|
4. resolve a meeting by full id or unique id prefix
|
|
146
|
-
5. render either a list, a
|
|
148
|
+
5. render either a list, a combined meeting view, focused notes/transcript output, or a machine-readable export bundle
|
|
147
149
|
|
|
148
150
|
The human-readable `view` command shows:
|
|
149
151
|
|
|
@@ -151,6 +153,11 @@ The human-readable `view` command shows:
|
|
|
151
153
|
- the selected notes content
|
|
152
154
|
- transcript lines when the local cache is available
|
|
153
155
|
|
|
156
|
+
The focused meeting subcommands are:
|
|
157
|
+
|
|
158
|
+
- `meeting notes` for just the selected note output
|
|
159
|
+
- `meeting transcript` for just the selected transcript output
|
|
160
|
+
|
|
154
161
|
The machine-readable `export` command includes:
|
|
155
162
|
|
|
156
163
|
- a meeting summary
|
package/dist/cli.js
CHANGED
|
@@ -327,14 +327,22 @@ var StoredSessionTokenProvider = class {
|
|
|
327
327
|
}
|
|
328
328
|
async invalidate() {
|
|
329
329
|
const session = await this.loadSession().catch(() => void 0);
|
|
330
|
-
if (session?.refreshToken && session.clientId) {
|
|
330
|
+
if (session?.refreshToken && session.clientId) try {
|
|
331
331
|
const refreshedSession = await refreshGranolaSession(session, this.options.fetchImpl);
|
|
332
332
|
this.#session = refreshedSession;
|
|
333
333
|
await this.store.writeSession(refreshedSession);
|
|
334
334
|
return;
|
|
335
|
+
} catch {
|
|
336
|
+
if (!this.options.source) {
|
|
337
|
+
this.#session = void 0;
|
|
338
|
+
await this.store.clearSession();
|
|
339
|
+
throw new Error("failed to refresh stored Granola session");
|
|
340
|
+
}
|
|
335
341
|
}
|
|
336
342
|
if (this.options.source) {
|
|
337
|
-
|
|
343
|
+
const sourcedSession = await this.options.source.loadSession();
|
|
344
|
+
this.#session = sourcedSession;
|
|
345
|
+
await this.store.writeSession(sourcedSession);
|
|
338
346
|
return;
|
|
339
347
|
}
|
|
340
348
|
this.#session = void 0;
|
|
@@ -939,6 +947,9 @@ function toJson(value) {
|
|
|
939
947
|
function repeatIndent(level) {
|
|
940
948
|
return " ".repeat(level);
|
|
941
949
|
}
|
|
950
|
+
function escapeMarkdownText(text) {
|
|
951
|
+
return text.replace(/\\/g, "\\\\").replace(/([*_`[\]])/g, "\\$1");
|
|
952
|
+
}
|
|
942
953
|
function renderInline(nodes = []) {
|
|
943
954
|
return nodes.map((node) => renderInlineNode(node)).join("");
|
|
944
955
|
}
|
|
@@ -949,6 +960,9 @@ function applyMarks(text, marks = []) {
|
|
|
949
960
|
case "em": return `*${current}*`;
|
|
950
961
|
case "code": return `\`${current}\``;
|
|
951
962
|
case "strike": return `~~${current}~~`;
|
|
963
|
+
case "underline": return `<u>${current}</u>`;
|
|
964
|
+
case "subscript": return `<sub>${current}</sub>`;
|
|
965
|
+
case "superscript": return `<sup>${current}</sup>`;
|
|
952
966
|
case "link": {
|
|
953
967
|
const href = typeof mark.attrs?.href === "string" ? mark.attrs.href : void 0;
|
|
954
968
|
return href ? `[${current}](${href})` : current;
|
|
@@ -959,8 +973,9 @@ function applyMarks(text, marks = []) {
|
|
|
959
973
|
}
|
|
960
974
|
function renderInlineNode(node) {
|
|
961
975
|
switch (node.type) {
|
|
962
|
-
case "text": return applyMarks(node.text ?? "", node.marks);
|
|
976
|
+
case "text": return applyMarks(escapeMarkdownText(node.text ?? ""), node.marks);
|
|
963
977
|
case "hardBreak": return " \n";
|
|
978
|
+
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);
|
|
964
979
|
default: return applyMarks(renderInline(node.content), node.marks);
|
|
965
980
|
}
|
|
966
981
|
}
|
|
@@ -968,21 +983,45 @@ function indentLines(value, level) {
|
|
|
968
983
|
const indent = repeatIndent(level);
|
|
969
984
|
return value.split("\n").map((line) => line.length === 0 ? line : `${indent}${line}`).join("\n");
|
|
970
985
|
}
|
|
971
|
-
function renderList(items, ordered, indentLevel) {
|
|
972
|
-
return items.map((item, index) => renderListItem(item, ordered ? `${
|
|
986
|
+
function renderList(items, ordered, indentLevel, start = 1) {
|
|
987
|
+
return items.map((item, index) => renderListItem(item, ordered ? `${start + index}.` : "-", indentLevel)).join("\n");
|
|
973
988
|
}
|
|
974
989
|
function renderListItem(node, marker, indentLevel) {
|
|
975
990
|
const children = node.content ?? [];
|
|
976
991
|
const blockChildren = children.filter((child) => child.type !== "bulletList" && child.type !== "orderedList");
|
|
977
992
|
const nestedLists = children.filter((child) => child.type === "bulletList" || child.type === "orderedList");
|
|
978
993
|
const mainText = blockChildren.map((child) => renderBlock(child, indentLevel + 1)).filter(Boolean).join("\n").trim();
|
|
979
|
-
|
|
994
|
+
const prefix = `${repeatIndent(indentLevel)}${marker} `;
|
|
995
|
+
const continuationIndent = `${repeatIndent(indentLevel)}${" ".repeat(marker.length + 1)}`;
|
|
996
|
+
let output = `${prefix}${mainText.split("\n").map((line, index) => index === 0 ? line : `${continuationIndent}${line}`).join("\n") || ""}`.trimEnd();
|
|
980
997
|
if (nestedLists.length > 0) {
|
|
981
998
|
const nestedText = nestedLists.map((child) => renderBlock(child, indentLevel + 1)).filter(Boolean).map((value) => indentLines(value, 0)).join("\n");
|
|
982
999
|
output = `${output}\n${nestedText}`;
|
|
983
1000
|
}
|
|
984
1001
|
return output;
|
|
985
1002
|
}
|
|
1003
|
+
function renderTaskList(items, indentLevel) {
|
|
1004
|
+
return items.map((item) => renderTaskItem(item, indentLevel)).join("\n");
|
|
1005
|
+
}
|
|
1006
|
+
function renderTaskItem(node, indentLevel) {
|
|
1007
|
+
return renderListItem(node, node.attrs?.checked === true ? "[x]" : "[ ]", indentLevel);
|
|
1008
|
+
}
|
|
1009
|
+
function renderTableCell(node) {
|
|
1010
|
+
return renderBlocks(node.content ?? [], 0).replace(/\n+/g, " <br> ").replace(/\|/g, "\\|").trim();
|
|
1011
|
+
}
|
|
1012
|
+
function renderTable(node) {
|
|
1013
|
+
const rows = (node.content ?? []).map((row) => (row.content ?? []).map((cell) => renderTableCell(cell))).filter((row) => row.length > 0);
|
|
1014
|
+
if (rows.length === 0) return "";
|
|
1015
|
+
const header = rows[0];
|
|
1016
|
+
const body = rows.slice(1);
|
|
1017
|
+
const separator = header.map(() => "---");
|
|
1018
|
+
const lines = [`| ${header.map((cell) => cell || " ").join(" | ")} |`, `| ${separator.join(" | ")} |`];
|
|
1019
|
+
for (const row of body) {
|
|
1020
|
+
const padded = header.map((_, index) => row[index] ?? " ");
|
|
1021
|
+
lines.push(`| ${padded.join(" | ")} |`);
|
|
1022
|
+
}
|
|
1023
|
+
return lines.join("\n");
|
|
1024
|
+
}
|
|
986
1025
|
function renderBlock(node, indentLevel) {
|
|
987
1026
|
switch (node.type) {
|
|
988
1027
|
case "heading": {
|
|
@@ -991,10 +1030,25 @@ function renderBlock(node, indentLevel) {
|
|
|
991
1030
|
}
|
|
992
1031
|
case "paragraph": return renderInline(node.content).trim();
|
|
993
1032
|
case "bulletList": return renderList(node.content ?? [], false, indentLevel);
|
|
994
|
-
case "orderedList":
|
|
1033
|
+
case "orderedList": {
|
|
1034
|
+
const start = typeof node.attrs?.start === "number" ? node.attrs.start : 1;
|
|
1035
|
+
return renderList(node.content ?? [], true, indentLevel, start);
|
|
1036
|
+
}
|
|
995
1037
|
case "listItem": return renderListItem(node, "-", indentLevel);
|
|
1038
|
+
case "taskList": return renderTaskList(node.content ?? [], indentLevel);
|
|
1039
|
+
case "taskItem": return renderTaskItem(node, indentLevel);
|
|
1040
|
+
case "table": return renderTable(node);
|
|
1041
|
+
case "tableRow": return (node.content ?? []).map((cell) => renderTableCell(cell)).join(" | ");
|
|
1042
|
+
case "tableCell":
|
|
1043
|
+
case "tableHeader": return renderTableCell(node);
|
|
996
1044
|
case "blockquote": return renderBlocks(node.content ?? [], indentLevel).split("\n").map((line) => line ? `> ${line}` : ">").join("\n").trim();
|
|
997
|
-
case "codeBlock":
|
|
1045
|
+
case "codeBlock": {
|
|
1046
|
+
const text = extractPlainText({
|
|
1047
|
+
type: "doc",
|
|
1048
|
+
content: node.content
|
|
1049
|
+
}).trimEnd();
|
|
1050
|
+
return `\`\`\`${typeof node.attrs?.language === "string" ? node.attrs.language.trim() : typeof node.attrs?.params === "string" ? node.attrs.params.trim() : ""}\n${text}\n\`\`\``;
|
|
1051
|
+
}
|
|
998
1052
|
case "horizontalRule": return "---";
|
|
999
1053
|
case "hardBreak": return "";
|
|
1000
1054
|
case "text": return renderInlineNode(node);
|
|
@@ -1315,6 +1369,7 @@ function buildMeetingTranscript(document, cacheData) {
|
|
|
1315
1369
|
loaded: false,
|
|
1316
1370
|
segmentCount: 0,
|
|
1317
1371
|
transcript: null,
|
|
1372
|
+
transcriptRecord: null,
|
|
1318
1373
|
transcriptText: null
|
|
1319
1374
|
};
|
|
1320
1375
|
const rawSegments = cacheData.transcripts[document.id] ?? [];
|
|
@@ -1323,6 +1378,7 @@ function buildMeetingTranscript(document, cacheData) {
|
|
|
1323
1378
|
loaded: true,
|
|
1324
1379
|
segmentCount: 0,
|
|
1325
1380
|
transcript: null,
|
|
1381
|
+
transcriptRecord: null,
|
|
1326
1382
|
transcriptText: null
|
|
1327
1383
|
};
|
|
1328
1384
|
const transcript = buildTranscriptExport(cacheDocumentForMeeting(document, cacheData), normalisedSegments, rawSegments);
|
|
@@ -1330,6 +1386,7 @@ function buildMeetingTranscript(document, cacheData) {
|
|
|
1330
1386
|
loaded: true,
|
|
1331
1387
|
segmentCount: transcript.segments.length,
|
|
1332
1388
|
transcript: serialiseTranscript(transcript),
|
|
1389
|
+
transcriptRecord: transcript,
|
|
1333
1390
|
transcriptText: renderTranscriptExport(transcript, "text")
|
|
1334
1391
|
};
|
|
1335
1392
|
}
|
|
@@ -1458,6 +1515,14 @@ function renderMeetingExport(record, format = "json") {
|
|
|
1458
1515
|
case "yaml": return toYaml(record);
|
|
1459
1516
|
}
|
|
1460
1517
|
}
|
|
1518
|
+
function renderMeetingNotes(document, format = "markdown") {
|
|
1519
|
+
return renderNoteExport(buildNoteExport(document), format);
|
|
1520
|
+
}
|
|
1521
|
+
function renderMeetingTranscript(document, cacheData, format = "text") {
|
|
1522
|
+
const transcript = buildMeetingTranscript(document, cacheData).transcriptRecord;
|
|
1523
|
+
if (!transcript) return "";
|
|
1524
|
+
return renderTranscriptExport(transcript, format);
|
|
1525
|
+
}
|
|
1461
1526
|
//#endregion
|
|
1462
1527
|
//#region src/commands/shared.ts
|
|
1463
1528
|
function debug(enabled, ...values) {
|
|
@@ -1469,16 +1534,18 @@ function meetingHelp() {
|
|
|
1469
1534
|
return `Granola meeting
|
|
1470
1535
|
|
|
1471
1536
|
Usage:
|
|
1472
|
-
granola meeting <list|view|export> [options]
|
|
1537
|
+
granola meeting <list|view|export|notes|transcript> [options]
|
|
1473
1538
|
|
|
1474
1539
|
Subcommands:
|
|
1475
1540
|
list List meetings from the Granola API
|
|
1476
1541
|
view <id> Show a single meeting with notes and transcript text
|
|
1477
1542
|
export <id> Export a single meeting as JSON or YAML
|
|
1543
|
+
notes <id> Show a single meeting's notes
|
|
1544
|
+
transcript <id> Show a single meeting's transcript
|
|
1478
1545
|
|
|
1479
1546
|
Options:
|
|
1480
1547
|
--cache <path> Path to Granola cache JSON for transcript data
|
|
1481
|
-
--format <value> list/view: text, json, yaml; export: json, yaml
|
|
1548
|
+
--format <value> list/view: text, json, yaml; export: json, yaml; notes: markdown, json, yaml, raw; transcript: text, json, yaml, raw
|
|
1482
1549
|
--limit <n> Number of meetings for list (default: 20)
|
|
1483
1550
|
--search <query> Filter list by title, id, or tag
|
|
1484
1551
|
--timeout <value> Request timeout, e.g. 2m, 30s, 120000 (default: 2m)
|
|
@@ -1514,6 +1581,26 @@ function resolveExportFormat(value) {
|
|
|
1514
1581
|
default: throw new Error("invalid meeting export format: expected json or yaml");
|
|
1515
1582
|
}
|
|
1516
1583
|
}
|
|
1584
|
+
function resolveNotesFormat(value) {
|
|
1585
|
+
switch (value) {
|
|
1586
|
+
case void 0: return "markdown";
|
|
1587
|
+
case "json":
|
|
1588
|
+
case "markdown":
|
|
1589
|
+
case "raw":
|
|
1590
|
+
case "yaml": return value;
|
|
1591
|
+
default: throw new Error("invalid meeting notes format: expected markdown, json, yaml, or raw");
|
|
1592
|
+
}
|
|
1593
|
+
}
|
|
1594
|
+
function resolveTranscriptFormat$1(value) {
|
|
1595
|
+
switch (value) {
|
|
1596
|
+
case void 0: return "text";
|
|
1597
|
+
case "json":
|
|
1598
|
+
case "raw":
|
|
1599
|
+
case "text":
|
|
1600
|
+
case "yaml": return value;
|
|
1601
|
+
default: throw new Error("invalid meeting transcript format: expected text, json, yaml, or raw");
|
|
1602
|
+
}
|
|
1603
|
+
}
|
|
1517
1604
|
function parseLimit(value) {
|
|
1518
1605
|
if (value === void 0) return 20;
|
|
1519
1606
|
if (typeof value !== "string" || !/^\d+$/.test(value)) throw new Error("invalid meeting limit: expected a positive integer");
|
|
@@ -1543,18 +1630,25 @@ const meetingCommand = {
|
|
|
1543
1630
|
case "export":
|
|
1544
1631
|
if (!id) throw new Error("meeting export requires an id");
|
|
1545
1632
|
return await exportMeeting(id, commandFlags, globalFlags);
|
|
1633
|
+
case "notes":
|
|
1634
|
+
if (!id) throw new Error("meeting notes requires an id");
|
|
1635
|
+
return await notes(id, commandFlags, globalFlags);
|
|
1636
|
+
case "transcript":
|
|
1637
|
+
if (!id) throw new Error("meeting transcript requires an id");
|
|
1638
|
+
return await transcript(id, commandFlags, globalFlags);
|
|
1546
1639
|
case void 0:
|
|
1547
1640
|
console.log(meetingHelp());
|
|
1548
1641
|
return 1;
|
|
1549
|
-
default: throw new Error("invalid meeting command: expected list, view, or
|
|
1642
|
+
default: throw new Error("invalid meeting command: expected list, view, export, notes, or transcript");
|
|
1550
1643
|
}
|
|
1551
1644
|
}
|
|
1552
1645
|
};
|
|
1553
|
-
async function loadMeetingData(commandFlags, globalFlags) {
|
|
1646
|
+
async function loadMeetingData(commandFlags, globalFlags, options = {}) {
|
|
1554
1647
|
const config = await loadConfig({
|
|
1555
1648
|
globalFlags,
|
|
1556
1649
|
subcommandFlags: commandFlags
|
|
1557
1650
|
});
|
|
1651
|
+
if (options.requireCache && !config.transcripts.cacheFile) throw new Error(`Granola cache file not found. Pass --cache or create .granola.toml. Expected locations include: ${granolaCacheCandidates().join(", ")}`);
|
|
1558
1652
|
if (config.transcripts.cacheFile && !existsSync(config.transcripts.cacheFile)) throw new Error(`Granola cache file not found: ${config.transcripts.cacheFile}`);
|
|
1559
1653
|
debug(config.debug, "using config", config.configFileUsed ?? "(none)");
|
|
1560
1654
|
debug(config.debug, "supabase", config.supabase);
|
|
@@ -1567,6 +1661,15 @@ async function loadMeetingData(commandFlags, globalFlags) {
|
|
|
1567
1661
|
granolaClient
|
|
1568
1662
|
};
|
|
1569
1663
|
}
|
|
1664
|
+
async function loadResolvedMeeting(id, commandFlags, globalFlags, options = {}) {
|
|
1665
|
+
const { cacheData, config, granolaClient } = await loadMeetingData(commandFlags, globalFlags, options);
|
|
1666
|
+
console.log("Fetching meeting from Granola API...");
|
|
1667
|
+
return {
|
|
1668
|
+
cacheData,
|
|
1669
|
+
config,
|
|
1670
|
+
document: resolveMeeting(await granolaClient.listDocuments({ timeoutMs: config.notes.timeoutMs }), id)
|
|
1671
|
+
};
|
|
1672
|
+
}
|
|
1570
1673
|
async function list(commandFlags, globalFlags) {
|
|
1571
1674
|
const format = resolveListFormat(commandFlags.format);
|
|
1572
1675
|
const limit = parseLimit(commandFlags.limit);
|
|
@@ -1583,20 +1686,32 @@ async function list(commandFlags, globalFlags) {
|
|
|
1583
1686
|
}
|
|
1584
1687
|
async function view(id, commandFlags, globalFlags) {
|
|
1585
1688
|
const format = resolveViewFormat(commandFlags.format);
|
|
1586
|
-
const { cacheData,
|
|
1587
|
-
|
|
1588
|
-
const meeting = buildMeetingRecord(resolveMeeting(await granolaClient.listDocuments({ timeoutMs: config.notes.timeoutMs }), id), cacheData);
|
|
1689
|
+
const { cacheData, document } = await loadResolvedMeeting(id, commandFlags, globalFlags);
|
|
1690
|
+
const meeting = buildMeetingRecord(document, cacheData);
|
|
1589
1691
|
console.log(renderMeetingView(meeting, format).trimEnd());
|
|
1590
1692
|
return 0;
|
|
1591
1693
|
}
|
|
1592
1694
|
async function exportMeeting(id, commandFlags, globalFlags) {
|
|
1593
1695
|
const format = resolveExportFormat(commandFlags.format);
|
|
1594
|
-
const { cacheData,
|
|
1595
|
-
|
|
1596
|
-
const meeting = buildMeetingRecord(resolveMeeting(await granolaClient.listDocuments({ timeoutMs: config.notes.timeoutMs }), id), cacheData);
|
|
1696
|
+
const { cacheData, document } = await loadResolvedMeeting(id, commandFlags, globalFlags);
|
|
1697
|
+
const meeting = buildMeetingRecord(document, cacheData);
|
|
1597
1698
|
console.log(renderMeetingExport(meeting, format).trimEnd());
|
|
1598
1699
|
return 0;
|
|
1599
1700
|
}
|
|
1701
|
+
async function notes(id, commandFlags, globalFlags) {
|
|
1702
|
+
const format = resolveNotesFormat(commandFlags.format);
|
|
1703
|
+
const { document } = await loadResolvedMeeting(id, commandFlags, globalFlags);
|
|
1704
|
+
console.log(renderMeetingNotes(document, format).trimEnd());
|
|
1705
|
+
return 0;
|
|
1706
|
+
}
|
|
1707
|
+
async function transcript(id, commandFlags, globalFlags) {
|
|
1708
|
+
const format = resolveTranscriptFormat$1(commandFlags.format);
|
|
1709
|
+
const { cacheData, document } = await loadResolvedMeeting(id, commandFlags, globalFlags, { requireCache: true });
|
|
1710
|
+
const output = renderMeetingTranscript(document, cacheData, format);
|
|
1711
|
+
if (!output.trim()) throw new Error(`no transcript found for meeting: ${document.id}`);
|
|
1712
|
+
console.log(output.trimEnd());
|
|
1713
|
+
return 0;
|
|
1714
|
+
}
|
|
1600
1715
|
//#endregion
|
|
1601
1716
|
//#region src/commands/notes.ts
|
|
1602
1717
|
function notesHelp() {
|