granola-toolkit 0.30.0 → 0.31.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 +27 -0
- package/dist/cli.js +504 -38
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -35,6 +35,7 @@ granola --help
|
|
|
35
35
|
granola attach --help
|
|
36
36
|
granola auth login
|
|
37
37
|
granola exports --help
|
|
38
|
+
granola folder --help
|
|
38
39
|
granola meeting --help
|
|
39
40
|
granola notes --help
|
|
40
41
|
granola serve --help
|
|
@@ -52,6 +53,7 @@ vp pack
|
|
|
52
53
|
node dist/cli.js --help
|
|
53
54
|
node dist/cli.js attach --help
|
|
54
55
|
node dist/cli.js exports --help
|
|
56
|
+
node dist/cli.js folder --help
|
|
55
57
|
node dist/cli.js meeting --help
|
|
56
58
|
node dist/cli.js notes --help
|
|
57
59
|
node dist/cli.js serve --help
|
|
@@ -94,8 +96,11 @@ node dist/cli.js transcripts --format yaml --output ./transcripts-yaml
|
|
|
94
96
|
Inspect individual meetings:
|
|
95
97
|
|
|
96
98
|
```bash
|
|
99
|
+
granola folder list
|
|
100
|
+
granola folder view Team
|
|
97
101
|
granola meeting list --limit 10
|
|
98
102
|
granola meeting list --search planning
|
|
103
|
+
granola meeting list --folder Team
|
|
99
104
|
granola meeting view 1234abcd
|
|
100
105
|
granola meeting notes 1234abcd
|
|
101
106
|
granola meeting transcript 1234abcd --format json
|
|
@@ -201,6 +206,24 @@ The machine-readable `export` command includes:
|
|
|
201
206
|
- structured note data plus rendered Markdown
|
|
202
207
|
- structured transcript data plus rendered transcript text when available
|
|
203
208
|
|
|
209
|
+
### Folders
|
|
210
|
+
|
|
211
|
+
`folder` exposes Granola document lists as a first-class concept instead of leaving meetings in one flat global list.
|
|
212
|
+
|
|
213
|
+
The flow is:
|
|
214
|
+
|
|
215
|
+
1. reuse the shared auth path that `notes` and `meeting` already use
|
|
216
|
+
2. call Granola's document-list API, with `v2` first and `v1` fallback
|
|
217
|
+
3. normalise folder metadata and document membership into shared folder records
|
|
218
|
+
4. attach folder membership to meetings in the shared app core
|
|
219
|
+
5. let folder commands and meeting filters resolve folders by id, prefix, or unique name
|
|
220
|
+
|
|
221
|
+
The current CLI surface includes:
|
|
222
|
+
|
|
223
|
+
- `folder list`
|
|
224
|
+
- `folder view <id|name>`
|
|
225
|
+
- `meeting list --folder <id|name>`
|
|
226
|
+
|
|
204
227
|
### Server
|
|
205
228
|
|
|
206
229
|
`serve` starts a long-lived local `Granola Toolkit` server on one shared app instance.
|
|
@@ -214,7 +237,11 @@ The initial server API includes:
|
|
|
214
237
|
- `GET /auth/status`
|
|
215
238
|
- `GET /state`
|
|
216
239
|
- `GET /events` for server-sent state updates
|
|
240
|
+
- `GET /folders`
|
|
241
|
+
- `GET /folders/resolve?q=<query>`
|
|
242
|
+
- `GET /folders/:id`
|
|
217
243
|
- `GET /meetings`
|
|
244
|
+
- `GET /meetings?folderId=<id>` for folder-scoped meeting lists
|
|
218
245
|
- `GET /meetings?refresh=true` to bypass the local meeting index and force a live refresh
|
|
219
246
|
- `GET /meetings/resolve?q=<query>`
|
|
220
247
|
- `GET /meetings/:id`
|
package/dist/cli.js
CHANGED
|
@@ -22,6 +22,8 @@ const granolaTransportPaths = {
|
|
|
22
22
|
exportJobs: "/exports/jobs",
|
|
23
23
|
exportNotes: "/exports/notes",
|
|
24
24
|
exportTranscripts: "/exports/transcripts",
|
|
25
|
+
folderResolve: "/folders/resolve",
|
|
26
|
+
folders: "/folders",
|
|
25
27
|
health: "/health",
|
|
26
28
|
meetingResolve: "/meetings/resolve",
|
|
27
29
|
meetings: "/meetings",
|
|
@@ -48,6 +50,7 @@ function granolaMeetingResolvePath(query, options = {}) {
|
|
|
48
50
|
}
|
|
49
51
|
function granolaMeetingsPath(options = {}) {
|
|
50
52
|
return appendSearchParams(granolaTransportPaths.meetings, {
|
|
53
|
+
folderId: options.folderId,
|
|
51
54
|
limit: options.limit,
|
|
52
55
|
refresh: options.forceRefresh ? "true" : void 0,
|
|
53
56
|
search: options.search,
|
|
@@ -56,6 +59,19 @@ function granolaMeetingsPath(options = {}) {
|
|
|
56
59
|
updatedTo: options.updatedTo
|
|
57
60
|
});
|
|
58
61
|
}
|
|
62
|
+
function granolaFolderPath(id) {
|
|
63
|
+
return `${granolaTransportPaths.folders}/${encodeURIComponent(id)}`;
|
|
64
|
+
}
|
|
65
|
+
function granolaFolderResolvePath(query) {
|
|
66
|
+
return appendSearchParams(granolaTransportPaths.folderResolve, { q: query });
|
|
67
|
+
}
|
|
68
|
+
function granolaFoldersPath(options = {}) {
|
|
69
|
+
return appendSearchParams(granolaTransportPaths.folders, {
|
|
70
|
+
limit: options.limit,
|
|
71
|
+
refresh: options.forceRefresh ? "true" : void 0,
|
|
72
|
+
search: options.search
|
|
73
|
+
});
|
|
74
|
+
}
|
|
59
75
|
function granolaExportJobsPath(options = {}) {
|
|
60
76
|
return appendSearchParams(granolaTransportPaths.exportJobs, { limit: options.limit });
|
|
61
77
|
}
|
|
@@ -131,7 +147,7 @@ var GranolaServerClient = class GranolaServerClient {
|
|
|
131
147
|
}) });
|
|
132
148
|
if (!infoResponse.ok) throw await responseError(infoResponse);
|
|
133
149
|
const info = await infoResponse.json();
|
|
134
|
-
if (info.protocolVersion !==
|
|
150
|
+
if (info.protocolVersion !== 2) throw new Error(`unsupported Granola transport protocol: expected 2, got ${info.protocolVersion}`);
|
|
135
151
|
const response = await fetchImpl(new URL(granolaTransportPaths.state, url), { headers: mergeHeaders({
|
|
136
152
|
...options.password?.trim() ? { "x-granola-password": options.password.trim() } : {},
|
|
137
153
|
accept: "application/json"
|
|
@@ -181,6 +197,15 @@ var GranolaServerClient = class GranolaServerClient {
|
|
|
181
197
|
method: "POST"
|
|
182
198
|
});
|
|
183
199
|
}
|
|
200
|
+
async listFolders(options = {}) {
|
|
201
|
+
return await this.requestJson(granolaFoldersPath(options));
|
|
202
|
+
}
|
|
203
|
+
async getFolder(id) {
|
|
204
|
+
return await this.requestJson(granolaFolderPath(id));
|
|
205
|
+
}
|
|
206
|
+
async findFolder(query) {
|
|
207
|
+
return await this.requestJson(granolaFolderResolvePath(query));
|
|
208
|
+
}
|
|
184
209
|
async listMeetings(options = {}) {
|
|
185
210
|
return await this.requestJson(granolaMeetingsPath(options));
|
|
186
211
|
}
|
|
@@ -1075,6 +1100,10 @@ function matchesMeetingSearch(document, search) {
|
|
|
1075
1100
|
...document.tags
|
|
1076
1101
|
].some((value) => value.toLowerCase().includes(query));
|
|
1077
1102
|
}
|
|
1103
|
+
function matchesMeetingFolders(documentId, folderId, foldersByDocumentId) {
|
|
1104
|
+
if (!folderId) return true;
|
|
1105
|
+
return (foldersByDocumentId?.get(documentId) ?? []).some((folder) => folder.id === folderId);
|
|
1106
|
+
}
|
|
1078
1107
|
function matchesMeetingSummarySearch(meeting, search) {
|
|
1079
1108
|
const query = search.trim().toLowerCase();
|
|
1080
1109
|
if (!query) return true;
|
|
@@ -1084,6 +1113,9 @@ function matchesMeetingSummarySearch(meeting, search) {
|
|
|
1084
1113
|
...meeting.tags
|
|
1085
1114
|
].some((value) => value.toLowerCase().includes(query));
|
|
1086
1115
|
}
|
|
1116
|
+
function meetingFolders(meeting) {
|
|
1117
|
+
return Array.isArray(meeting.folders) ? meeting.folders : [];
|
|
1118
|
+
}
|
|
1087
1119
|
function parseDateFilter(value, label) {
|
|
1088
1120
|
const trimmed = value?.trim();
|
|
1089
1121
|
if (!trimmed) return;
|
|
@@ -1110,7 +1142,7 @@ function matchesMeetingSummaryUpdatedRange(meeting, updatedFrom, updatedTo) {
|
|
|
1110
1142
|
if (to != null && updatedAt > to) return false;
|
|
1111
1143
|
return true;
|
|
1112
1144
|
}
|
|
1113
|
-
function truncate(value, width) {
|
|
1145
|
+
function truncate$1(value, width) {
|
|
1114
1146
|
if (value.length <= width) return value.padEnd(width);
|
|
1115
1147
|
return `${value.slice(0, Math.max(0, width - 1))}…`;
|
|
1116
1148
|
}
|
|
@@ -1126,11 +1158,12 @@ function formatTranscriptLines(transcript) {
|
|
|
1126
1158
|
if (!transcript || transcript.segments.length === 0) return "";
|
|
1127
1159
|
return transcript.segments.map((segment) => `[${formatTimestampForTranscript(segment.startTimestamp)}] ${segment.speaker}: ${segment.text}`).join("\n");
|
|
1128
1160
|
}
|
|
1129
|
-
function buildMeetingSummary(document, cacheData) {
|
|
1161
|
+
function buildMeetingSummary(document, cacheData, folders = []) {
|
|
1130
1162
|
const note = buildNoteExport(document);
|
|
1131
1163
|
const transcript = buildMeetingTranscript(document, cacheData);
|
|
1132
1164
|
return {
|
|
1133
1165
|
createdAt: document.createdAt,
|
|
1166
|
+
folders: folders.map((folder) => ({ ...folder })),
|
|
1134
1167
|
id: document.id,
|
|
1135
1168
|
noteContentSource: note.contentSource,
|
|
1136
1169
|
tags: [...document.tags],
|
|
@@ -1140,12 +1173,13 @@ function buildMeetingSummary(document, cacheData) {
|
|
|
1140
1173
|
updatedAt: latestDocumentTimestamp(document)
|
|
1141
1174
|
};
|
|
1142
1175
|
}
|
|
1143
|
-
function buildMeetingRecord(document, cacheData) {
|
|
1176
|
+
function buildMeetingRecord(document, cacheData, folders = []) {
|
|
1144
1177
|
const note = buildNoteExport(document);
|
|
1145
1178
|
const transcript = buildMeetingTranscript(document, cacheData);
|
|
1146
1179
|
return {
|
|
1147
1180
|
meeting: {
|
|
1148
1181
|
createdAt: document.createdAt,
|
|
1182
|
+
folders: folders.map((folder) => ({ ...folder })),
|
|
1149
1183
|
id: document.id,
|
|
1150
1184
|
noteContentSource: note.contentSource,
|
|
1151
1185
|
tags: [...document.tags],
|
|
@@ -1163,13 +1197,14 @@ function buildMeetingRecord(document, cacheData) {
|
|
|
1163
1197
|
function listMeetings(documents, options = {}) {
|
|
1164
1198
|
const limit = options.limit ?? 20;
|
|
1165
1199
|
const sort = options.sort ?? "updated-desc";
|
|
1166
|
-
return documents.filter((document) => options.search ? matchesMeetingSearch(document, options.search) : true).filter((document) => matchesUpdatedRange(document, options.updatedFrom, options.updatedTo)).sort((left, right) => compareMeetingDocumentsBySort(left, right, sort)).slice(0, limit).map((document) => buildMeetingSummary(document, options.cacheData));
|
|
1200
|
+
return documents.filter((document) => options.search ? matchesMeetingSearch(document, options.search) : true).filter((document) => matchesMeetingFolders(document.id, options.folderId, options.foldersByDocumentId)).filter((document) => matchesUpdatedRange(document, options.updatedFrom, options.updatedTo)).sort((left, right) => compareMeetingDocumentsBySort(left, right, sort)).slice(0, limit).map((document) => buildMeetingSummary(document, options.cacheData, options.foldersByDocumentId?.get(document.id)));
|
|
1167
1201
|
}
|
|
1168
1202
|
function filterMeetingSummaries(meetings, options = {}) {
|
|
1169
1203
|
const limit = options.limit ?? 20;
|
|
1170
1204
|
const sort = options.sort ?? "updated-desc";
|
|
1171
|
-
return meetings.filter((meeting) => options.search ? matchesMeetingSummarySearch(meeting, options.search) : true).filter((meeting) => matchesMeetingSummaryUpdatedRange(meeting, options.updatedFrom, options.updatedTo)).sort((left, right) => compareMeetingSummariesBySort(left, right, sort)).slice(0, limit).map((meeting) => ({
|
|
1205
|
+
return meetings.filter((meeting) => options.folderId ? meetingFolders(meeting).some((folder) => folder.id === options.folderId) : true).filter((meeting) => options.search ? matchesMeetingSummarySearch(meeting, options.search) : true).filter((meeting) => matchesMeetingSummaryUpdatedRange(meeting, options.updatedFrom, options.updatedTo)).sort((left, right) => compareMeetingSummariesBySort(left, right, sort)).slice(0, limit).map((meeting) => ({
|
|
1172
1206
|
...meeting,
|
|
1207
|
+
folders: meetingFolders(meeting).map((folder) => ({ ...folder })),
|
|
1173
1208
|
tags: [...meeting.tags]
|
|
1174
1209
|
}));
|
|
1175
1210
|
}
|
|
@@ -1206,14 +1241,18 @@ function renderMeetingList(meetings, format = "text") {
|
|
|
1206
1241
|
case "text": break;
|
|
1207
1242
|
}
|
|
1208
1243
|
if (meetings.length === 0) return "No meetings found\n";
|
|
1209
|
-
const lines = [`${"ID".padEnd(10)} ${"DATE".padEnd(10)} ${"TITLE".padEnd(
|
|
1210
|
-
for (const meeting of meetings)
|
|
1211
|
-
meeting.
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1244
|
+
const lines = [`${"ID".padEnd(10)} ${"DATE".padEnd(10)} ${"TITLE".padEnd(34)} ${"FOLDERS".padEnd(18)} ${"NOTE".padEnd(18)} TRANSCRIPT`, `${"-".repeat(10)} ${"-".repeat(10)} ${"-".repeat(34)} ${"-".repeat(18)} ${"-".repeat(18)} ${"-".repeat(10)}`];
|
|
1245
|
+
for (const meeting of meetings) {
|
|
1246
|
+
const folderLabel = meetingFolders(meeting).length === 0 ? "-" : meetingFolders(meeting).length === 1 ? meetingFolders(meeting)[0].name : `${meetingFolders(meeting)[0].name} +${meetingFolders(meeting).length - 1}`;
|
|
1247
|
+
lines.push([
|
|
1248
|
+
meeting.id.slice(0, 8).padEnd(10),
|
|
1249
|
+
formatMeetingDate(meeting.updatedAt || meeting.createdAt).padEnd(10),
|
|
1250
|
+
truncate$1(meeting.title || meeting.id, 34),
|
|
1251
|
+
truncate$1(folderLabel, 18),
|
|
1252
|
+
truncate$1(meeting.noteContentSource, 18),
|
|
1253
|
+
formatTranscriptStatus(meeting)
|
|
1254
|
+
].join(" "));
|
|
1255
|
+
}
|
|
1217
1256
|
return `${lines.join("\n").trimEnd()}\n`;
|
|
1218
1257
|
}
|
|
1219
1258
|
function renderMeetingView(record, format = "text") {
|
|
@@ -1223,6 +1262,7 @@ function renderMeetingView(record, format = "text") {
|
|
|
1223
1262
|
case "text": break;
|
|
1224
1263
|
}
|
|
1225
1264
|
const tags = record.meeting.tags.length > 0 ? record.meeting.tags.join(", ") : "(none)";
|
|
1265
|
+
const folders = meetingFolders(record.meeting).length > 0 ? meetingFolders(record.meeting).map((folder) => folder.name).join(", ") : "(none)";
|
|
1226
1266
|
const transcriptStatus = !record.meeting.transcriptLoaded ? "cache not loaded" : record.meeting.transcriptSegmentCount === 0 ? "no transcript segments" : `${record.meeting.transcriptSegmentCount} segment(s)`;
|
|
1227
1267
|
return `${[
|
|
1228
1268
|
`# ${record.meeting.title || record.meeting.id}`,
|
|
@@ -1231,6 +1271,7 @@ function renderMeetingView(record, format = "text") {
|
|
|
1231
1271
|
`Created: ${record.meeting.createdAt || "-"}`,
|
|
1232
1272
|
`Updated: ${record.meeting.updatedAt || "-"}`,
|
|
1233
1273
|
`Tags: ${tags}`,
|
|
1274
|
+
`Folders: ${folders}`,
|
|
1234
1275
|
`Note source: ${record.meeting.noteContentSource}`,
|
|
1235
1276
|
`Transcript: ${transcriptStatus}`,
|
|
1236
1277
|
"",
|
|
@@ -2591,10 +2632,39 @@ function parseDocument(value) {
|
|
|
2591
2632
|
updatedAt: stringValue(record.updated_at)
|
|
2592
2633
|
};
|
|
2593
2634
|
}
|
|
2635
|
+
function parseFolderDocumentIds(value) {
|
|
2636
|
+
if (!Array.isArray(value)) return [];
|
|
2637
|
+
return value.map((item) => {
|
|
2638
|
+
if (typeof item === "string") return item;
|
|
2639
|
+
const record = asRecord(item);
|
|
2640
|
+
if (!record) return "";
|
|
2641
|
+
return stringValue(record.id) || stringValue(record.document_id);
|
|
2642
|
+
}).filter(Boolean);
|
|
2643
|
+
}
|
|
2644
|
+
function parseFolder(value) {
|
|
2645
|
+
const record = asRecord(value);
|
|
2646
|
+
if (!record) throw new Error("folder payload is not an object");
|
|
2647
|
+
const createdAt = stringValue(record.created_at);
|
|
2648
|
+
const updatedAt = stringValue(record.updated_at) || createdAt;
|
|
2649
|
+
const name = stringValue(record.name) || stringValue(record.title);
|
|
2650
|
+
const documents = parseFolderDocumentIds(record.documents);
|
|
2651
|
+
const documentIds = documents.length > 0 ? documents : parseFolderDocumentIds(record.document_ids);
|
|
2652
|
+
return {
|
|
2653
|
+
createdAt,
|
|
2654
|
+
description: stringValue(record.description) || void 0,
|
|
2655
|
+
documentIds,
|
|
2656
|
+
id: stringValue(record.id),
|
|
2657
|
+
isFavourite: Boolean(record.is_favourite),
|
|
2658
|
+
name,
|
|
2659
|
+
updatedAt,
|
|
2660
|
+
workspaceId: stringValue(record.workspace_id) || void 0
|
|
2661
|
+
};
|
|
2662
|
+
}
|
|
2594
2663
|
//#endregion
|
|
2595
2664
|
//#region src/client/granola.ts
|
|
2596
2665
|
const DEFAULT_CLIENT_VERSION = "5.354.0";
|
|
2597
2666
|
const DOCUMENTS_URL = "https://api.granola.ai/v2/get-documents";
|
|
2667
|
+
const FOLDERS_URLS = ["https://api.granola.ai/v2/get-document-lists", "https://api.granola.ai/v1/get-document-lists"];
|
|
2598
2668
|
function resolveClientVersion(value) {
|
|
2599
2669
|
return value?.trim() || process.env.GRANOLA_CLIENT_VERSION?.trim() || DEFAULT_CLIENT_VERSION;
|
|
2600
2670
|
}
|
|
@@ -2640,6 +2710,29 @@ var GranolaApiClient = class {
|
|
|
2640
2710
|
}
|
|
2641
2711
|
return documents;
|
|
2642
2712
|
}
|
|
2713
|
+
async listFolders(options) {
|
|
2714
|
+
let lastError;
|
|
2715
|
+
for (const foldersUrl of FOLDERS_URLS) {
|
|
2716
|
+
const response = await this.httpClient.postJson(foldersUrl, {}, {
|
|
2717
|
+
headers: {
|
|
2718
|
+
"User-Agent": `Granola/${this.clientVersion}`,
|
|
2719
|
+
"X-Client-Version": this.clientVersion
|
|
2720
|
+
},
|
|
2721
|
+
timeoutMs: options.timeoutMs
|
|
2722
|
+
});
|
|
2723
|
+
if (response.status === 404) {
|
|
2724
|
+
lastError = /* @__PURE__ */ new Error(`failed to get folders: ${response.status} ${response.statusText}`);
|
|
2725
|
+
continue;
|
|
2726
|
+
}
|
|
2727
|
+
if (!response.ok) {
|
|
2728
|
+
const body = (await response.text()).slice(0, 500);
|
|
2729
|
+
throw new Error(`failed to get folders: ${response.status} ${response.statusText}${body ? `: ${body}` : ""}`);
|
|
2730
|
+
}
|
|
2731
|
+
const payload = await response.json();
|
|
2732
|
+
return (Array.isArray(payload) ? payload : Array.isArray(payload.lists) ? payload.lists : Array.isArray(payload.document_lists) ? payload.document_lists : []).map(parseFolder);
|
|
2733
|
+
}
|
|
2734
|
+
throw lastError ?? /* @__PURE__ */ new Error("failed to get folders");
|
|
2735
|
+
}
|
|
2643
2736
|
};
|
|
2644
2737
|
//#endregion
|
|
2645
2738
|
//#region src/client/http.ts
|
|
@@ -2826,8 +2919,121 @@ function createDefaultExportJobStore() {
|
|
|
2826
2919
|
return new FileExportJobStore();
|
|
2827
2920
|
}
|
|
2828
2921
|
//#endregion
|
|
2922
|
+
//#region src/folders.ts
|
|
2923
|
+
function truncate(value, width) {
|
|
2924
|
+
if (value.length <= width) return value.padEnd(width);
|
|
2925
|
+
return `${value.slice(0, Math.max(0, width - 1))}…`;
|
|
2926
|
+
}
|
|
2927
|
+
function compareFolders(left, right) {
|
|
2928
|
+
return right.updatedAt.localeCompare(left.updatedAt) || compareStrings(left.name || left.id, right.name || right.id) || compareStrings(left.id, right.id);
|
|
2929
|
+
}
|
|
2930
|
+
function matchesFolderSearch(folder, search) {
|
|
2931
|
+
const query = search.trim().toLowerCase();
|
|
2932
|
+
if (!query) return true;
|
|
2933
|
+
return [
|
|
2934
|
+
folder.id,
|
|
2935
|
+
folder.name,
|
|
2936
|
+
folder.description ?? "",
|
|
2937
|
+
folder.workspaceId ?? ""
|
|
2938
|
+
].some((value) => value.toLowerCase().includes(query));
|
|
2939
|
+
}
|
|
2940
|
+
function formatFolderDate(value) {
|
|
2941
|
+
return value.trim().slice(0, 10) || "-";
|
|
2942
|
+
}
|
|
2943
|
+
function buildFolderSummary(folder) {
|
|
2944
|
+
return {
|
|
2945
|
+
createdAt: folder.createdAt,
|
|
2946
|
+
description: folder.description,
|
|
2947
|
+
documentCount: folder.documentIds.length,
|
|
2948
|
+
id: folder.id,
|
|
2949
|
+
isFavourite: folder.isFavourite,
|
|
2950
|
+
name: folder.name,
|
|
2951
|
+
updatedAt: folder.updatedAt,
|
|
2952
|
+
workspaceId: folder.workspaceId
|
|
2953
|
+
};
|
|
2954
|
+
}
|
|
2955
|
+
function buildFolderRecord(folder, meetings) {
|
|
2956
|
+
return {
|
|
2957
|
+
...buildFolderSummary(folder),
|
|
2958
|
+
documentIds: [...folder.documentIds],
|
|
2959
|
+
meetings: meetings.map((meeting) => ({
|
|
2960
|
+
...meeting,
|
|
2961
|
+
folders: meeting.folders.map((candidate) => ({ ...candidate })),
|
|
2962
|
+
tags: [...meeting.tags]
|
|
2963
|
+
}))
|
|
2964
|
+
};
|
|
2965
|
+
}
|
|
2966
|
+
function filterFolders(folders, options = {}) {
|
|
2967
|
+
const limit = options.limit ?? 20;
|
|
2968
|
+
return folders.filter((folder) => options.search ? matchesFolderSearch(folder, options.search) : true).sort(compareFolders).slice(0, limit).map((folder) => ({ ...folder }));
|
|
2969
|
+
}
|
|
2970
|
+
function resolveFolder(folders, id) {
|
|
2971
|
+
const exactMatch = folders.find((folder) => folder.id === id);
|
|
2972
|
+
if (exactMatch) return exactMatch;
|
|
2973
|
+
const matches = folders.filter((folder) => folder.id.startsWith(id));
|
|
2974
|
+
if (matches.length === 1) return matches[0];
|
|
2975
|
+
if (matches.length > 1) throw new Error(`ambiguous folder id: ${id}`);
|
|
2976
|
+
throw new Error(`folder not found: ${id}`);
|
|
2977
|
+
}
|
|
2978
|
+
function resolveFolderQuery(folders, query) {
|
|
2979
|
+
const trimmed = query.trim();
|
|
2980
|
+
if (!trimmed) throw new Error("folder query is required");
|
|
2981
|
+
const lower = trimmed.toLowerCase();
|
|
2982
|
+
const exactId = folders.find((folder) => folder.id === trimmed);
|
|
2983
|
+
if (exactId) return exactId;
|
|
2984
|
+
const exactNameMatches = folders.filter((folder) => folder.name.toLowerCase() === lower);
|
|
2985
|
+
if (exactNameMatches.length === 1) return exactNameMatches[0];
|
|
2986
|
+
const prefixMatches = folders.filter((folder) => folder.id.startsWith(trimmed));
|
|
2987
|
+
if (prefixMatches.length === 1) return prefixMatches[0];
|
|
2988
|
+
const nameMatches = folders.filter((folder) => folder.name.toLowerCase().includes(lower)).sort(compareFolders);
|
|
2989
|
+
if (nameMatches.length === 1) return nameMatches[0];
|
|
2990
|
+
if (exactNameMatches.length > 1 || prefixMatches.length > 1 || nameMatches.length > 1) throw new Error(`ambiguous folder query: ${trimmed}`);
|
|
2991
|
+
throw new Error(`folder not found: ${trimmed}`);
|
|
2992
|
+
}
|
|
2993
|
+
function renderFolderList(folders, format = "text") {
|
|
2994
|
+
switch (format) {
|
|
2995
|
+
case "json": return toJson(folders);
|
|
2996
|
+
case "yaml": return toYaml(folders);
|
|
2997
|
+
case "text": break;
|
|
2998
|
+
}
|
|
2999
|
+
if (folders.length === 0) return "No folders found\n";
|
|
3000
|
+
const lines = [`${"ID".padEnd(10)} ${"COUNT".padEnd(7)} ${"UPDATED".padEnd(10)} NAME`, `${"-".repeat(10)} ${"-".repeat(7)} ${"-".repeat(10)} ${"-".repeat(42)}`];
|
|
3001
|
+
for (const folder of folders) {
|
|
3002
|
+
const name = `${folder.isFavourite ? "★ " : ""}${folder.name || folder.id}`;
|
|
3003
|
+
lines.push([
|
|
3004
|
+
folder.id.slice(0, 8).padEnd(10),
|
|
3005
|
+
String(folder.documentCount).padEnd(7),
|
|
3006
|
+
formatFolderDate(folder.updatedAt || folder.createdAt).padEnd(10),
|
|
3007
|
+
truncate(name, 42)
|
|
3008
|
+
].join(" "));
|
|
3009
|
+
}
|
|
3010
|
+
return `${lines.join("\n").trimEnd()}\n`;
|
|
3011
|
+
}
|
|
3012
|
+
function renderFolderView(folder, format = "text") {
|
|
3013
|
+
switch (format) {
|
|
3014
|
+
case "json": return toJson(folder);
|
|
3015
|
+
case "yaml": return toYaml(folder);
|
|
3016
|
+
case "text": break;
|
|
3017
|
+
}
|
|
3018
|
+
const lines = [
|
|
3019
|
+
`# ${folder.name || folder.id}`,
|
|
3020
|
+
"",
|
|
3021
|
+
`ID: ${folder.id}`,
|
|
3022
|
+
`Created: ${folder.createdAt || "-"}`,
|
|
3023
|
+
`Updated: ${folder.updatedAt || "-"}`,
|
|
3024
|
+
`Documents: ${folder.documentCount}`,
|
|
3025
|
+
`Favourite: ${folder.isFavourite ? "yes" : "no"}`,
|
|
3026
|
+
`Workspace: ${folder.workspaceId || "-"}`
|
|
3027
|
+
];
|
|
3028
|
+
if (folder.description) lines.push(`Description: ${folder.description}`);
|
|
3029
|
+
lines.push("", "## Meetings", "");
|
|
3030
|
+
lines.push(renderMeetingList(folder.meetings, "text").trimEnd());
|
|
3031
|
+
lines.push("");
|
|
3032
|
+
return `${lines.join("\n").trimEnd()}\n`;
|
|
3033
|
+
}
|
|
3034
|
+
//#endregion
|
|
2829
3035
|
//#region src/meeting-index.ts
|
|
2830
|
-
const MEETING_INDEX_VERSION =
|
|
3036
|
+
const MEETING_INDEX_VERSION = 2;
|
|
2831
3037
|
var FileMeetingIndexStore = class {
|
|
2832
3038
|
constructor(filePath = defaultMeetingIndexFilePath()) {
|
|
2833
3039
|
this.filePath = filePath;
|
|
@@ -2838,6 +3044,7 @@ var FileMeetingIndexStore = class {
|
|
|
2838
3044
|
if (!parsed || parsed.version !== MEETING_INDEX_VERSION || !Array.isArray(parsed.meetings)) return [];
|
|
2839
3045
|
return parsed.meetings.map((meeting) => ({
|
|
2840
3046
|
...meeting,
|
|
3047
|
+
folders: Array.isArray(meeting.folders) ? meeting.folders.map((folder) => ({ ...folder })) : [],
|
|
2841
3048
|
tags: [...meeting.tags]
|
|
2842
3049
|
}));
|
|
2843
3050
|
} catch {
|
|
@@ -2849,6 +3056,7 @@ var FileMeetingIndexStore = class {
|
|
|
2849
3056
|
const payload = {
|
|
2850
3057
|
meetings: meetings.map((meeting) => ({
|
|
2851
3058
|
...meeting,
|
|
3059
|
+
folders: Array.isArray(meeting.folders) ? meeting.folders.map((folder) => ({ ...folder })) : [],
|
|
2852
3060
|
tags: [...meeting.tags]
|
|
2853
3061
|
})),
|
|
2854
3062
|
updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
@@ -2877,6 +3085,16 @@ function cloneExportState(state) {
|
|
|
2877
3085
|
function cloneExportJob(job) {
|
|
2878
3086
|
return { ...job };
|
|
2879
3087
|
}
|
|
3088
|
+
function cloneFolderSummary(folder) {
|
|
3089
|
+
return { ...folder };
|
|
3090
|
+
}
|
|
3091
|
+
function cloneMeetingSummary(meeting) {
|
|
3092
|
+
return {
|
|
3093
|
+
...meeting,
|
|
3094
|
+
folders: Array.isArray(meeting.folders) ? meeting.folders.map((folder) => cloneFolderSummary(folder)) : [],
|
|
3095
|
+
tags: [...meeting.tags]
|
|
3096
|
+
};
|
|
3097
|
+
}
|
|
2880
3098
|
function cloneState(state) {
|
|
2881
3099
|
return {
|
|
2882
3100
|
auth: { ...state.auth },
|
|
@@ -2887,6 +3105,7 @@ function cloneState(state) {
|
|
|
2887
3105
|
transcripts: { ...state.config.transcripts }
|
|
2888
3106
|
},
|
|
2889
3107
|
documents: { ...state.documents },
|
|
3108
|
+
folders: { ...state.folders },
|
|
2890
3109
|
exports: {
|
|
2891
3110
|
jobs: state.exports.jobs.map((job) => cloneExportJob(job)),
|
|
2892
3111
|
notes: cloneExportState(state.exports.notes),
|
|
@@ -2915,6 +3134,10 @@ function defaultState(config, auth, surface) {
|
|
|
2915
3134
|
count: 0,
|
|
2916
3135
|
loaded: false
|
|
2917
3136
|
},
|
|
3137
|
+
folders: {
|
|
3138
|
+
count: 0,
|
|
3139
|
+
loaded: false
|
|
3140
|
+
},
|
|
2918
3141
|
exports: { jobs: [] },
|
|
2919
3142
|
index: {
|
|
2920
3143
|
available: false,
|
|
@@ -2931,6 +3154,7 @@ function defaultState(config, auth, surface) {
|
|
|
2931
3154
|
var GranolaApp = class {
|
|
2932
3155
|
#cacheData;
|
|
2933
3156
|
#cacheResolved = false;
|
|
3157
|
+
#folders;
|
|
2934
3158
|
#granolaClient;
|
|
2935
3159
|
#documents;
|
|
2936
3160
|
#meetingIndex;
|
|
@@ -2942,10 +3166,7 @@ var GranolaApp = class {
|
|
|
2942
3166
|
this.deps = deps;
|
|
2943
3167
|
this.#state = defaultState(config, deps.auth, options.surface ?? "cli");
|
|
2944
3168
|
this.#state.exports.jobs = (deps.exportJobs ?? []).map((job) => cloneExportJob(job));
|
|
2945
|
-
this.#meetingIndex = (deps.meetingIndex ?? []).map((meeting) => (
|
|
2946
|
-
...meeting,
|
|
2947
|
-
tags: [...meeting.tags]
|
|
2948
|
-
}));
|
|
3169
|
+
this.#meetingIndex = (deps.meetingIndex ?? []).map((meeting) => cloneMeetingSummary(meeting));
|
|
2949
3170
|
this.#state.index = {
|
|
2950
3171
|
available: this.#meetingIndex.length > 0,
|
|
2951
3172
|
filePath: defaultMeetingIndexFilePath(),
|
|
@@ -2971,16 +3192,21 @@ var GranolaApp = class {
|
|
|
2971
3192
|
this.emitStateUpdate();
|
|
2972
3193
|
return this.getState();
|
|
2973
3194
|
}
|
|
2974
|
-
|
|
3195
|
+
resetRemoteState() {
|
|
2975
3196
|
this.#granolaClient = void 0;
|
|
3197
|
+
this.#folders = void 0;
|
|
2976
3198
|
this.#documents = void 0;
|
|
2977
3199
|
this.#state.documents = {
|
|
2978
3200
|
count: 0,
|
|
2979
3201
|
loaded: false
|
|
2980
3202
|
};
|
|
3203
|
+
this.#state.folders = {
|
|
3204
|
+
count: 0,
|
|
3205
|
+
loaded: false
|
|
3206
|
+
};
|
|
2981
3207
|
}
|
|
2982
3208
|
applyAuthState(auth, options = {}) {
|
|
2983
|
-
if (options.resetDocuments) this.
|
|
3209
|
+
if (options.resetDocuments) this.resetRemoteState();
|
|
2984
3210
|
this.#state.auth = { ...auth };
|
|
2985
3211
|
if (options.view) this.#state.ui = {
|
|
2986
3212
|
...this.#state.ui,
|
|
@@ -2990,10 +3216,7 @@ var GranolaApp = class {
|
|
|
2990
3216
|
return { ...auth };
|
|
2991
3217
|
}
|
|
2992
3218
|
async persistMeetingIndex(meetings) {
|
|
2993
|
-
this.#meetingIndex = meetings.map((meeting) => (
|
|
2994
|
-
...meeting,
|
|
2995
|
-
tags: [...meeting.tags]
|
|
2996
|
-
}));
|
|
3219
|
+
this.#meetingIndex = meetings.map((meeting) => cloneMeetingSummary(meeting));
|
|
2997
3220
|
this.#state.index = {
|
|
2998
3221
|
available: this.#meetingIndex.length > 0,
|
|
2999
3222
|
filePath: this.#state.index.filePath,
|
|
@@ -3007,8 +3230,10 @@ var GranolaApp = class {
|
|
|
3007
3230
|
async refreshMeetingIndexFromLiveData() {
|
|
3008
3231
|
const cacheData = await this.loadCache();
|
|
3009
3232
|
const documents = await this.listDocuments();
|
|
3233
|
+
const folders = await this.loadFolders();
|
|
3010
3234
|
const meetings = listMeetings(documents, {
|
|
3011
3235
|
cacheData,
|
|
3236
|
+
foldersByDocumentId: this.buildFoldersByDocumentId(folders),
|
|
3012
3237
|
limit: documents.length || 1,
|
|
3013
3238
|
sort: "updated-desc"
|
|
3014
3239
|
});
|
|
@@ -3047,6 +3272,54 @@ var GranolaApp = class {
|
|
|
3047
3272
|
this.applyAuthState(runtime.auth);
|
|
3048
3273
|
return this.#granolaClient;
|
|
3049
3274
|
}
|
|
3275
|
+
buildFoldersByDocumentId(folders) {
|
|
3276
|
+
if (!folders || folders.length === 0) return;
|
|
3277
|
+
const byDocumentId = /* @__PURE__ */ new Map();
|
|
3278
|
+
for (const folder of folders) {
|
|
3279
|
+
const summary = buildFolderSummary(folder);
|
|
3280
|
+
for (const documentId of folder.documentIds) {
|
|
3281
|
+
const existing = byDocumentId.get(documentId) ?? [];
|
|
3282
|
+
existing.push(summary);
|
|
3283
|
+
byDocumentId.set(documentId, existing);
|
|
3284
|
+
}
|
|
3285
|
+
}
|
|
3286
|
+
for (const [documentId, summaries] of byDocumentId.entries()) byDocumentId.set(documentId, summaries.slice().sort((left, right) => left.name.localeCompare(right.name)).map((folder) => cloneFolderSummary(folder)));
|
|
3287
|
+
return byDocumentId;
|
|
3288
|
+
}
|
|
3289
|
+
async loadFolders(options = {}) {
|
|
3290
|
+
if (options.forceRefresh) {
|
|
3291
|
+
this.resetRemoteState();
|
|
3292
|
+
this.emitStateUpdate();
|
|
3293
|
+
}
|
|
3294
|
+
if (this.#folders) return this.#folders.map((folder) => ({
|
|
3295
|
+
...folder,
|
|
3296
|
+
documentIds: [...folder.documentIds]
|
|
3297
|
+
}));
|
|
3298
|
+
const client = await this.getGranolaClient();
|
|
3299
|
+
if (!client.listFolders) {
|
|
3300
|
+
if (options.required) throw new Error("Granola folder API is not configured");
|
|
3301
|
+
return;
|
|
3302
|
+
}
|
|
3303
|
+
try {
|
|
3304
|
+
this.#folders = (await client.listFolders({ timeoutMs: this.config.notes.timeoutMs })).map((folder) => ({
|
|
3305
|
+
...folder,
|
|
3306
|
+
documentIds: [...folder.documentIds]
|
|
3307
|
+
}));
|
|
3308
|
+
this.#state.folders = {
|
|
3309
|
+
count: this.#folders.length,
|
|
3310
|
+
loaded: true,
|
|
3311
|
+
loadedAt: this.nowIso()
|
|
3312
|
+
};
|
|
3313
|
+
this.emitStateUpdate();
|
|
3314
|
+
return this.#folders.map((folder) => ({
|
|
3315
|
+
...folder,
|
|
3316
|
+
documentIds: [...folder.documentIds]
|
|
3317
|
+
}));
|
|
3318
|
+
} catch (error) {
|
|
3319
|
+
if (options.required) throw error;
|
|
3320
|
+
return;
|
|
3321
|
+
}
|
|
3322
|
+
}
|
|
3050
3323
|
missingCacheError() {
|
|
3051
3324
|
return /* @__PURE__ */ new Error(`Granola cache file not found. Pass --cache or create .granola.toml. Expected locations include: ${granolaCacheCandidates().join(", ")}`);
|
|
3052
3325
|
}
|
|
@@ -3159,12 +3432,7 @@ var GranolaApp = class {
|
|
|
3159
3432
|
}
|
|
3160
3433
|
async listDocuments(options = {}) {
|
|
3161
3434
|
if (options.forceRefresh) {
|
|
3162
|
-
this
|
|
3163
|
-
this.#documents = void 0;
|
|
3164
|
-
this.#state.documents = {
|
|
3165
|
-
count: 0,
|
|
3166
|
-
loaded: false
|
|
3167
|
-
};
|
|
3435
|
+
this.resetRemoteState();
|
|
3168
3436
|
this.emitStateUpdate();
|
|
3169
3437
|
}
|
|
3170
3438
|
if (this.#documents) return this.#documents;
|
|
@@ -3205,16 +3473,57 @@ var GranolaApp = class {
|
|
|
3205
3473
|
if (options.required && !cacheData) throw this.missingCacheError();
|
|
3206
3474
|
return cacheData;
|
|
3207
3475
|
}
|
|
3476
|
+
async listFolders(options = {}) {
|
|
3477
|
+
const summaries = filterFolders((await this.loadFolders({
|
|
3478
|
+
forceRefresh: options.forceRefresh,
|
|
3479
|
+
required: true
|
|
3480
|
+
}) ?? []).map((folder) => buildFolderSummary(folder)), {
|
|
3481
|
+
limit: options.limit,
|
|
3482
|
+
search: options.search
|
|
3483
|
+
});
|
|
3484
|
+
this.setUiState({
|
|
3485
|
+
folderSearch: options.search,
|
|
3486
|
+
selectedFolderId: void 0,
|
|
3487
|
+
view: "folder-list"
|
|
3488
|
+
});
|
|
3489
|
+
return { folders: summaries };
|
|
3490
|
+
}
|
|
3491
|
+
async getFolder(id) {
|
|
3492
|
+
const folders = await this.loadFolders({ required: true });
|
|
3493
|
+
const cacheData = await this.loadCache();
|
|
3494
|
+
const documents = await this.listDocuments();
|
|
3495
|
+
const folder = resolveFolder((folders ?? []).map((folder) => buildFolderSummary(folder)), id);
|
|
3496
|
+
const rawFolder = (folders ?? []).find((candidate) => candidate.id === folder.id);
|
|
3497
|
+
if (!rawFolder) throw new Error(`folder not found: ${id}`);
|
|
3498
|
+
const record = buildFolderRecord(rawFolder, listMeetings(documents, {
|
|
3499
|
+
cacheData,
|
|
3500
|
+
folderId: folder.id,
|
|
3501
|
+
foldersByDocumentId: this.buildFoldersByDocumentId(folders),
|
|
3502
|
+
limit: Math.max(rawFolder.documentIds.length, 1),
|
|
3503
|
+
sort: "updated-desc"
|
|
3504
|
+
}));
|
|
3505
|
+
this.setUiState({
|
|
3506
|
+
selectedFolderId: folder.id,
|
|
3507
|
+
view: "folder-detail"
|
|
3508
|
+
});
|
|
3509
|
+
return record;
|
|
3510
|
+
}
|
|
3511
|
+
async findFolder(query) {
|
|
3512
|
+
const summary = resolveFolderQuery((await this.loadFolders({ required: true }) ?? []).map((folder) => buildFolderSummary(folder)), query);
|
|
3513
|
+
return await this.getFolder(summary.id);
|
|
3514
|
+
}
|
|
3208
3515
|
async listMeetings(options = {}) {
|
|
3209
3516
|
const preferIndex = options.preferIndex ?? (this.#state.ui.surface === "web" || this.#state.ui.surface === "server");
|
|
3210
3517
|
if (!options.forceRefresh && preferIndex && !this.#documents && this.#meetingIndex.length > 0) {
|
|
3211
3518
|
const meetings = filterMeetingSummaries(this.#meetingIndex, options);
|
|
3212
3519
|
this.setUiState({
|
|
3520
|
+
folderSearch: void 0,
|
|
3213
3521
|
meetingListSource: "index",
|
|
3214
3522
|
meetingSearch: options.search,
|
|
3215
3523
|
meetingSort: options.sort,
|
|
3216
3524
|
meetingUpdatedFrom: options.updatedFrom,
|
|
3217
3525
|
meetingUpdatedTo: options.updatedTo,
|
|
3526
|
+
selectedFolderId: options.folderId,
|
|
3218
3527
|
selectedMeetingId: void 0,
|
|
3219
3528
|
view: "meeting-list"
|
|
3220
3529
|
});
|
|
@@ -3226,8 +3535,14 @@ var GranolaApp = class {
|
|
|
3226
3535
|
}
|
|
3227
3536
|
const cacheData = await this.loadCache();
|
|
3228
3537
|
const documents = await this.listDocuments({ forceRefresh: options.forceRefresh });
|
|
3538
|
+
const folders = await this.loadFolders({
|
|
3539
|
+
forceRefresh: options.forceRefresh,
|
|
3540
|
+
required: Boolean(options.folderId)
|
|
3541
|
+
});
|
|
3229
3542
|
const meetings = listMeetings(documents, {
|
|
3230
3543
|
cacheData,
|
|
3544
|
+
folderId: options.folderId,
|
|
3545
|
+
foldersByDocumentId: this.buildFoldersByDocumentId(folders),
|
|
3231
3546
|
limit: options.limit,
|
|
3232
3547
|
search: options.search,
|
|
3233
3548
|
sort: options.sort,
|
|
@@ -3236,15 +3551,18 @@ var GranolaApp = class {
|
|
|
3236
3551
|
});
|
|
3237
3552
|
await this.persistMeetingIndex(listMeetings(documents, {
|
|
3238
3553
|
cacheData,
|
|
3554
|
+
foldersByDocumentId: this.buildFoldersByDocumentId(folders),
|
|
3239
3555
|
limit: Math.max(documents.length, 1),
|
|
3240
3556
|
sort: "updated-desc"
|
|
3241
3557
|
}));
|
|
3242
3558
|
this.setUiState({
|
|
3559
|
+
folderSearch: void 0,
|
|
3243
3560
|
meetingListSource: "live",
|
|
3244
3561
|
meetingSearch: options.search,
|
|
3245
3562
|
meetingSort: options.sort,
|
|
3246
3563
|
meetingUpdatedFrom: options.updatedFrom,
|
|
3247
3564
|
meetingUpdatedTo: options.updatedTo,
|
|
3565
|
+
selectedFolderId: options.folderId,
|
|
3248
3566
|
selectedMeetingId: void 0,
|
|
3249
3567
|
view: "meeting-list"
|
|
3250
3568
|
});
|
|
@@ -3256,9 +3574,11 @@ var GranolaApp = class {
|
|
|
3256
3574
|
async getMeeting(id, options = {}) {
|
|
3257
3575
|
const documents = await this.listDocuments();
|
|
3258
3576
|
const cacheData = await this.loadCache({ required: options.requireCache });
|
|
3577
|
+
const folders = await this.loadFolders();
|
|
3259
3578
|
const document = resolveMeeting(documents, id);
|
|
3260
|
-
const meeting = buildMeetingRecord(document, cacheData);
|
|
3579
|
+
const meeting = buildMeetingRecord(document, cacheData, this.buildFoldersByDocumentId(folders)?.get(document.id));
|
|
3261
3580
|
this.setUiState({
|
|
3581
|
+
selectedFolderId: meeting.meeting.folders[0]?.id,
|
|
3262
3582
|
selectedMeetingId: document.id,
|
|
3263
3583
|
view: "meeting-detail"
|
|
3264
3584
|
});
|
|
@@ -3271,9 +3591,11 @@ var GranolaApp = class {
|
|
|
3271
3591
|
async findMeeting(query, options = {}) {
|
|
3272
3592
|
const documents = await this.listDocuments();
|
|
3273
3593
|
const cacheData = await this.loadCache({ required: options.requireCache });
|
|
3594
|
+
const folders = await this.loadFolders();
|
|
3274
3595
|
const document = resolveMeetingQuery(documents, query);
|
|
3275
|
-
const meeting = buildMeetingRecord(document, cacheData);
|
|
3596
|
+
const meeting = buildMeetingRecord(document, cacheData, this.buildFoldersByDocumentId(folders)?.get(document.id));
|
|
3276
3597
|
this.setUiState({
|
|
3598
|
+
selectedFolderId: meeting.meeting.folders[0]?.id,
|
|
3277
3599
|
selectedMeetingId: document.id,
|
|
3278
3600
|
view: "meeting-detail"
|
|
3279
3601
|
});
|
|
@@ -3670,7 +3992,7 @@ function resolveListFormat$1(value) {
|
|
|
3670
3992
|
default: throw new Error("invalid exports format: expected text, json, or yaml");
|
|
3671
3993
|
}
|
|
3672
3994
|
}
|
|
3673
|
-
function parseLimit$
|
|
3995
|
+
function parseLimit$2(value) {
|
|
3674
3996
|
if (value === void 0) return 20;
|
|
3675
3997
|
if (typeof value !== "string" || !/^\d+$/.test(value)) throw new Error("invalid exports limit: expected a positive integer");
|
|
3676
3998
|
const limit = Number(value);
|
|
@@ -3699,7 +4021,7 @@ const exportsCommand = {
|
|
|
3699
4021
|
async run({ commandArgs, commandFlags, globalFlags }) {
|
|
3700
4022
|
const [action, id] = commandArgs;
|
|
3701
4023
|
switch (action) {
|
|
3702
|
-
case "list": return await list$
|
|
4024
|
+
case "list": return await list$2(commandFlags, globalFlags);
|
|
3703
4025
|
case "rerun":
|
|
3704
4026
|
if (!id) throw new Error("exports rerun requires a job id");
|
|
3705
4027
|
return await rerun(id, commandFlags, globalFlags);
|
|
@@ -3710,9 +4032,9 @@ const exportsCommand = {
|
|
|
3710
4032
|
}
|
|
3711
4033
|
}
|
|
3712
4034
|
};
|
|
3713
|
-
async function list$
|
|
4035
|
+
async function list$2(commandFlags, globalFlags) {
|
|
3714
4036
|
const format = resolveListFormat$1(commandFlags.format);
|
|
3715
|
-
const limit = parseLimit$
|
|
4037
|
+
const limit = parseLimit$2(commandFlags.limit);
|
|
3716
4038
|
const config = await loadConfig({
|
|
3717
4039
|
globalFlags,
|
|
3718
4040
|
subcommandFlags: commandFlags
|
|
@@ -3739,6 +4061,110 @@ async function rerun(id, commandFlags, globalFlags) {
|
|
|
3739
4061
|
return 0;
|
|
3740
4062
|
}
|
|
3741
4063
|
//#endregion
|
|
4064
|
+
//#region src/commands/folder.ts
|
|
4065
|
+
function folderHelp() {
|
|
4066
|
+
return `Granola folder
|
|
4067
|
+
|
|
4068
|
+
Usage:
|
|
4069
|
+
granola folder <list|view> [options]
|
|
4070
|
+
|
|
4071
|
+
Subcommands:
|
|
4072
|
+
list List folders from the Granola API
|
|
4073
|
+
view <id|name> Show one folder and its meetings
|
|
4074
|
+
|
|
4075
|
+
Options:
|
|
4076
|
+
--format <value> text, json, yaml
|
|
4077
|
+
--limit <n> Number of folders for list (default: 20)
|
|
4078
|
+
--search <query> Filter folders by id, name, or description
|
|
4079
|
+
--timeout <value> Request timeout, e.g. 2m, 30s, 120000 (default: 2m)
|
|
4080
|
+
--supabase <path> Path to supabase.json
|
|
4081
|
+
--debug Enable debug logging
|
|
4082
|
+
--config <path> Path to .granola.toml
|
|
4083
|
+
-h, --help Show help
|
|
4084
|
+
`;
|
|
4085
|
+
}
|
|
4086
|
+
function resolveFolderListFormat(value) {
|
|
4087
|
+
switch (value) {
|
|
4088
|
+
case void 0: return "text";
|
|
4089
|
+
case "json":
|
|
4090
|
+
case "text":
|
|
4091
|
+
case "yaml": return value;
|
|
4092
|
+
default: throw new Error("invalid folder format: expected text, json, or yaml");
|
|
4093
|
+
}
|
|
4094
|
+
}
|
|
4095
|
+
function resolveFolderDetailFormat(value) {
|
|
4096
|
+
return resolveFolderListFormat(value);
|
|
4097
|
+
}
|
|
4098
|
+
function parseLimit$1(value) {
|
|
4099
|
+
if (value === void 0) return 20;
|
|
4100
|
+
if (typeof value !== "string" || !/^\d+$/.test(value)) throw new Error("invalid folder limit: expected a positive integer");
|
|
4101
|
+
const limit = Number(value);
|
|
4102
|
+
if (!Number.isInteger(limit) || limit < 1) throw new Error("invalid folder limit: expected a positive integer");
|
|
4103
|
+
return limit;
|
|
4104
|
+
}
|
|
4105
|
+
const folderCommand = {
|
|
4106
|
+
description: "Inspect Granola folders and their meetings",
|
|
4107
|
+
flags: {
|
|
4108
|
+
format: { type: "string" },
|
|
4109
|
+
help: { type: "boolean" },
|
|
4110
|
+
limit: { type: "string" },
|
|
4111
|
+
search: { type: "string" },
|
|
4112
|
+
timeout: { type: "string" }
|
|
4113
|
+
},
|
|
4114
|
+
help: folderHelp,
|
|
4115
|
+
name: "folder",
|
|
4116
|
+
async run({ commandArgs, commandFlags, globalFlags }) {
|
|
4117
|
+
const [action, query] = commandArgs;
|
|
4118
|
+
switch (action) {
|
|
4119
|
+
case "list": return await list$1(commandFlags, globalFlags);
|
|
4120
|
+
case "view":
|
|
4121
|
+
if (!query) throw new Error("folder view requires an id or name");
|
|
4122
|
+
return await view$1(query, commandFlags, globalFlags);
|
|
4123
|
+
case void 0:
|
|
4124
|
+
console.log(folderHelp());
|
|
4125
|
+
return 1;
|
|
4126
|
+
default: throw new Error("invalid folder command: expected list or view");
|
|
4127
|
+
}
|
|
4128
|
+
}
|
|
4129
|
+
};
|
|
4130
|
+
async function list$1(commandFlags, globalFlags) {
|
|
4131
|
+
const format = resolveFolderListFormat(commandFlags.format);
|
|
4132
|
+
const limit = parseLimit$1(commandFlags.limit);
|
|
4133
|
+
const search = typeof commandFlags.search === "string" ? commandFlags.search : void 0;
|
|
4134
|
+
const config = await loadConfig({
|
|
4135
|
+
globalFlags,
|
|
4136
|
+
subcommandFlags: commandFlags
|
|
4137
|
+
});
|
|
4138
|
+
debug(config.debug, "using config", config.configFileUsed ?? "(none)");
|
|
4139
|
+
debug(config.debug, "supabase", config.supabase);
|
|
4140
|
+
debug(config.debug, "timeoutMs", config.notes.timeoutMs);
|
|
4141
|
+
const app = await createGranolaApp(config);
|
|
4142
|
+
debug(config.debug, "authMode", app.getState().auth.mode);
|
|
4143
|
+
console.log("Loading folders...");
|
|
4144
|
+
const result = await app.listFolders({
|
|
4145
|
+
limit,
|
|
4146
|
+
search
|
|
4147
|
+
});
|
|
4148
|
+
console.log(renderFolderList(result.folders, format).trimEnd());
|
|
4149
|
+
return 0;
|
|
4150
|
+
}
|
|
4151
|
+
async function view$1(query, commandFlags, globalFlags) {
|
|
4152
|
+
const format = resolveFolderDetailFormat(commandFlags.format);
|
|
4153
|
+
const config = await loadConfig({
|
|
4154
|
+
globalFlags,
|
|
4155
|
+
subcommandFlags: commandFlags
|
|
4156
|
+
});
|
|
4157
|
+
debug(config.debug, "using config", config.configFileUsed ?? "(none)");
|
|
4158
|
+
debug(config.debug, "supabase", config.supabase);
|
|
4159
|
+
debug(config.debug, "timeoutMs", config.notes.timeoutMs);
|
|
4160
|
+
const app = await createGranolaApp(config);
|
|
4161
|
+
debug(config.debug, "authMode", app.getState().auth.mode);
|
|
4162
|
+
console.log("Fetching folder from Granola API...");
|
|
4163
|
+
const result = await app.findFolder(query);
|
|
4164
|
+
console.log(renderFolderView(result, format).trimEnd());
|
|
4165
|
+
return 0;
|
|
4166
|
+
}
|
|
4167
|
+
//#endregion
|
|
3742
4168
|
//#region src/browser.ts
|
|
3743
4169
|
const execFileAsync = promisify(execFile);
|
|
3744
4170
|
function getBrowserOpenCommand(url, platform = process.platform) {
|
|
@@ -5430,6 +5856,7 @@ async function startGranolaServer(app, options = {}) {
|
|
|
5430
5856
|
auth: true,
|
|
5431
5857
|
events: true,
|
|
5432
5858
|
exports: true,
|
|
5859
|
+
folders: true,
|
|
5433
5860
|
meetingOpen: true,
|
|
5434
5861
|
webClient: enableWebClient
|
|
5435
5862
|
},
|
|
@@ -5439,7 +5866,7 @@ async function startGranolaServer(app, options = {}) {
|
|
|
5439
5866
|
sessionStore: defaultGranolaToolkitPersistenceLayout().sessionStoreKind
|
|
5440
5867
|
},
|
|
5441
5868
|
product: "granola-toolkit",
|
|
5442
|
-
protocolVersion:
|
|
5869
|
+
protocolVersion: 2,
|
|
5443
5870
|
transport: "local-http"
|
|
5444
5871
|
};
|
|
5445
5872
|
const server = createServer(async (request, response) => {
|
|
@@ -5561,6 +5988,7 @@ async function startGranolaServer(app, options = {}) {
|
|
|
5561
5988
|
return;
|
|
5562
5989
|
}
|
|
5563
5990
|
if (method === "GET" && path === granolaTransportPaths.meetings) {
|
|
5991
|
+
const folderId = url.searchParams.get("folderId")?.trim() || void 0;
|
|
5564
5992
|
const limit = parseInteger(url.searchParams.get("limit"));
|
|
5565
5993
|
const refresh = url.searchParams.get("refresh") === "true";
|
|
5566
5994
|
const search = url.searchParams.get("search")?.trim() || void 0;
|
|
@@ -5568,6 +5996,7 @@ async function startGranolaServer(app, options = {}) {
|
|
|
5568
5996
|
const updatedFrom = url.searchParams.get("updatedFrom")?.trim() || void 0;
|
|
5569
5997
|
const updatedTo = url.searchParams.get("updatedTo")?.trim() || void 0;
|
|
5570
5998
|
const result = await app.listMeetings({
|
|
5999
|
+
folderId,
|
|
5571
6000
|
forceRefresh: refresh,
|
|
5572
6001
|
limit,
|
|
5573
6002
|
search,
|
|
@@ -5576,6 +6005,7 @@ async function startGranolaServer(app, options = {}) {
|
|
|
5576
6005
|
updatedTo
|
|
5577
6006
|
});
|
|
5578
6007
|
sendJson(response, {
|
|
6008
|
+
folderId,
|
|
5579
6009
|
meetings: result.meetings,
|
|
5580
6010
|
refresh,
|
|
5581
6011
|
search,
|
|
@@ -5586,12 +6016,39 @@ async function startGranolaServer(app, options = {}) {
|
|
|
5586
6016
|
}, { headers: originHeaders });
|
|
5587
6017
|
return;
|
|
5588
6018
|
}
|
|
6019
|
+
if (method === "GET" && path === granolaTransportPaths.folders) {
|
|
6020
|
+
const limit = parseInteger(url.searchParams.get("limit"));
|
|
6021
|
+
const refresh = url.searchParams.get("refresh") === "true";
|
|
6022
|
+
const search = url.searchParams.get("search")?.trim() || void 0;
|
|
6023
|
+
sendJson(response, {
|
|
6024
|
+
folders: (await app.listFolders({
|
|
6025
|
+
forceRefresh: refresh,
|
|
6026
|
+
limit,
|
|
6027
|
+
search
|
|
6028
|
+
})).folders,
|
|
6029
|
+
refresh,
|
|
6030
|
+
search
|
|
6031
|
+
}, { headers: originHeaders });
|
|
6032
|
+
return;
|
|
6033
|
+
}
|
|
6034
|
+
if (method === "GET" && path === granolaTransportPaths.folderResolve) {
|
|
6035
|
+
const query = url.searchParams.get("q")?.trim();
|
|
6036
|
+
if (!query) throw new Error("folder query is required");
|
|
6037
|
+
sendJson(response, await app.findFolder(query), { headers: originHeaders });
|
|
6038
|
+
return;
|
|
6039
|
+
}
|
|
5589
6040
|
if (method === "GET" && path === granolaTransportPaths.meetingResolve) {
|
|
5590
6041
|
const query = url.searchParams.get("q")?.trim();
|
|
5591
6042
|
if (!query) throw new Error("meeting query is required");
|
|
5592
6043
|
sendJson(response, await app.findMeeting(query, { requireCache: url.searchParams.get("includeTranscript") === "true" }), { headers: originHeaders });
|
|
5593
6044
|
return;
|
|
5594
6045
|
}
|
|
6046
|
+
if (method === "GET" && path.startsWith(`${granolaTransportPaths.folders}/`) && path !== granolaTransportPaths.folderResolve) {
|
|
6047
|
+
const id = decodeURIComponent(path.slice(`${granolaTransportPaths.folders}/`.length));
|
|
6048
|
+
if (!id) throw new Error("folder id is required");
|
|
6049
|
+
sendJson(response, await app.getFolder(id), { headers: originHeaders });
|
|
6050
|
+
return;
|
|
6051
|
+
}
|
|
5595
6052
|
if (method === "GET" && path.startsWith(`${granolaTransportPaths.meetings}/`) && path !== granolaTransportPaths.meetingResolve) {
|
|
5596
6053
|
const id = decodeURIComponent(path.slice(`${granolaTransportPaths.meetings}/`.length));
|
|
5597
6054
|
if (!id) throw new Error("meeting id is required");
|
|
@@ -5776,6 +6233,7 @@ Subcommands:
|
|
|
5776
6233
|
|
|
5777
6234
|
Options:
|
|
5778
6235
|
--cache <path> Path to Granola cache JSON for transcript data
|
|
6236
|
+
--folder <query> Filter list to one folder id or name
|
|
5779
6237
|
--format <value> list/view: text, json, yaml; export: json, yaml; notes: markdown, json, yaml, raw; transcript: text, json, yaml, raw
|
|
5780
6238
|
--network <mode> open: local or lan (default: local)
|
|
5781
6239
|
--hostname <value> open: hostname to bind (overrides network default)
|
|
@@ -5849,6 +6307,7 @@ const meetingCommand = {
|
|
|
5849
6307
|
description: "Inspect and export individual Granola meetings",
|
|
5850
6308
|
flags: {
|
|
5851
6309
|
cache: { type: "string" },
|
|
6310
|
+
folder: { type: "string" },
|
|
5852
6311
|
format: { type: "string" },
|
|
5853
6312
|
help: { type: "boolean" },
|
|
5854
6313
|
hostname: { type: "string" },
|
|
@@ -5892,6 +6351,7 @@ const meetingCommand = {
|
|
|
5892
6351
|
async function list(commandFlags, globalFlags) {
|
|
5893
6352
|
const format = resolveListFormat(commandFlags.format);
|
|
5894
6353
|
const limit = parseLimit(commandFlags.limit);
|
|
6354
|
+
const folderQuery = typeof commandFlags.folder === "string" ? commandFlags.folder : void 0;
|
|
5895
6355
|
const search = typeof commandFlags.search === "string" ? commandFlags.search : void 0;
|
|
5896
6356
|
const config = await loadConfig({
|
|
5897
6357
|
globalFlags,
|
|
@@ -5904,11 +6364,15 @@ async function list(commandFlags, globalFlags) {
|
|
|
5904
6364
|
const app = await createGranolaApp(config);
|
|
5905
6365
|
debug(config.debug, "authMode", app.getState().auth.mode);
|
|
5906
6366
|
console.log("Loading meetings...");
|
|
6367
|
+
const folder = folderQuery ? await app.findFolder(folderQuery) : void 0;
|
|
6368
|
+
const folderId = folder?.id;
|
|
5907
6369
|
const result = await app.listMeetings({
|
|
6370
|
+
folderId,
|
|
5908
6371
|
limit,
|
|
5909
6372
|
search
|
|
5910
6373
|
});
|
|
5911
6374
|
console.log(result.source === "index" ? "Loaded meetings from the local index" : "Fetched meetings from Granola API");
|
|
6375
|
+
if (folder) console.log(`Folder: ${folder.name} (${folder.id})`);
|
|
5912
6376
|
console.log(renderMeetingList(result.meetings, format).trimEnd());
|
|
5913
6377
|
return 0;
|
|
5914
6378
|
}
|
|
@@ -6271,6 +6735,7 @@ const commands = [
|
|
|
6271
6735
|
attachCommand,
|
|
6272
6736
|
authCommand,
|
|
6273
6737
|
exportsCommand,
|
|
6738
|
+
folderCommand,
|
|
6274
6739
|
meetingCommand,
|
|
6275
6740
|
notesCommand,
|
|
6276
6741
|
serveCommand,
|
|
@@ -6399,6 +6864,7 @@ Global options:
|
|
|
6399
6864
|
|
|
6400
6865
|
Examples:
|
|
6401
6866
|
granola attach http://127.0.0.1:4123
|
|
6867
|
+
granola folder list
|
|
6402
6868
|
granola notes --supabase "${granolaSupabaseCandidates()[0] ?? "/path/to/supabase.json"}"
|
|
6403
6869
|
granola transcripts --cache "${granolaCacheCandidates()[0] ?? "/path/to/cache-v3.json"}"
|
|
6404
6870
|
`;
|