granola-toolkit 0.30.0 → 0.32.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 +30 -0
- package/dist/cli.js +718 -49
- package/package.json +1 -1
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) {
|
|
@@ -3778,10 +4204,13 @@ const workspaceTabs = ["notes", "transcript", "metadata", "raw"];
|
|
|
3778
4204
|
const state = {
|
|
3779
4205
|
appState: null,
|
|
3780
4206
|
detailError: "",
|
|
4207
|
+
folderError: "",
|
|
4208
|
+
folders: [],
|
|
3781
4209
|
listError: "",
|
|
3782
4210
|
meetings: [],
|
|
3783
4211
|
quickOpen: "",
|
|
3784
4212
|
search: "",
|
|
4213
|
+
selectedFolderId: null,
|
|
3785
4214
|
selectedMeeting: null,
|
|
3786
4215
|
selectedMeetingBundle: null,
|
|
3787
4216
|
selectedMeetingId: null,
|
|
@@ -3799,6 +4228,7 @@ const els = {
|
|
|
3799
4228
|
detailBody: document.querySelector("[data-detail-body]"),
|
|
3800
4229
|
detailMeta: document.querySelector("[data-detail-meta]"),
|
|
3801
4230
|
empty: document.querySelector("[data-empty]"),
|
|
4231
|
+
folderList: document.querySelector("[data-folder-list]"),
|
|
3802
4232
|
jobsList: document.querySelector("[data-jobs-list]"),
|
|
3803
4233
|
list: document.querySelector("[data-meeting-list]"),
|
|
3804
4234
|
noteButton: document.querySelector("[data-export-notes]"),
|
|
@@ -3825,6 +4255,7 @@ function parseWorkspaceTab(value) {
|
|
|
3825
4255
|
function startupSelection() {
|
|
3826
4256
|
const params = new URLSearchParams(window.location.search);
|
|
3827
4257
|
return {
|
|
4258
|
+
folderId: params.get("folder")?.trim() || "",
|
|
3828
4259
|
meetingId: params.get("meeting")?.trim() || "",
|
|
3829
4260
|
workspaceTab: parseWorkspaceTab(params.get("tab")),
|
|
3830
4261
|
};
|
|
@@ -3833,6 +4264,12 @@ function startupSelection() {
|
|
|
3833
4264
|
function syncBrowserUrl() {
|
|
3834
4265
|
const url = new URL(window.location.href);
|
|
3835
4266
|
|
|
4267
|
+
if (state.selectedFolderId) {
|
|
4268
|
+
url.searchParams.set("folder", state.selectedFolderId);
|
|
4269
|
+
} else {
|
|
4270
|
+
url.searchParams.delete("folder");
|
|
4271
|
+
}
|
|
4272
|
+
|
|
3836
4273
|
if (state.selectedMeetingId) {
|
|
3837
4274
|
url.searchParams.set("meeting", state.selectedMeetingId);
|
|
3838
4275
|
} else {
|
|
@@ -3876,6 +4313,11 @@ function syncFilterInputs() {
|
|
|
3876
4313
|
function currentFilterSummary() {
|
|
3877
4314
|
const parts = [];
|
|
3878
4315
|
|
|
4316
|
+
if (state.selectedFolderId) {
|
|
4317
|
+
const folder = state.folders.find((candidate) => candidate.id === state.selectedFolderId);
|
|
4318
|
+
parts.push("folder " + (folder ? '"' + folder.name + '"' : '"' + state.selectedFolderId + '"'));
|
|
4319
|
+
}
|
|
4320
|
+
|
|
3879
4321
|
if (state.search) {
|
|
3880
4322
|
parts.push('search "' + state.search + '"');
|
|
3881
4323
|
}
|
|
@@ -3918,6 +4360,9 @@ function renderAppState() {
|
|
|
3918
4360
|
: appState.index.available
|
|
3919
4361
|
? "available"
|
|
3920
4362
|
: "not built";
|
|
4363
|
+
const folderStatus = appState.folders.loaded
|
|
4364
|
+
? appState.folders.count + " folders"
|
|
4365
|
+
: "not loaded";
|
|
3921
4366
|
|
|
3922
4367
|
els.appState.innerHTML = [
|
|
3923
4368
|
'<div class="status-grid">',
|
|
@@ -3925,6 +4370,7 @@ function renderAppState() {
|
|
|
3925
4370
|
'<div><span class="status-label">View</span><strong>' + escapeHtml(appState.ui.view) + "</strong></div>",
|
|
3926
4371
|
'<div><span class="status-label">Auth</span><strong>' + escapeHtml(authMode) + "</strong></div>",
|
|
3927
4372
|
'<div><span class="status-label">Documents</span><strong>' + escapeHtml(docs) + "</strong></div>",
|
|
4373
|
+
'<div><span class="status-label">Folders</span><strong>' + escapeHtml(folderStatus) + "</strong></div>",
|
|
3928
4374
|
'<div><span class="status-label">Cache</span><strong>' + escapeHtml(cache) + "</strong></div>",
|
|
3929
4375
|
'<div><span class="status-label">Index</span><strong>' + escapeHtml(indexStatus) + "</strong></div>",
|
|
3930
4376
|
"</div>",
|
|
@@ -3935,6 +4381,50 @@ function renderAppState() {
|
|
|
3935
4381
|
renderExportJobs();
|
|
3936
4382
|
}
|
|
3937
4383
|
|
|
4384
|
+
function renderFolderList() {
|
|
4385
|
+
if (state.folderError) {
|
|
4386
|
+
els.folderList.innerHTML =
|
|
4387
|
+
'<div class="folder-empty folder-empty--error">' + escapeHtml(state.folderError) + "</div>";
|
|
4388
|
+
return;
|
|
4389
|
+
}
|
|
4390
|
+
|
|
4391
|
+
const buttons = [
|
|
4392
|
+
[
|
|
4393
|
+
'<button class="folder-row"' +
|
|
4394
|
+
(state.selectedFolderId ? "" : ' data-selected="true"') +
|
|
4395
|
+
' data-folder-id="">',
|
|
4396
|
+
'<span class="folder-row__title">All meetings</span>',
|
|
4397
|
+
'<span class="folder-row__meta">Browse the full meeting list.</span>',
|
|
4398
|
+
"</button>",
|
|
4399
|
+
].join(""),
|
|
4400
|
+
];
|
|
4401
|
+
|
|
4402
|
+
for (const folder of state.folders) {
|
|
4403
|
+
buttons.push(
|
|
4404
|
+
[
|
|
4405
|
+
'<button class="folder-row"' +
|
|
4406
|
+
(folder.id === state.selectedFolderId ? ' data-selected="true"' : "") +
|
|
4407
|
+
' data-folder-id="' +
|
|
4408
|
+
escapeHtml(folder.id) +
|
|
4409
|
+
'">',
|
|
4410
|
+
'<span class="folder-row__title">' +
|
|
4411
|
+
escapeHtml((folder.isFavourite ? "★ " : "") + (folder.name || folder.id)) +
|
|
4412
|
+
"</span>",
|
|
4413
|
+
'<span class="folder-row__meta">' +
|
|
4414
|
+
escapeHtml(String(folder.documentCount) + " meetings") +
|
|
4415
|
+
"</span>",
|
|
4416
|
+
"</button>",
|
|
4417
|
+
].join(""),
|
|
4418
|
+
);
|
|
4419
|
+
}
|
|
4420
|
+
|
|
4421
|
+
if (buttons.length === 1) {
|
|
4422
|
+
buttons.push('<div class="folder-empty">No folders found.</div>');
|
|
4423
|
+
}
|
|
4424
|
+
|
|
4425
|
+
els.folderList.innerHTML = buttons.join("");
|
|
4426
|
+
}
|
|
4427
|
+
|
|
3938
4428
|
function renderSecurityPanel() {
|
|
3939
4429
|
els.securityPanel.hidden = !state.serverLocked;
|
|
3940
4430
|
}
|
|
@@ -4078,6 +4568,7 @@ function renderMeetingDetail() {
|
|
|
4078
4568
|
"Title: " + (record.meeting.title || record.meeting.id),
|
|
4079
4569
|
"Created: " + record.meeting.createdAt,
|
|
4080
4570
|
"Updated: " + record.meeting.updatedAt,
|
|
4571
|
+
"Folders: " + (record.meeting.folders.length ? record.meeting.folders.map((folder) => folder.name).join(", ") : "none"),
|
|
4081
4572
|
"Tags: " + (record.meeting.tags.length ? record.meeting.tags.join(", ") : "none"),
|
|
4082
4573
|
"Transcript loaded: " + (record.meeting.transcriptLoaded ? "yes" : "no"),
|
|
4083
4574
|
].join("\n");
|
|
@@ -4187,6 +4678,10 @@ function buildMeetingsQuery(limit = 100, refresh = false) {
|
|
|
4187
4678
|
params.set("updatedTo", state.updatedTo);
|
|
4188
4679
|
}
|
|
4189
4680
|
|
|
4681
|
+
if (state.selectedFolderId) {
|
|
4682
|
+
params.set("folderId", state.selectedFolderId);
|
|
4683
|
+
}
|
|
4684
|
+
|
|
4190
4685
|
if (refresh) {
|
|
4191
4686
|
params.set("refresh", "true");
|
|
4192
4687
|
}
|
|
@@ -4194,6 +4689,39 @@ function buildMeetingsQuery(limit = 100, refresh = false) {
|
|
|
4194
4689
|
return "?" + params.toString();
|
|
4195
4690
|
}
|
|
4196
4691
|
|
|
4692
|
+
async function loadFolders(options = {}) {
|
|
4693
|
+
const refresh = options.refresh === true;
|
|
4694
|
+
|
|
4695
|
+
try {
|
|
4696
|
+
state.folderError = "";
|
|
4697
|
+
const params = new URLSearchParams();
|
|
4698
|
+
params.set("limit", "100");
|
|
4699
|
+
if (refresh) {
|
|
4700
|
+
params.set("refresh", "true");
|
|
4701
|
+
}
|
|
4702
|
+
|
|
4703
|
+
const payload = await fetchJson("/folders?" + params.toString());
|
|
4704
|
+
state.folders = payload.folders || [];
|
|
4705
|
+
if (
|
|
4706
|
+
state.selectedFolderId &&
|
|
4707
|
+
!state.folders.some((folder) => folder.id === state.selectedFolderId)
|
|
4708
|
+
) {
|
|
4709
|
+
state.selectedFolderId = null;
|
|
4710
|
+
}
|
|
4711
|
+
} catch (error) {
|
|
4712
|
+
if (error.authRequired) {
|
|
4713
|
+
throw error;
|
|
4714
|
+
}
|
|
4715
|
+
|
|
4716
|
+
state.folderError = error instanceof Error ? error.message : String(error);
|
|
4717
|
+
state.folders = [];
|
|
4718
|
+
state.selectedFolderId = null;
|
|
4719
|
+
}
|
|
4720
|
+
|
|
4721
|
+
renderFolderList();
|
|
4722
|
+
syncBrowserUrl();
|
|
4723
|
+
}
|
|
4724
|
+
|
|
4197
4725
|
async function loadMeetings(options = {}) {
|
|
4198
4726
|
const preferredMeetingId = options.preferredMeetingId || state.selectedMeetingId;
|
|
4199
4727
|
const refresh = options.refresh === true;
|
|
@@ -4257,10 +4785,12 @@ async function quickOpenMeeting() {
|
|
|
4257
4785
|
try {
|
|
4258
4786
|
state.quickOpen = query;
|
|
4259
4787
|
const payload = await fetchJson("/meetings/resolve?q=" + encodeURIComponent(query));
|
|
4788
|
+
state.selectedFolderId = payload.meeting?.meeting?.folders?.[0]?.id || null;
|
|
4260
4789
|
state.search = "";
|
|
4261
4790
|
state.updatedFrom = "";
|
|
4262
4791
|
state.updatedTo = "";
|
|
4263
4792
|
syncFilterInputs();
|
|
4793
|
+
renderFolderList();
|
|
4264
4794
|
await loadMeetings({
|
|
4265
4795
|
preferredMeetingId: payload.document.id,
|
|
4266
4796
|
});
|
|
@@ -4275,11 +4805,9 @@ async function quickOpenMeeting() {
|
|
|
4275
4805
|
async function refreshAll(forceLiveMeetings = false) {
|
|
4276
4806
|
setStatus("Refreshing…", "busy");
|
|
4277
4807
|
try {
|
|
4278
|
-
|
|
4279
|
-
|
|
4280
|
-
|
|
4281
|
-
loadMeetings({ refresh: forceLiveMeetings }),
|
|
4282
|
-
]);
|
|
4808
|
+
await loadFolders({ refresh: forceLiveMeetings });
|
|
4809
|
+
const [appState, authState] = await Promise.all([fetchJson("/state"), fetchJson("/auth/status")]);
|
|
4810
|
+
await loadMeetings({ refresh: forceLiveMeetings });
|
|
4283
4811
|
state.serverLocked = false;
|
|
4284
4812
|
state.appState = {
|
|
4285
4813
|
...appState,
|
|
@@ -4430,17 +4958,40 @@ async function lockServer() {
|
|
|
4430
4958
|
|
|
4431
4959
|
state.serverLocked = true;
|
|
4432
4960
|
state.appState = null;
|
|
4961
|
+
state.folders = [];
|
|
4433
4962
|
state.meetings = [];
|
|
4963
|
+
state.selectedFolderId = null;
|
|
4434
4964
|
state.selectedMeeting = null;
|
|
4435
4965
|
state.selectedMeetingBundle = null;
|
|
4436
4966
|
state.detailError = "";
|
|
4967
|
+
state.folderError = "";
|
|
4437
4968
|
els.serverPassword.value = "";
|
|
4438
4969
|
renderSecurityPanel();
|
|
4970
|
+
renderFolderList();
|
|
4439
4971
|
renderMeetingList();
|
|
4440
4972
|
renderMeetingDetail();
|
|
4441
4973
|
setStatus("Server locked", "error");
|
|
4442
4974
|
}
|
|
4443
4975
|
|
|
4976
|
+
els.folderList.addEventListener("click", (event) => {
|
|
4977
|
+
if (!(event.target instanceof Element)) {
|
|
4978
|
+
return;
|
|
4979
|
+
}
|
|
4980
|
+
|
|
4981
|
+
const button = event.target.closest("[data-folder-id]");
|
|
4982
|
+
if (!button) {
|
|
4983
|
+
return;
|
|
4984
|
+
}
|
|
4985
|
+
|
|
4986
|
+
const nextFolderId = button.dataset.folderId || null;
|
|
4987
|
+
state.selectedFolderId = nextFolderId;
|
|
4988
|
+
state.selectedMeetingId = null;
|
|
4989
|
+
state.selectedMeeting = null;
|
|
4990
|
+
state.selectedMeetingBundle = null;
|
|
4991
|
+
renderFolderList();
|
|
4992
|
+
void loadMeetings();
|
|
4993
|
+
});
|
|
4994
|
+
|
|
4444
4995
|
els.list.addEventListener("click", (event) => {
|
|
4445
4996
|
if (!(event.target instanceof Element)) {
|
|
4446
4997
|
return;
|
|
@@ -4645,6 +5196,7 @@ document.addEventListener("keydown", (event) => {
|
|
|
4645
5196
|
});
|
|
4646
5197
|
|
|
4647
5198
|
const initialSelection = startupSelection();
|
|
5199
|
+
state.selectedFolderId = initialSelection.folderId || null;
|
|
4648
5200
|
state.selectedMeetingId = initialSelection.meetingId || null;
|
|
4649
5201
|
state.workspaceTab = initialSelection.workspaceTab;
|
|
4650
5202
|
|
|
@@ -4660,9 +5212,12 @@ events.addEventListener("state.updated", (event) => {
|
|
|
4660
5212
|
payload.state.documents?.loadedAt &&
|
|
4661
5213
|
payload.state.documents.loadedAt !== previousLoadedAt
|
|
4662
5214
|
) {
|
|
4663
|
-
void
|
|
4664
|
-
|
|
4665
|
-
|
|
5215
|
+
void (async () => {
|
|
5216
|
+
await loadFolders();
|
|
5217
|
+
await loadMeetings({
|
|
5218
|
+
preferredMeetingId: state.selectedMeetingId,
|
|
5219
|
+
});
|
|
5220
|
+
})();
|
|
4666
5221
|
}
|
|
4667
5222
|
});
|
|
4668
5223
|
events.addEventListener("error", () => {
|
|
@@ -4671,6 +5226,7 @@ events.addEventListener("error", () => {
|
|
|
4671
5226
|
|
|
4672
5227
|
syncFilterInputs();
|
|
4673
5228
|
renderSecurityPanel();
|
|
5229
|
+
renderFolderList();
|
|
4674
5230
|
|
|
4675
5231
|
void refreshAll().catch((error) => {
|
|
4676
5232
|
setStatus("Error", "error");
|
|
@@ -4685,7 +5241,7 @@ const granolaWebMarkup = String.raw`
|
|
|
4685
5241
|
<aside class="pane sidebar">
|
|
4686
5242
|
<section class="hero">
|
|
4687
5243
|
<h1>Granola Toolkit</h1>
|
|
4688
|
-
<p>Browser workspace for meetings, notes, transcripts, and export flows on top of one local server instance.</p>
|
|
5244
|
+
<p>Browser workspace for folders, meetings, notes, transcripts, and export flows on top of one local server instance.</p>
|
|
4689
5245
|
<input class="search" data-search placeholder="Search meetings, ids, or tags" />
|
|
4690
5246
|
<div class="field-row field-row--inline">
|
|
4691
5247
|
<label>
|
|
@@ -4707,6 +5263,13 @@ const granolaWebMarkup = String.raw`
|
|
|
4707
5263
|
<input class="field-input" data-updated-to type="date" />
|
|
4708
5264
|
</label>
|
|
4709
5265
|
</section>
|
|
5266
|
+
<section class="folder-panel">
|
|
5267
|
+
<div class="folder-panel__head">
|
|
5268
|
+
<h2>Folders</h2>
|
|
5269
|
+
<p>Pick a folder to scope the meeting browser, or stay on All meetings.</p>
|
|
5270
|
+
</div>
|
|
5271
|
+
<div class="folder-list" data-folder-list></div>
|
|
5272
|
+
</section>
|
|
4710
5273
|
<section class="toolbar">
|
|
4711
5274
|
<div>
|
|
4712
5275
|
<p>Meetings are loaded from the shared server state so this view can later coexist with the terminal UI.</p>
|
|
@@ -4828,11 +5391,11 @@ body {
|
|
|
4828
5391
|
|
|
4829
5392
|
.sidebar {
|
|
4830
5393
|
display: grid;
|
|
4831
|
-
grid-template-rows: auto auto 1fr;
|
|
5394
|
+
grid-template-rows: auto auto auto 1fr;
|
|
4832
5395
|
overflow: hidden;
|
|
4833
5396
|
}
|
|
4834
5397
|
|
|
4835
|
-
.hero, .toolbar, .detail-head {
|
|
5398
|
+
.hero, .toolbar, .detail-head, .folder-panel {
|
|
4836
5399
|
padding: 22px 24px;
|
|
4837
5400
|
border-bottom: 1px solid var(--line);
|
|
4838
5401
|
}
|
|
@@ -4889,6 +5452,68 @@ body {
|
|
|
4889
5452
|
overflow: auto;
|
|
4890
5453
|
}
|
|
4891
5454
|
|
|
5455
|
+
.folder-panel {
|
|
5456
|
+
display: grid;
|
|
5457
|
+
gap: 14px;
|
|
5458
|
+
}
|
|
5459
|
+
|
|
5460
|
+
.folder-panel__head h2 {
|
|
5461
|
+
margin: 0;
|
|
5462
|
+
font-size: 0.92rem;
|
|
5463
|
+
letter-spacing: 0.08em;
|
|
5464
|
+
text-transform: uppercase;
|
|
5465
|
+
}
|
|
5466
|
+
|
|
5467
|
+
.folder-panel__head p {
|
|
5468
|
+
margin: 6px 0 0;
|
|
5469
|
+
color: var(--muted);
|
|
5470
|
+
font-size: 0.9rem;
|
|
5471
|
+
}
|
|
5472
|
+
|
|
5473
|
+
.folder-list {
|
|
5474
|
+
display: grid;
|
|
5475
|
+
gap: 10px;
|
|
5476
|
+
}
|
|
5477
|
+
|
|
5478
|
+
.folder-row {
|
|
5479
|
+
width: 100%;
|
|
5480
|
+
display: grid;
|
|
5481
|
+
gap: 4px;
|
|
5482
|
+
text-align: left;
|
|
5483
|
+
padding: 12px 14px;
|
|
5484
|
+
border: 1px solid transparent;
|
|
5485
|
+
border-radius: 16px;
|
|
5486
|
+
background: rgba(255, 255, 255, 0.72);
|
|
5487
|
+
color: inherit;
|
|
5488
|
+
cursor: pointer;
|
|
5489
|
+
transition: transform 140ms ease, border-color 140ms ease, background 140ms ease;
|
|
5490
|
+
}
|
|
5491
|
+
|
|
5492
|
+
.folder-row:hover,
|
|
5493
|
+
.folder-row[data-selected="true"] {
|
|
5494
|
+
transform: translateY(-1px);
|
|
5495
|
+
border-color: rgba(163, 79, 47, 0.26);
|
|
5496
|
+
background: var(--panel-strong);
|
|
5497
|
+
}
|
|
5498
|
+
|
|
5499
|
+
.folder-row__title {
|
|
5500
|
+
font-weight: 700;
|
|
5501
|
+
}
|
|
5502
|
+
|
|
5503
|
+
.folder-row__meta {
|
|
5504
|
+
color: var(--muted);
|
|
5505
|
+
font-size: 0.88rem;
|
|
5506
|
+
}
|
|
5507
|
+
|
|
5508
|
+
.folder-empty {
|
|
5509
|
+
color: var(--muted);
|
|
5510
|
+
font-size: 0.92rem;
|
|
5511
|
+
}
|
|
5512
|
+
|
|
5513
|
+
.folder-empty--error {
|
|
5514
|
+
color: var(--error);
|
|
5515
|
+
}
|
|
5516
|
+
|
|
4892
5517
|
.meeting-row {
|
|
4893
5518
|
width: 100%;
|
|
4894
5519
|
display: grid;
|
|
@@ -5430,6 +6055,7 @@ async function startGranolaServer(app, options = {}) {
|
|
|
5430
6055
|
auth: true,
|
|
5431
6056
|
events: true,
|
|
5432
6057
|
exports: true,
|
|
6058
|
+
folders: true,
|
|
5433
6059
|
meetingOpen: true,
|
|
5434
6060
|
webClient: enableWebClient
|
|
5435
6061
|
},
|
|
@@ -5439,7 +6065,7 @@ async function startGranolaServer(app, options = {}) {
|
|
|
5439
6065
|
sessionStore: defaultGranolaToolkitPersistenceLayout().sessionStoreKind
|
|
5440
6066
|
},
|
|
5441
6067
|
product: "granola-toolkit",
|
|
5442
|
-
protocolVersion:
|
|
6068
|
+
protocolVersion: 2,
|
|
5443
6069
|
transport: "local-http"
|
|
5444
6070
|
};
|
|
5445
6071
|
const server = createServer(async (request, response) => {
|
|
@@ -5561,6 +6187,7 @@ async function startGranolaServer(app, options = {}) {
|
|
|
5561
6187
|
return;
|
|
5562
6188
|
}
|
|
5563
6189
|
if (method === "GET" && path === granolaTransportPaths.meetings) {
|
|
6190
|
+
const folderId = url.searchParams.get("folderId")?.trim() || void 0;
|
|
5564
6191
|
const limit = parseInteger(url.searchParams.get("limit"));
|
|
5565
6192
|
const refresh = url.searchParams.get("refresh") === "true";
|
|
5566
6193
|
const search = url.searchParams.get("search")?.trim() || void 0;
|
|
@@ -5568,6 +6195,7 @@ async function startGranolaServer(app, options = {}) {
|
|
|
5568
6195
|
const updatedFrom = url.searchParams.get("updatedFrom")?.trim() || void 0;
|
|
5569
6196
|
const updatedTo = url.searchParams.get("updatedTo")?.trim() || void 0;
|
|
5570
6197
|
const result = await app.listMeetings({
|
|
6198
|
+
folderId,
|
|
5571
6199
|
forceRefresh: refresh,
|
|
5572
6200
|
limit,
|
|
5573
6201
|
search,
|
|
@@ -5576,6 +6204,7 @@ async function startGranolaServer(app, options = {}) {
|
|
|
5576
6204
|
updatedTo
|
|
5577
6205
|
});
|
|
5578
6206
|
sendJson(response, {
|
|
6207
|
+
folderId,
|
|
5579
6208
|
meetings: result.meetings,
|
|
5580
6209
|
refresh,
|
|
5581
6210
|
search,
|
|
@@ -5586,12 +6215,39 @@ async function startGranolaServer(app, options = {}) {
|
|
|
5586
6215
|
}, { headers: originHeaders });
|
|
5587
6216
|
return;
|
|
5588
6217
|
}
|
|
6218
|
+
if (method === "GET" && path === granolaTransportPaths.folders) {
|
|
6219
|
+
const limit = parseInteger(url.searchParams.get("limit"));
|
|
6220
|
+
const refresh = url.searchParams.get("refresh") === "true";
|
|
6221
|
+
const search = url.searchParams.get("search")?.trim() || void 0;
|
|
6222
|
+
sendJson(response, {
|
|
6223
|
+
folders: (await app.listFolders({
|
|
6224
|
+
forceRefresh: refresh,
|
|
6225
|
+
limit,
|
|
6226
|
+
search
|
|
6227
|
+
})).folders,
|
|
6228
|
+
refresh,
|
|
6229
|
+
search
|
|
6230
|
+
}, { headers: originHeaders });
|
|
6231
|
+
return;
|
|
6232
|
+
}
|
|
6233
|
+
if (method === "GET" && path === granolaTransportPaths.folderResolve) {
|
|
6234
|
+
const query = url.searchParams.get("q")?.trim();
|
|
6235
|
+
if (!query) throw new Error("folder query is required");
|
|
6236
|
+
sendJson(response, await app.findFolder(query), { headers: originHeaders });
|
|
6237
|
+
return;
|
|
6238
|
+
}
|
|
5589
6239
|
if (method === "GET" && path === granolaTransportPaths.meetingResolve) {
|
|
5590
6240
|
const query = url.searchParams.get("q")?.trim();
|
|
5591
6241
|
if (!query) throw new Error("meeting query is required");
|
|
5592
6242
|
sendJson(response, await app.findMeeting(query, { requireCache: url.searchParams.get("includeTranscript") === "true" }), { headers: originHeaders });
|
|
5593
6243
|
return;
|
|
5594
6244
|
}
|
|
6245
|
+
if (method === "GET" && path.startsWith(`${granolaTransportPaths.folders}/`) && path !== granolaTransportPaths.folderResolve) {
|
|
6246
|
+
const id = decodeURIComponent(path.slice(`${granolaTransportPaths.folders}/`.length));
|
|
6247
|
+
if (!id) throw new Error("folder id is required");
|
|
6248
|
+
sendJson(response, await app.getFolder(id), { headers: originHeaders });
|
|
6249
|
+
return;
|
|
6250
|
+
}
|
|
5595
6251
|
if (method === "GET" && path.startsWith(`${granolaTransportPaths.meetings}/`) && path !== granolaTransportPaths.meetingResolve) {
|
|
5596
6252
|
const id = decodeURIComponent(path.slice(`${granolaTransportPaths.meetings}/`.length));
|
|
5597
6253
|
if (!id) throw new Error("meeting id is required");
|
|
@@ -5717,7 +6373,11 @@ function printWebRoutes() {
|
|
|
5717
6373
|
console.log(" GET /auth/status");
|
|
5718
6374
|
console.log(" GET /state");
|
|
5719
6375
|
console.log(" GET /events");
|
|
6376
|
+
console.log(" GET /folders");
|
|
6377
|
+
console.log(" GET /folders/resolve?q=<query>");
|
|
6378
|
+
console.log(" GET /folders/:id");
|
|
5720
6379
|
console.log(" GET /meetings");
|
|
6380
|
+
console.log(" GET /meetings?folderId=<id>");
|
|
5721
6381
|
console.log(" GET /meetings/:id");
|
|
5722
6382
|
console.log(" GET /exports/jobs");
|
|
5723
6383
|
console.log(" POST /auth/login");
|
|
@@ -5776,6 +6436,7 @@ Subcommands:
|
|
|
5776
6436
|
|
|
5777
6437
|
Options:
|
|
5778
6438
|
--cache <path> Path to Granola cache JSON for transcript data
|
|
6439
|
+
--folder <query> Filter list to one folder id or name
|
|
5779
6440
|
--format <value> list/view: text, json, yaml; export: json, yaml; notes: markdown, json, yaml, raw; transcript: text, json, yaml, raw
|
|
5780
6441
|
--network <mode> open: local or lan (default: local)
|
|
5781
6442
|
--hostname <value> open: hostname to bind (overrides network default)
|
|
@@ -5849,6 +6510,7 @@ const meetingCommand = {
|
|
|
5849
6510
|
description: "Inspect and export individual Granola meetings",
|
|
5850
6511
|
flags: {
|
|
5851
6512
|
cache: { type: "string" },
|
|
6513
|
+
folder: { type: "string" },
|
|
5852
6514
|
format: { type: "string" },
|
|
5853
6515
|
help: { type: "boolean" },
|
|
5854
6516
|
hostname: { type: "string" },
|
|
@@ -5892,6 +6554,7 @@ const meetingCommand = {
|
|
|
5892
6554
|
async function list(commandFlags, globalFlags) {
|
|
5893
6555
|
const format = resolveListFormat(commandFlags.format);
|
|
5894
6556
|
const limit = parseLimit(commandFlags.limit);
|
|
6557
|
+
const folderQuery = typeof commandFlags.folder === "string" ? commandFlags.folder : void 0;
|
|
5895
6558
|
const search = typeof commandFlags.search === "string" ? commandFlags.search : void 0;
|
|
5896
6559
|
const config = await loadConfig({
|
|
5897
6560
|
globalFlags,
|
|
@@ -5904,11 +6567,15 @@ async function list(commandFlags, globalFlags) {
|
|
|
5904
6567
|
const app = await createGranolaApp(config);
|
|
5905
6568
|
debug(config.debug, "authMode", app.getState().auth.mode);
|
|
5906
6569
|
console.log("Loading meetings...");
|
|
6570
|
+
const folder = folderQuery ? await app.findFolder(folderQuery) : void 0;
|
|
6571
|
+
const folderId = folder?.id;
|
|
5907
6572
|
const result = await app.listMeetings({
|
|
6573
|
+
folderId,
|
|
5908
6574
|
limit,
|
|
5909
6575
|
search
|
|
5910
6576
|
});
|
|
5911
6577
|
console.log(result.source === "index" ? "Loaded meetings from the local index" : "Fetched meetings from Granola API");
|
|
6578
|
+
if (folder) console.log(`Folder: ${folder.name} (${folder.id})`);
|
|
5912
6579
|
console.log(renderMeetingList(result.meetings, format).trimEnd());
|
|
5913
6580
|
return 0;
|
|
5914
6581
|
}
|
|
@@ -6271,6 +6938,7 @@ const commands = [
|
|
|
6271
6938
|
attachCommand,
|
|
6272
6939
|
authCommand,
|
|
6273
6940
|
exportsCommand,
|
|
6941
|
+
folderCommand,
|
|
6274
6942
|
meetingCommand,
|
|
6275
6943
|
notesCommand,
|
|
6276
6944
|
serveCommand,
|
|
@@ -6399,6 +7067,7 @@ Global options:
|
|
|
6399
7067
|
|
|
6400
7068
|
Examples:
|
|
6401
7069
|
granola attach http://127.0.0.1:4123
|
|
7070
|
+
granola folder list
|
|
6402
7071
|
granola notes --supabase "${granolaSupabaseCandidates()[0] ?? "/path/to/supabase.json"}"
|
|
6403
7072
|
granola transcripts --cache "${granolaCacheCandidates()[0] ?? "/path/to/cache-v3.json"}"
|
|
6404
7073
|
`;
|