granola-toolkit 0.29.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 +38 -0
- package/dist/cli.js +647 -98
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -9,6 +9,76 @@ import { NodeHtmlMarkdown } from "node-html-markdown";
|
|
|
9
9
|
import { execFile } from "node:child_process";
|
|
10
10
|
import { promisify } from "node:util";
|
|
11
11
|
import { createServer } from "node:http";
|
|
12
|
+
//#region src/transport.ts
|
|
13
|
+
const granolaTransportPaths = {
|
|
14
|
+
authLock: "/auth/lock",
|
|
15
|
+
authLogin: "/auth/login",
|
|
16
|
+
authLogout: "/auth/logout",
|
|
17
|
+
authMode: "/auth/mode",
|
|
18
|
+
authRefresh: "/auth/refresh",
|
|
19
|
+
authStatus: "/auth/status",
|
|
20
|
+
authUnlock: "/auth/unlock",
|
|
21
|
+
events: "/events",
|
|
22
|
+
exportJobs: "/exports/jobs",
|
|
23
|
+
exportNotes: "/exports/notes",
|
|
24
|
+
exportTranscripts: "/exports/transcripts",
|
|
25
|
+
folderResolve: "/folders/resolve",
|
|
26
|
+
folders: "/folders",
|
|
27
|
+
health: "/health",
|
|
28
|
+
meetingResolve: "/meetings/resolve",
|
|
29
|
+
meetings: "/meetings",
|
|
30
|
+
root: "/",
|
|
31
|
+
serverInfo: "/server/info",
|
|
32
|
+
state: "/state"
|
|
33
|
+
};
|
|
34
|
+
function appendSearchParams(path, params) {
|
|
35
|
+
const url = new URL(path, "http://localhost");
|
|
36
|
+
for (const [key, value] of Object.entries(params)) {
|
|
37
|
+
if (value === void 0 || value === false || value === "") continue;
|
|
38
|
+
url.searchParams.set(key, String(value));
|
|
39
|
+
}
|
|
40
|
+
return `${url.pathname}${url.search}`;
|
|
41
|
+
}
|
|
42
|
+
function granolaMeetingPath(id) {
|
|
43
|
+
return `${granolaTransportPaths.meetings}/${encodeURIComponent(id)}`;
|
|
44
|
+
}
|
|
45
|
+
function granolaMeetingResolvePath(query, options = {}) {
|
|
46
|
+
return appendSearchParams(granolaTransportPaths.meetingResolve, {
|
|
47
|
+
includeTranscript: options.includeTranscript ? "true" : void 0,
|
|
48
|
+
q: query
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
function granolaMeetingsPath(options = {}) {
|
|
52
|
+
return appendSearchParams(granolaTransportPaths.meetings, {
|
|
53
|
+
folderId: options.folderId,
|
|
54
|
+
limit: options.limit,
|
|
55
|
+
refresh: options.forceRefresh ? "true" : void 0,
|
|
56
|
+
search: options.search,
|
|
57
|
+
sort: options.sort,
|
|
58
|
+
updatedFrom: options.updatedFrom,
|
|
59
|
+
updatedTo: options.updatedTo
|
|
60
|
+
});
|
|
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
|
+
}
|
|
75
|
+
function granolaExportJobsPath(options = {}) {
|
|
76
|
+
return appendSearchParams(granolaTransportPaths.exportJobs, { limit: options.limit });
|
|
77
|
+
}
|
|
78
|
+
function granolaExportJobRerunPath(id) {
|
|
79
|
+
return `${granolaTransportPaths.exportJobs}/${encodeURIComponent(id)}/rerun`;
|
|
80
|
+
}
|
|
81
|
+
//#endregion
|
|
12
82
|
//#region src/server/client.ts
|
|
13
83
|
function cloneValue(value) {
|
|
14
84
|
return structuredClone(value);
|
|
@@ -24,14 +94,6 @@ function normaliseServerUrl(serverUrl) {
|
|
|
24
94
|
parsed.hash = "";
|
|
25
95
|
return parsed;
|
|
26
96
|
}
|
|
27
|
-
function appendSearchParams(path, params) {
|
|
28
|
-
const url = new URL(path, "http://localhost");
|
|
29
|
-
for (const [key, value] of Object.entries(params)) {
|
|
30
|
-
if (value === void 0 || value === false || value === "") continue;
|
|
31
|
-
url.searchParams.set(key, String(value));
|
|
32
|
-
}
|
|
33
|
-
return `${url.pathname}${url.search}`;
|
|
34
|
-
}
|
|
35
97
|
function mergeHeaders(...values) {
|
|
36
98
|
const headers = new Headers();
|
|
37
99
|
for (const value of values) {
|
|
@@ -66,22 +128,32 @@ var GranolaServerClient = class GranolaServerClient {
|
|
|
66
128
|
#fetchImpl;
|
|
67
129
|
#password;
|
|
68
130
|
#reconnectDelayMs;
|
|
131
|
+
info;
|
|
69
132
|
#streamAbortController;
|
|
70
|
-
constructor(url, initialState, options = {}) {
|
|
133
|
+
constructor(info, url, initialState, options = {}) {
|
|
71
134
|
this.url = url;
|
|
72
135
|
this.#fetchImpl = options.fetchImpl ?? fetch;
|
|
136
|
+
this.info = cloneValue(info);
|
|
73
137
|
this.#password = options.password?.trim() || void 0;
|
|
74
138
|
this.#reconnectDelayMs = options.reconnectDelayMs ?? 1e3;
|
|
75
139
|
this.#state = cloneValue(initialState);
|
|
76
140
|
}
|
|
77
141
|
static async connect(serverUrl, options = {}) {
|
|
78
142
|
const url = normaliseServerUrl(serverUrl);
|
|
79
|
-
const
|
|
143
|
+
const fetchImpl = options.fetchImpl ?? fetch;
|
|
144
|
+
const infoResponse = await fetchImpl(new URL(granolaTransportPaths.serverInfo, url), { headers: mergeHeaders({
|
|
145
|
+
...options.password?.trim() ? { "x-granola-password": options.password.trim() } : {},
|
|
146
|
+
accept: "application/json"
|
|
147
|
+
}) });
|
|
148
|
+
if (!infoResponse.ok) throw await responseError(infoResponse);
|
|
149
|
+
const info = await infoResponse.json();
|
|
150
|
+
if (info.protocolVersion !== 2) throw new Error(`unsupported Granola transport protocol: expected 2, got ${info.protocolVersion}`);
|
|
151
|
+
const response = await fetchImpl(new URL(granolaTransportPaths.state, url), { headers: mergeHeaders({
|
|
80
152
|
...options.password?.trim() ? { "x-granola-password": options.password.trim() } : {},
|
|
81
153
|
accept: "application/json"
|
|
82
154
|
}) });
|
|
83
155
|
if (!response.ok) throw await responseError(response);
|
|
84
|
-
const client = new GranolaServerClient(url, await response.json(), options);
|
|
156
|
+
const client = new GranolaServerClient(info, url, await response.json(), options);
|
|
85
157
|
client.startEvents();
|
|
86
158
|
return client;
|
|
87
159
|
}
|
|
@@ -103,66 +175,65 @@ var GranolaServerClient = class GranolaServerClient {
|
|
|
103
175
|
};
|
|
104
176
|
}
|
|
105
177
|
async inspectAuth() {
|
|
106
|
-
return await this.requestJson(
|
|
178
|
+
return await this.requestJson(granolaTransportPaths.authStatus);
|
|
107
179
|
}
|
|
108
180
|
async loginAuth(options = {}) {
|
|
109
|
-
return await this.requestJson(
|
|
181
|
+
return await this.requestJson(granolaTransportPaths.authLogin, {
|
|
110
182
|
body: JSON.stringify(options),
|
|
111
183
|
headers: { "content-type": "application/json" },
|
|
112
184
|
method: "POST"
|
|
113
185
|
});
|
|
114
186
|
}
|
|
115
187
|
async logoutAuth() {
|
|
116
|
-
return await this.requestJson(
|
|
188
|
+
return await this.requestJson(granolaTransportPaths.authLogout, { method: "POST" });
|
|
117
189
|
}
|
|
118
190
|
async refreshAuth() {
|
|
119
|
-
return await this.requestJson(
|
|
191
|
+
return await this.requestJson(granolaTransportPaths.authRefresh, { method: "POST" });
|
|
120
192
|
}
|
|
121
193
|
async switchAuthMode(mode) {
|
|
122
|
-
return await this.requestJson(
|
|
194
|
+
return await this.requestJson(granolaTransportPaths.authMode, {
|
|
123
195
|
body: JSON.stringify({ mode }),
|
|
124
196
|
headers: { "content-type": "application/json" },
|
|
125
197
|
method: "POST"
|
|
126
198
|
});
|
|
127
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
|
+
}
|
|
128
209
|
async listMeetings(options = {}) {
|
|
129
|
-
return await this.requestJson(
|
|
130
|
-
limit: options.limit,
|
|
131
|
-
refresh: options.forceRefresh ? "true" : void 0,
|
|
132
|
-
search: options.search,
|
|
133
|
-
sort: options.sort,
|
|
134
|
-
updatedFrom: options.updatedFrom,
|
|
135
|
-
updatedTo: options.updatedTo
|
|
136
|
-
}));
|
|
210
|
+
return await this.requestJson(granolaMeetingsPath(options));
|
|
137
211
|
}
|
|
138
212
|
async getMeeting(id, options = {}) {
|
|
139
|
-
return await this.requestJson(
|
|
213
|
+
return await this.requestJson(`${granolaMeetingPath(id)}${options.requireCache ? "?includeTranscript=true" : ""}`);
|
|
140
214
|
}
|
|
141
215
|
async findMeeting(query, options = {}) {
|
|
142
|
-
return await this.requestJson(
|
|
143
|
-
includeTranscript: options.requireCache ? "true" : void 0,
|
|
144
|
-
q: query
|
|
145
|
-
}));
|
|
216
|
+
return await this.requestJson(granolaMeetingResolvePath(query, { includeTranscript: options.requireCache }));
|
|
146
217
|
}
|
|
147
218
|
async listExportJobs(options = {}) {
|
|
148
|
-
return await this.requestJson(
|
|
219
|
+
return await this.requestJson(granolaExportJobsPath(options));
|
|
149
220
|
}
|
|
150
221
|
async exportNotes(format = "markdown") {
|
|
151
|
-
return await this.requestJson(
|
|
222
|
+
return await this.requestJson(granolaTransportPaths.exportNotes, {
|
|
152
223
|
body: JSON.stringify({ format }),
|
|
153
224
|
headers: { "content-type": "application/json" },
|
|
154
225
|
method: "POST"
|
|
155
226
|
});
|
|
156
227
|
}
|
|
157
228
|
async exportTranscripts(format = "text") {
|
|
158
|
-
return await this.requestJson(
|
|
229
|
+
return await this.requestJson(granolaTransportPaths.exportTranscripts, {
|
|
159
230
|
body: JSON.stringify({ format }),
|
|
160
231
|
headers: { "content-type": "application/json" },
|
|
161
232
|
method: "POST"
|
|
162
233
|
});
|
|
163
234
|
}
|
|
164
235
|
async rerunExportJob(id) {
|
|
165
|
-
return await this.requestJson(
|
|
236
|
+
return await this.requestJson(granolaExportJobRerunPath(id), { method: "POST" });
|
|
166
237
|
}
|
|
167
238
|
async request(path, init = {}) {
|
|
168
239
|
const response = await this.#fetchImpl(new URL(path, this.url), {
|
|
@@ -192,7 +263,7 @@ var GranolaServerClient = class GranolaServerClient {
|
|
|
192
263
|
const controller = new AbortController();
|
|
193
264
|
this.#streamAbortController = controller;
|
|
194
265
|
try {
|
|
195
|
-
const response = await this.request(
|
|
266
|
+
const response = await this.request(granolaTransportPaths.events, {
|
|
196
267
|
headers: { accept: "text/event-stream" },
|
|
197
268
|
signal: controller.signal
|
|
198
269
|
});
|
|
@@ -1029,6 +1100,10 @@ function matchesMeetingSearch(document, search) {
|
|
|
1029
1100
|
...document.tags
|
|
1030
1101
|
].some((value) => value.toLowerCase().includes(query));
|
|
1031
1102
|
}
|
|
1103
|
+
function matchesMeetingFolders(documentId, folderId, foldersByDocumentId) {
|
|
1104
|
+
if (!folderId) return true;
|
|
1105
|
+
return (foldersByDocumentId?.get(documentId) ?? []).some((folder) => folder.id === folderId);
|
|
1106
|
+
}
|
|
1032
1107
|
function matchesMeetingSummarySearch(meeting, search) {
|
|
1033
1108
|
const query = search.trim().toLowerCase();
|
|
1034
1109
|
if (!query) return true;
|
|
@@ -1038,6 +1113,9 @@ function matchesMeetingSummarySearch(meeting, search) {
|
|
|
1038
1113
|
...meeting.tags
|
|
1039
1114
|
].some((value) => value.toLowerCase().includes(query));
|
|
1040
1115
|
}
|
|
1116
|
+
function meetingFolders(meeting) {
|
|
1117
|
+
return Array.isArray(meeting.folders) ? meeting.folders : [];
|
|
1118
|
+
}
|
|
1041
1119
|
function parseDateFilter(value, label) {
|
|
1042
1120
|
const trimmed = value?.trim();
|
|
1043
1121
|
if (!trimmed) return;
|
|
@@ -1064,7 +1142,7 @@ function matchesMeetingSummaryUpdatedRange(meeting, updatedFrom, updatedTo) {
|
|
|
1064
1142
|
if (to != null && updatedAt > to) return false;
|
|
1065
1143
|
return true;
|
|
1066
1144
|
}
|
|
1067
|
-
function truncate(value, width) {
|
|
1145
|
+
function truncate$1(value, width) {
|
|
1068
1146
|
if (value.length <= width) return value.padEnd(width);
|
|
1069
1147
|
return `${value.slice(0, Math.max(0, width - 1))}…`;
|
|
1070
1148
|
}
|
|
@@ -1080,11 +1158,12 @@ function formatTranscriptLines(transcript) {
|
|
|
1080
1158
|
if (!transcript || transcript.segments.length === 0) return "";
|
|
1081
1159
|
return transcript.segments.map((segment) => `[${formatTimestampForTranscript(segment.startTimestamp)}] ${segment.speaker}: ${segment.text}`).join("\n");
|
|
1082
1160
|
}
|
|
1083
|
-
function buildMeetingSummary(document, cacheData) {
|
|
1161
|
+
function buildMeetingSummary(document, cacheData, folders = []) {
|
|
1084
1162
|
const note = buildNoteExport(document);
|
|
1085
1163
|
const transcript = buildMeetingTranscript(document, cacheData);
|
|
1086
1164
|
return {
|
|
1087
1165
|
createdAt: document.createdAt,
|
|
1166
|
+
folders: folders.map((folder) => ({ ...folder })),
|
|
1088
1167
|
id: document.id,
|
|
1089
1168
|
noteContentSource: note.contentSource,
|
|
1090
1169
|
tags: [...document.tags],
|
|
@@ -1094,12 +1173,13 @@ function buildMeetingSummary(document, cacheData) {
|
|
|
1094
1173
|
updatedAt: latestDocumentTimestamp(document)
|
|
1095
1174
|
};
|
|
1096
1175
|
}
|
|
1097
|
-
function buildMeetingRecord(document, cacheData) {
|
|
1176
|
+
function buildMeetingRecord(document, cacheData, folders = []) {
|
|
1098
1177
|
const note = buildNoteExport(document);
|
|
1099
1178
|
const transcript = buildMeetingTranscript(document, cacheData);
|
|
1100
1179
|
return {
|
|
1101
1180
|
meeting: {
|
|
1102
1181
|
createdAt: document.createdAt,
|
|
1182
|
+
folders: folders.map((folder) => ({ ...folder })),
|
|
1103
1183
|
id: document.id,
|
|
1104
1184
|
noteContentSource: note.contentSource,
|
|
1105
1185
|
tags: [...document.tags],
|
|
@@ -1117,13 +1197,14 @@ function buildMeetingRecord(document, cacheData) {
|
|
|
1117
1197
|
function listMeetings(documents, options = {}) {
|
|
1118
1198
|
const limit = options.limit ?? 20;
|
|
1119
1199
|
const sort = options.sort ?? "updated-desc";
|
|
1120
|
-
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)));
|
|
1121
1201
|
}
|
|
1122
1202
|
function filterMeetingSummaries(meetings, options = {}) {
|
|
1123
1203
|
const limit = options.limit ?? 20;
|
|
1124
1204
|
const sort = options.sort ?? "updated-desc";
|
|
1125
|
-
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) => ({
|
|
1126
1206
|
...meeting,
|
|
1207
|
+
folders: meetingFolders(meeting).map((folder) => ({ ...folder })),
|
|
1127
1208
|
tags: [...meeting.tags]
|
|
1128
1209
|
}));
|
|
1129
1210
|
}
|
|
@@ -1160,14 +1241,18 @@ function renderMeetingList(meetings, format = "text") {
|
|
|
1160
1241
|
case "text": break;
|
|
1161
1242
|
}
|
|
1162
1243
|
if (meetings.length === 0) return "No meetings found\n";
|
|
1163
|
-
const lines = [`${"ID".padEnd(10)} ${"DATE".padEnd(10)} ${"TITLE".padEnd(
|
|
1164
|
-
for (const meeting of meetings)
|
|
1165
|
-
meeting.
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
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
|
+
}
|
|
1171
1256
|
return `${lines.join("\n").trimEnd()}\n`;
|
|
1172
1257
|
}
|
|
1173
1258
|
function renderMeetingView(record, format = "text") {
|
|
@@ -1177,6 +1262,7 @@ function renderMeetingView(record, format = "text") {
|
|
|
1177
1262
|
case "text": break;
|
|
1178
1263
|
}
|
|
1179
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)";
|
|
1180
1266
|
const transcriptStatus = !record.meeting.transcriptLoaded ? "cache not loaded" : record.meeting.transcriptSegmentCount === 0 ? "no transcript segments" : `${record.meeting.transcriptSegmentCount} segment(s)`;
|
|
1181
1267
|
return `${[
|
|
1182
1268
|
`# ${record.meeting.title || record.meeting.id}`,
|
|
@@ -1185,6 +1271,7 @@ function renderMeetingView(record, format = "text") {
|
|
|
1185
1271
|
`Created: ${record.meeting.createdAt || "-"}`,
|
|
1186
1272
|
`Updated: ${record.meeting.updatedAt || "-"}`,
|
|
1187
1273
|
`Tags: ${tags}`,
|
|
1274
|
+
`Folders: ${folders}`,
|
|
1188
1275
|
`Note source: ${record.meeting.noteContentSource}`,
|
|
1189
1276
|
`Transcript: ${transcriptStatus}`,
|
|
1190
1277
|
"",
|
|
@@ -2131,6 +2218,22 @@ function parseCacheContents(contents) {
|
|
|
2131
2218
|
};
|
|
2132
2219
|
}
|
|
2133
2220
|
//#endregion
|
|
2221
|
+
//#region src/persistence/layout.ts
|
|
2222
|
+
function defaultGranolaToolkitDataDirectory(targetPlatform = platform(), homeDirectory = homedir()) {
|
|
2223
|
+
return targetPlatform === "darwin" ? join(homeDirectory, "Library", "Application Support", "granola-toolkit") : join(homeDirectory, ".config", "granola-toolkit");
|
|
2224
|
+
}
|
|
2225
|
+
function defaultGranolaToolkitPersistenceLayout(options = {}) {
|
|
2226
|
+
const targetPlatform = options.platform ?? platform();
|
|
2227
|
+
const dataDirectory = defaultGranolaToolkitDataDirectory(targetPlatform, options.homeDirectory ?? homedir());
|
|
2228
|
+
return {
|
|
2229
|
+
dataDirectory,
|
|
2230
|
+
exportJobsFile: join(dataDirectory, "export-jobs.json"),
|
|
2231
|
+
meetingIndexFile: join(dataDirectory, "meeting-index.json"),
|
|
2232
|
+
sessionFile: join(dataDirectory, "session.json"),
|
|
2233
|
+
sessionStoreKind: targetPlatform === "darwin" ? "keychain" : "file"
|
|
2234
|
+
};
|
|
2235
|
+
}
|
|
2236
|
+
//#endregion
|
|
2134
2237
|
//#region src/client/auth.ts
|
|
2135
2238
|
const execFileAsync$1 = promisify(execFile);
|
|
2136
2239
|
const DEFAULT_CLIENT_ID = "client_GranolaMac";
|
|
@@ -2351,11 +2454,10 @@ async function refreshGranolaSession(session, fetchImpl = fetch) {
|
|
|
2351
2454
|
};
|
|
2352
2455
|
}
|
|
2353
2456
|
function defaultSessionFilePath() {
|
|
2354
|
-
|
|
2355
|
-
return platform() === "darwin" ? join(home, "Library", "Application Support", "granola-toolkit", "session.json") : join(home, ".config", "granola-toolkit", "session.json");
|
|
2457
|
+
return defaultGranolaToolkitPersistenceLayout().sessionFile;
|
|
2356
2458
|
}
|
|
2357
2459
|
function createDefaultSessionStore() {
|
|
2358
|
-
return
|
|
2460
|
+
return defaultGranolaToolkitPersistenceLayout().sessionStoreKind === "keychain" ? new KeychainSessionStore() : new FileSessionStore();
|
|
2359
2461
|
}
|
|
2360
2462
|
//#endregion
|
|
2361
2463
|
//#region src/client/default-auth.ts
|
|
@@ -2530,10 +2632,39 @@ function parseDocument(value) {
|
|
|
2530
2632
|
updatedAt: stringValue(record.updated_at)
|
|
2531
2633
|
};
|
|
2532
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
|
+
}
|
|
2533
2663
|
//#endregion
|
|
2534
2664
|
//#region src/client/granola.ts
|
|
2535
2665
|
const DEFAULT_CLIENT_VERSION = "5.354.0";
|
|
2536
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"];
|
|
2537
2668
|
function resolveClientVersion(value) {
|
|
2538
2669
|
return value?.trim() || process.env.GRANOLA_CLIENT_VERSION?.trim() || DEFAULT_CLIENT_VERSION;
|
|
2539
2670
|
}
|
|
@@ -2579,6 +2710,29 @@ var GranolaApiClient = class {
|
|
|
2579
2710
|
}
|
|
2580
2711
|
return documents;
|
|
2581
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
|
+
}
|
|
2582
2736
|
};
|
|
2583
2737
|
//#endregion
|
|
2584
2738
|
//#region src/client/http.ts
|
|
@@ -2736,8 +2890,7 @@ function createExportJobId(kind) {
|
|
|
2736
2890
|
return `${kind}-${randomUUID()}`;
|
|
2737
2891
|
}
|
|
2738
2892
|
function defaultExportJobsFilePath() {
|
|
2739
|
-
|
|
2740
|
-
return platform() === "darwin" ? join(home, "Library", "Application Support", "granola-toolkit", "export-jobs.json") : join(home, ".config", "granola-toolkit", "export-jobs.json");
|
|
2893
|
+
return defaultGranolaToolkitPersistenceLayout().exportJobsFile;
|
|
2741
2894
|
}
|
|
2742
2895
|
var FileExportJobStore = class {
|
|
2743
2896
|
constructor(filePath = defaultExportJobsFilePath()) {
|
|
@@ -2766,8 +2919,121 @@ function createDefaultExportJobStore() {
|
|
|
2766
2919
|
return new FileExportJobStore();
|
|
2767
2920
|
}
|
|
2768
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
|
|
2769
3035
|
//#region src/meeting-index.ts
|
|
2770
|
-
const MEETING_INDEX_VERSION =
|
|
3036
|
+
const MEETING_INDEX_VERSION = 2;
|
|
2771
3037
|
var FileMeetingIndexStore = class {
|
|
2772
3038
|
constructor(filePath = defaultMeetingIndexFilePath()) {
|
|
2773
3039
|
this.filePath = filePath;
|
|
@@ -2778,6 +3044,7 @@ var FileMeetingIndexStore = class {
|
|
|
2778
3044
|
if (!parsed || parsed.version !== MEETING_INDEX_VERSION || !Array.isArray(parsed.meetings)) return [];
|
|
2779
3045
|
return parsed.meetings.map((meeting) => ({
|
|
2780
3046
|
...meeting,
|
|
3047
|
+
folders: Array.isArray(meeting.folders) ? meeting.folders.map((folder) => ({ ...folder })) : [],
|
|
2781
3048
|
tags: [...meeting.tags]
|
|
2782
3049
|
}));
|
|
2783
3050
|
} catch {
|
|
@@ -2789,6 +3056,7 @@ var FileMeetingIndexStore = class {
|
|
|
2789
3056
|
const payload = {
|
|
2790
3057
|
meetings: meetings.map((meeting) => ({
|
|
2791
3058
|
...meeting,
|
|
3059
|
+
folders: Array.isArray(meeting.folders) ? meeting.folders.map((folder) => ({ ...folder })) : [],
|
|
2792
3060
|
tags: [...meeting.tags]
|
|
2793
3061
|
})),
|
|
2794
3062
|
updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
@@ -2801,8 +3069,7 @@ var FileMeetingIndexStore = class {
|
|
|
2801
3069
|
}
|
|
2802
3070
|
};
|
|
2803
3071
|
function defaultMeetingIndexFilePath() {
|
|
2804
|
-
|
|
2805
|
-
return platform() === "darwin" ? join(home, "Library", "Application Support", "granola-toolkit", "meeting-index.json") : join(home, ".config", "granola-toolkit", "meeting-index.json");
|
|
3072
|
+
return defaultGranolaToolkitPersistenceLayout().meetingIndexFile;
|
|
2806
3073
|
}
|
|
2807
3074
|
function createDefaultMeetingIndexStore() {
|
|
2808
3075
|
return new FileMeetingIndexStore();
|
|
@@ -2818,6 +3085,16 @@ function cloneExportState(state) {
|
|
|
2818
3085
|
function cloneExportJob(job) {
|
|
2819
3086
|
return { ...job };
|
|
2820
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
|
+
}
|
|
2821
3098
|
function cloneState(state) {
|
|
2822
3099
|
return {
|
|
2823
3100
|
auth: { ...state.auth },
|
|
@@ -2828,6 +3105,7 @@ function cloneState(state) {
|
|
|
2828
3105
|
transcripts: { ...state.config.transcripts }
|
|
2829
3106
|
},
|
|
2830
3107
|
documents: { ...state.documents },
|
|
3108
|
+
folders: { ...state.folders },
|
|
2831
3109
|
exports: {
|
|
2832
3110
|
jobs: state.exports.jobs.map((job) => cloneExportJob(job)),
|
|
2833
3111
|
notes: cloneExportState(state.exports.notes),
|
|
@@ -2856,6 +3134,10 @@ function defaultState(config, auth, surface) {
|
|
|
2856
3134
|
count: 0,
|
|
2857
3135
|
loaded: false
|
|
2858
3136
|
},
|
|
3137
|
+
folders: {
|
|
3138
|
+
count: 0,
|
|
3139
|
+
loaded: false
|
|
3140
|
+
},
|
|
2859
3141
|
exports: { jobs: [] },
|
|
2860
3142
|
index: {
|
|
2861
3143
|
available: false,
|
|
@@ -2872,6 +3154,7 @@ function defaultState(config, auth, surface) {
|
|
|
2872
3154
|
var GranolaApp = class {
|
|
2873
3155
|
#cacheData;
|
|
2874
3156
|
#cacheResolved = false;
|
|
3157
|
+
#folders;
|
|
2875
3158
|
#granolaClient;
|
|
2876
3159
|
#documents;
|
|
2877
3160
|
#meetingIndex;
|
|
@@ -2883,10 +3166,7 @@ var GranolaApp = class {
|
|
|
2883
3166
|
this.deps = deps;
|
|
2884
3167
|
this.#state = defaultState(config, deps.auth, options.surface ?? "cli");
|
|
2885
3168
|
this.#state.exports.jobs = (deps.exportJobs ?? []).map((job) => cloneExportJob(job));
|
|
2886
|
-
this.#meetingIndex = (deps.meetingIndex ?? []).map((meeting) => (
|
|
2887
|
-
...meeting,
|
|
2888
|
-
tags: [...meeting.tags]
|
|
2889
|
-
}));
|
|
3169
|
+
this.#meetingIndex = (deps.meetingIndex ?? []).map((meeting) => cloneMeetingSummary(meeting));
|
|
2890
3170
|
this.#state.index = {
|
|
2891
3171
|
available: this.#meetingIndex.length > 0,
|
|
2892
3172
|
filePath: defaultMeetingIndexFilePath(),
|
|
@@ -2912,16 +3192,21 @@ var GranolaApp = class {
|
|
|
2912
3192
|
this.emitStateUpdate();
|
|
2913
3193
|
return this.getState();
|
|
2914
3194
|
}
|
|
2915
|
-
|
|
3195
|
+
resetRemoteState() {
|
|
2916
3196
|
this.#granolaClient = void 0;
|
|
3197
|
+
this.#folders = void 0;
|
|
2917
3198
|
this.#documents = void 0;
|
|
2918
3199
|
this.#state.documents = {
|
|
2919
3200
|
count: 0,
|
|
2920
3201
|
loaded: false
|
|
2921
3202
|
};
|
|
3203
|
+
this.#state.folders = {
|
|
3204
|
+
count: 0,
|
|
3205
|
+
loaded: false
|
|
3206
|
+
};
|
|
2922
3207
|
}
|
|
2923
3208
|
applyAuthState(auth, options = {}) {
|
|
2924
|
-
if (options.resetDocuments) this.
|
|
3209
|
+
if (options.resetDocuments) this.resetRemoteState();
|
|
2925
3210
|
this.#state.auth = { ...auth };
|
|
2926
3211
|
if (options.view) this.#state.ui = {
|
|
2927
3212
|
...this.#state.ui,
|
|
@@ -2931,10 +3216,7 @@ var GranolaApp = class {
|
|
|
2931
3216
|
return { ...auth };
|
|
2932
3217
|
}
|
|
2933
3218
|
async persistMeetingIndex(meetings) {
|
|
2934
|
-
this.#meetingIndex = meetings.map((meeting) => (
|
|
2935
|
-
...meeting,
|
|
2936
|
-
tags: [...meeting.tags]
|
|
2937
|
-
}));
|
|
3219
|
+
this.#meetingIndex = meetings.map((meeting) => cloneMeetingSummary(meeting));
|
|
2938
3220
|
this.#state.index = {
|
|
2939
3221
|
available: this.#meetingIndex.length > 0,
|
|
2940
3222
|
filePath: this.#state.index.filePath,
|
|
@@ -2948,8 +3230,10 @@ var GranolaApp = class {
|
|
|
2948
3230
|
async refreshMeetingIndexFromLiveData() {
|
|
2949
3231
|
const cacheData = await this.loadCache();
|
|
2950
3232
|
const documents = await this.listDocuments();
|
|
3233
|
+
const folders = await this.loadFolders();
|
|
2951
3234
|
const meetings = listMeetings(documents, {
|
|
2952
3235
|
cacheData,
|
|
3236
|
+
foldersByDocumentId: this.buildFoldersByDocumentId(folders),
|
|
2953
3237
|
limit: documents.length || 1,
|
|
2954
3238
|
sort: "updated-desc"
|
|
2955
3239
|
});
|
|
@@ -2988,6 +3272,54 @@ var GranolaApp = class {
|
|
|
2988
3272
|
this.applyAuthState(runtime.auth);
|
|
2989
3273
|
return this.#granolaClient;
|
|
2990
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
|
+
}
|
|
2991
3323
|
missingCacheError() {
|
|
2992
3324
|
return /* @__PURE__ */ new Error(`Granola cache file not found. Pass --cache or create .granola.toml. Expected locations include: ${granolaCacheCandidates().join(", ")}`);
|
|
2993
3325
|
}
|
|
@@ -3100,12 +3432,7 @@ var GranolaApp = class {
|
|
|
3100
3432
|
}
|
|
3101
3433
|
async listDocuments(options = {}) {
|
|
3102
3434
|
if (options.forceRefresh) {
|
|
3103
|
-
this
|
|
3104
|
-
this.#documents = void 0;
|
|
3105
|
-
this.#state.documents = {
|
|
3106
|
-
count: 0,
|
|
3107
|
-
loaded: false
|
|
3108
|
-
};
|
|
3435
|
+
this.resetRemoteState();
|
|
3109
3436
|
this.emitStateUpdate();
|
|
3110
3437
|
}
|
|
3111
3438
|
if (this.#documents) return this.#documents;
|
|
@@ -3146,16 +3473,57 @@ var GranolaApp = class {
|
|
|
3146
3473
|
if (options.required && !cacheData) throw this.missingCacheError();
|
|
3147
3474
|
return cacheData;
|
|
3148
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
|
+
}
|
|
3149
3515
|
async listMeetings(options = {}) {
|
|
3150
3516
|
const preferIndex = options.preferIndex ?? (this.#state.ui.surface === "web" || this.#state.ui.surface === "server");
|
|
3151
3517
|
if (!options.forceRefresh && preferIndex && !this.#documents && this.#meetingIndex.length > 0) {
|
|
3152
3518
|
const meetings = filterMeetingSummaries(this.#meetingIndex, options);
|
|
3153
3519
|
this.setUiState({
|
|
3520
|
+
folderSearch: void 0,
|
|
3154
3521
|
meetingListSource: "index",
|
|
3155
3522
|
meetingSearch: options.search,
|
|
3156
3523
|
meetingSort: options.sort,
|
|
3157
3524
|
meetingUpdatedFrom: options.updatedFrom,
|
|
3158
3525
|
meetingUpdatedTo: options.updatedTo,
|
|
3526
|
+
selectedFolderId: options.folderId,
|
|
3159
3527
|
selectedMeetingId: void 0,
|
|
3160
3528
|
view: "meeting-list"
|
|
3161
3529
|
});
|
|
@@ -3167,8 +3535,14 @@ var GranolaApp = class {
|
|
|
3167
3535
|
}
|
|
3168
3536
|
const cacheData = await this.loadCache();
|
|
3169
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
|
+
});
|
|
3170
3542
|
const meetings = listMeetings(documents, {
|
|
3171
3543
|
cacheData,
|
|
3544
|
+
folderId: options.folderId,
|
|
3545
|
+
foldersByDocumentId: this.buildFoldersByDocumentId(folders),
|
|
3172
3546
|
limit: options.limit,
|
|
3173
3547
|
search: options.search,
|
|
3174
3548
|
sort: options.sort,
|
|
@@ -3177,15 +3551,18 @@ var GranolaApp = class {
|
|
|
3177
3551
|
});
|
|
3178
3552
|
await this.persistMeetingIndex(listMeetings(documents, {
|
|
3179
3553
|
cacheData,
|
|
3554
|
+
foldersByDocumentId: this.buildFoldersByDocumentId(folders),
|
|
3180
3555
|
limit: Math.max(documents.length, 1),
|
|
3181
3556
|
sort: "updated-desc"
|
|
3182
3557
|
}));
|
|
3183
3558
|
this.setUiState({
|
|
3559
|
+
folderSearch: void 0,
|
|
3184
3560
|
meetingListSource: "live",
|
|
3185
3561
|
meetingSearch: options.search,
|
|
3186
3562
|
meetingSort: options.sort,
|
|
3187
3563
|
meetingUpdatedFrom: options.updatedFrom,
|
|
3188
3564
|
meetingUpdatedTo: options.updatedTo,
|
|
3565
|
+
selectedFolderId: options.folderId,
|
|
3189
3566
|
selectedMeetingId: void 0,
|
|
3190
3567
|
view: "meeting-list"
|
|
3191
3568
|
});
|
|
@@ -3197,9 +3574,11 @@ var GranolaApp = class {
|
|
|
3197
3574
|
async getMeeting(id, options = {}) {
|
|
3198
3575
|
const documents = await this.listDocuments();
|
|
3199
3576
|
const cacheData = await this.loadCache({ required: options.requireCache });
|
|
3577
|
+
const folders = await this.loadFolders();
|
|
3200
3578
|
const document = resolveMeeting(documents, id);
|
|
3201
|
-
const meeting = buildMeetingRecord(document, cacheData);
|
|
3579
|
+
const meeting = buildMeetingRecord(document, cacheData, this.buildFoldersByDocumentId(folders)?.get(document.id));
|
|
3202
3580
|
this.setUiState({
|
|
3581
|
+
selectedFolderId: meeting.meeting.folders[0]?.id,
|
|
3203
3582
|
selectedMeetingId: document.id,
|
|
3204
3583
|
view: "meeting-detail"
|
|
3205
3584
|
});
|
|
@@ -3212,9 +3591,11 @@ var GranolaApp = class {
|
|
|
3212
3591
|
async findMeeting(query, options = {}) {
|
|
3213
3592
|
const documents = await this.listDocuments();
|
|
3214
3593
|
const cacheData = await this.loadCache({ required: options.requireCache });
|
|
3594
|
+
const folders = await this.loadFolders();
|
|
3215
3595
|
const document = resolveMeetingQuery(documents, query);
|
|
3216
|
-
const meeting = buildMeetingRecord(document, cacheData);
|
|
3596
|
+
const meeting = buildMeetingRecord(document, cacheData, this.buildFoldersByDocumentId(folders)?.get(document.id));
|
|
3217
3597
|
this.setUiState({
|
|
3598
|
+
selectedFolderId: meeting.meeting.folders[0]?.id,
|
|
3218
3599
|
selectedMeetingId: document.id,
|
|
3219
3600
|
view: "meeting-detail"
|
|
3220
3601
|
});
|
|
@@ -3611,7 +3992,7 @@ function resolveListFormat$1(value) {
|
|
|
3611
3992
|
default: throw new Error("invalid exports format: expected text, json, or yaml");
|
|
3612
3993
|
}
|
|
3613
3994
|
}
|
|
3614
|
-
function parseLimit$
|
|
3995
|
+
function parseLimit$2(value) {
|
|
3615
3996
|
if (value === void 0) return 20;
|
|
3616
3997
|
if (typeof value !== "string" || !/^\d+$/.test(value)) throw new Error("invalid exports limit: expected a positive integer");
|
|
3617
3998
|
const limit = Number(value);
|
|
@@ -3640,7 +4021,7 @@ const exportsCommand = {
|
|
|
3640
4021
|
async run({ commandArgs, commandFlags, globalFlags }) {
|
|
3641
4022
|
const [action, id] = commandArgs;
|
|
3642
4023
|
switch (action) {
|
|
3643
|
-
case "list": return await list$
|
|
4024
|
+
case "list": return await list$2(commandFlags, globalFlags);
|
|
3644
4025
|
case "rerun":
|
|
3645
4026
|
if (!id) throw new Error("exports rerun requires a job id");
|
|
3646
4027
|
return await rerun(id, commandFlags, globalFlags);
|
|
@@ -3651,9 +4032,9 @@ const exportsCommand = {
|
|
|
3651
4032
|
}
|
|
3652
4033
|
}
|
|
3653
4034
|
};
|
|
3654
|
-
async function list$
|
|
4035
|
+
async function list$2(commandFlags, globalFlags) {
|
|
3655
4036
|
const format = resolveListFormat$1(commandFlags.format);
|
|
3656
|
-
const limit = parseLimit$
|
|
4037
|
+
const limit = parseLimit$2(commandFlags.limit);
|
|
3657
4038
|
const config = await loadConfig({
|
|
3658
4039
|
globalFlags,
|
|
3659
4040
|
subcommandFlags: commandFlags
|
|
@@ -3680,6 +4061,110 @@ async function rerun(id, commandFlags, globalFlags) {
|
|
|
3680
4061
|
return 0;
|
|
3681
4062
|
}
|
|
3682
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
|
|
3683
4168
|
//#region src/browser.ts
|
|
3684
4169
|
const execFileAsync = promisify(execFile);
|
|
3685
4170
|
function getBrowserOpenCommand(url, platform = process.platform) {
|
|
@@ -5355,7 +5840,7 @@ function isPasswordAuthenticated(request, password) {
|
|
|
5355
5840
|
return parseCookies(request)[PASSWORD_COOKIE_NAME] === password;
|
|
5356
5841
|
}
|
|
5357
5842
|
function publicRoute(path, enableWebClient) {
|
|
5358
|
-
return path ===
|
|
5843
|
+
return path === granolaTransportPaths.health || path === granolaTransportPaths.serverInfo || path === granolaTransportPaths.authUnlock || enableWebClient && path === granolaTransportPaths.root;
|
|
5359
5844
|
}
|
|
5360
5845
|
async function startGranolaServer(app, options = {}) {
|
|
5361
5846
|
const enableWebClient = options.enableWebClient ?? false;
|
|
@@ -5365,6 +5850,25 @@ async function startGranolaServer(app, options = {}) {
|
|
|
5365
5850
|
password: options.security?.password?.trim() || void 0,
|
|
5366
5851
|
trustedOrigins: (options.security?.trustedOrigins ?? []).map((origin) => origin.trim()).filter(Boolean)
|
|
5367
5852
|
};
|
|
5853
|
+
const serverInfo = {
|
|
5854
|
+
capabilities: {
|
|
5855
|
+
attach: true,
|
|
5856
|
+
auth: true,
|
|
5857
|
+
events: true,
|
|
5858
|
+
exports: true,
|
|
5859
|
+
folders: true,
|
|
5860
|
+
meetingOpen: true,
|
|
5861
|
+
webClient: enableWebClient
|
|
5862
|
+
},
|
|
5863
|
+
persistence: {
|
|
5864
|
+
exportJobs: true,
|
|
5865
|
+
meetingIndex: true,
|
|
5866
|
+
sessionStore: defaultGranolaToolkitPersistenceLayout().sessionStoreKind
|
|
5867
|
+
},
|
|
5868
|
+
product: "granola-toolkit",
|
|
5869
|
+
protocolVersion: 2,
|
|
5870
|
+
transport: "local-http"
|
|
5871
|
+
};
|
|
5368
5872
|
const server = createServer(async (request, response) => {
|
|
5369
5873
|
const method = request.method ?? "GET";
|
|
5370
5874
|
const url = new URL(request.url ?? "/", `http://${hostname}`);
|
|
@@ -5392,11 +5896,11 @@ async function startGranolaServer(app, options = {}) {
|
|
|
5392
5896
|
sendNoContent(response, 204, originHeaders);
|
|
5393
5897
|
return;
|
|
5394
5898
|
}
|
|
5395
|
-
if (method === "GET" && path ===
|
|
5899
|
+
if (method === "GET" && path === granolaTransportPaths.root && enableWebClient) {
|
|
5396
5900
|
sendHtml(response, renderGranolaWebPage({ serverPasswordRequired: Boolean(security.password) }), 200, originHeaders);
|
|
5397
5901
|
return;
|
|
5398
5902
|
}
|
|
5399
|
-
if (method === "GET" && path ===
|
|
5903
|
+
if (method === "GET" && path === granolaTransportPaths.health) {
|
|
5400
5904
|
sendJson(response, {
|
|
5401
5905
|
ok: true,
|
|
5402
5906
|
service: "granola-toolkit",
|
|
@@ -5404,7 +5908,11 @@ async function startGranolaServer(app, options = {}) {
|
|
|
5404
5908
|
}, { headers: originHeaders });
|
|
5405
5909
|
return;
|
|
5406
5910
|
}
|
|
5407
|
-
if (method === "
|
|
5911
|
+
if (method === "GET" && path === granolaTransportPaths.serverInfo) {
|
|
5912
|
+
sendJson(response, serverInfo, { headers: originHeaders });
|
|
5913
|
+
return;
|
|
5914
|
+
}
|
|
5915
|
+
if (method === "POST" && path === granolaTransportPaths.authUnlock) {
|
|
5408
5916
|
if (!security.password) {
|
|
5409
5917
|
sendJson(response, {
|
|
5410
5918
|
ok: true,
|
|
@@ -5443,22 +5951,22 @@ async function startGranolaServer(app, options = {}) {
|
|
|
5443
5951
|
});
|
|
5444
5952
|
return;
|
|
5445
5953
|
}
|
|
5446
|
-
if (method === "GET" && path ===
|
|
5954
|
+
if (method === "GET" && path === granolaTransportPaths.state) {
|
|
5447
5955
|
sendJson(response, app.getState(), { headers: originHeaders });
|
|
5448
5956
|
return;
|
|
5449
5957
|
}
|
|
5450
|
-
if (method === "GET" && path ===
|
|
5958
|
+
if (method === "GET" && path === granolaTransportPaths.authStatus) {
|
|
5451
5959
|
sendJson(response, await app.inspectAuth(), { headers: originHeaders });
|
|
5452
5960
|
return;
|
|
5453
5961
|
}
|
|
5454
|
-
if (method === "POST" && path ===
|
|
5962
|
+
if (method === "POST" && path === granolaTransportPaths.authLock) {
|
|
5455
5963
|
sendJson(response, { ok: true }, { headers: {
|
|
5456
5964
|
...originHeaders,
|
|
5457
5965
|
"set-cookie": clearPasswordCookieHeader()
|
|
5458
5966
|
} });
|
|
5459
5967
|
return;
|
|
5460
5968
|
}
|
|
5461
|
-
if (method === "GET" && path ===
|
|
5969
|
+
if (method === "GET" && path === granolaTransportPaths.events) {
|
|
5462
5970
|
response.writeHead(200, {
|
|
5463
5971
|
"cache-control": "no-cache, no-transform",
|
|
5464
5972
|
connection: "keep-alive",
|
|
@@ -5479,7 +5987,8 @@ async function startGranolaServer(app, options = {}) {
|
|
|
5479
5987
|
});
|
|
5480
5988
|
return;
|
|
5481
5989
|
}
|
|
5482
|
-
if (method === "GET" && path ===
|
|
5990
|
+
if (method === "GET" && path === granolaTransportPaths.meetings) {
|
|
5991
|
+
const folderId = url.searchParams.get("folderId")?.trim() || void 0;
|
|
5483
5992
|
const limit = parseInteger(url.searchParams.get("limit"));
|
|
5484
5993
|
const refresh = url.searchParams.get("refresh") === "true";
|
|
5485
5994
|
const search = url.searchParams.get("search")?.trim() || void 0;
|
|
@@ -5487,6 +5996,7 @@ async function startGranolaServer(app, options = {}) {
|
|
|
5487
5996
|
const updatedFrom = url.searchParams.get("updatedFrom")?.trim() || void 0;
|
|
5488
5997
|
const updatedTo = url.searchParams.get("updatedTo")?.trim() || void 0;
|
|
5489
5998
|
const result = await app.listMeetings({
|
|
5999
|
+
folderId,
|
|
5490
6000
|
forceRefresh: refresh,
|
|
5491
6001
|
limit,
|
|
5492
6002
|
search,
|
|
@@ -5495,6 +6005,7 @@ async function startGranolaServer(app, options = {}) {
|
|
|
5495
6005
|
updatedTo
|
|
5496
6006
|
});
|
|
5497
6007
|
sendJson(response, {
|
|
6008
|
+
folderId,
|
|
5498
6009
|
meetings: result.meetings,
|
|
5499
6010
|
refresh,
|
|
5500
6011
|
search,
|
|
@@ -5505,38 +6016,65 @@ async function startGranolaServer(app, options = {}) {
|
|
|
5505
6016
|
}, { headers: originHeaders });
|
|
5506
6017
|
return;
|
|
5507
6018
|
}
|
|
5508
|
-
if (method === "GET" && path ===
|
|
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
|
+
}
|
|
6040
|
+
if (method === "GET" && path === granolaTransportPaths.meetingResolve) {
|
|
5509
6041
|
const query = url.searchParams.get("q")?.trim();
|
|
5510
6042
|
if (!query) throw new Error("meeting query is required");
|
|
5511
6043
|
sendJson(response, await app.findMeeting(query, { requireCache: url.searchParams.get("includeTranscript") === "true" }), { headers: originHeaders });
|
|
5512
6044
|
return;
|
|
5513
6045
|
}
|
|
5514
|
-
if (method === "GET" && path.startsWith(
|
|
5515
|
-
const id = decodeURIComponent(path.slice(
|
|
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
|
+
}
|
|
6052
|
+
if (method === "GET" && path.startsWith(`${granolaTransportPaths.meetings}/`) && path !== granolaTransportPaths.meetingResolve) {
|
|
6053
|
+
const id = decodeURIComponent(path.slice(`${granolaTransportPaths.meetings}/`.length));
|
|
5516
6054
|
if (!id) throw new Error("meeting id is required");
|
|
5517
6055
|
sendJson(response, await app.getMeeting(id, { requireCache: url.searchParams.get("includeTranscript") === "true" }), { headers: originHeaders });
|
|
5518
6056
|
return;
|
|
5519
6057
|
}
|
|
5520
|
-
if (method === "POST" && path ===
|
|
6058
|
+
if (method === "POST" && path === granolaTransportPaths.authLogin) {
|
|
5521
6059
|
const body = await readJsonBody(request);
|
|
5522
6060
|
const supabasePath = typeof body.supabasePath === "string" && body.supabasePath.trim() ? body.supabasePath.trim() : void 0;
|
|
5523
6061
|
sendJson(response, await app.loginAuth({ supabasePath }), { headers: originHeaders });
|
|
5524
6062
|
return;
|
|
5525
6063
|
}
|
|
5526
|
-
if (method === "POST" && path ===
|
|
6064
|
+
if (method === "POST" && path === granolaTransportPaths.authLogout) {
|
|
5527
6065
|
sendJson(response, await app.logoutAuth(), { headers: originHeaders });
|
|
5528
6066
|
return;
|
|
5529
6067
|
}
|
|
5530
|
-
if (method === "POST" && path ===
|
|
6068
|
+
if (method === "POST" && path === granolaTransportPaths.authRefresh) {
|
|
5531
6069
|
sendJson(response, await app.refreshAuth(), { headers: originHeaders });
|
|
5532
6070
|
return;
|
|
5533
6071
|
}
|
|
5534
|
-
if (method === "POST" && path ===
|
|
6072
|
+
if (method === "POST" && path === granolaTransportPaths.authMode) {
|
|
5535
6073
|
const body = await readJsonBody(request);
|
|
5536
6074
|
sendJson(response, await app.switchAuthMode(parseAuthMode(body.mode)), { headers: originHeaders });
|
|
5537
6075
|
return;
|
|
5538
6076
|
}
|
|
5539
|
-
if (method === "POST" && path ===
|
|
6077
|
+
if (method === "POST" && path === granolaTransportPaths.exportNotes) {
|
|
5540
6078
|
const body = await readJsonBody(request);
|
|
5541
6079
|
sendJson(response, await app.exportNotes(noteFormatFromBody(body.format)), {
|
|
5542
6080
|
headers: originHeaders,
|
|
@@ -5544,13 +6082,13 @@ async function startGranolaServer(app, options = {}) {
|
|
|
5544
6082
|
});
|
|
5545
6083
|
return;
|
|
5546
6084
|
}
|
|
5547
|
-
if (method === "GET" && path ===
|
|
6085
|
+
if (method === "GET" && path === granolaTransportPaths.exportJobs) {
|
|
5548
6086
|
const limit = parseInteger(url.searchParams.get("limit"));
|
|
5549
6087
|
sendJson(response, await app.listExportJobs({ limit }), { headers: originHeaders });
|
|
5550
6088
|
return;
|
|
5551
6089
|
}
|
|
5552
|
-
if (method === "POST" && path.startsWith(
|
|
5553
|
-
const id = decodeURIComponent(path.slice(
|
|
6090
|
+
if (method === "POST" && path.startsWith(`${granolaTransportPaths.exportJobs}/`) && path.endsWith("/rerun")) {
|
|
6091
|
+
const id = decodeURIComponent(path.slice(`${granolaTransportPaths.exportJobs}/`.length, -6));
|
|
5554
6092
|
if (!id) throw new Error("export job id is required");
|
|
5555
6093
|
sendJson(response, await app.rerunExportJob(id), {
|
|
5556
6094
|
headers: originHeaders,
|
|
@@ -5558,7 +6096,7 @@ async function startGranolaServer(app, options = {}) {
|
|
|
5558
6096
|
});
|
|
5559
6097
|
return;
|
|
5560
6098
|
}
|
|
5561
|
-
if (method === "POST" && path ===
|
|
6099
|
+
if (method === "POST" && path === granolaTransportPaths.exportTranscripts) {
|
|
5562
6100
|
const body = await readJsonBody(request);
|
|
5563
6101
|
sendJson(response, await app.exportTranscripts(transcriptFormatFromBody(body.format)), {
|
|
5564
6102
|
headers: originHeaders,
|
|
@@ -5630,6 +6168,7 @@ function printWebRoutes() {
|
|
|
5630
6168
|
console.log("Routes:");
|
|
5631
6169
|
console.log(" GET /");
|
|
5632
6170
|
console.log(" GET /health");
|
|
6171
|
+
console.log(" GET /server/info");
|
|
5633
6172
|
console.log(" POST /auth/unlock");
|
|
5634
6173
|
console.log(" POST /auth/lock");
|
|
5635
6174
|
console.log(" GET /auth/status");
|
|
@@ -5694,6 +6233,7 @@ Subcommands:
|
|
|
5694
6233
|
|
|
5695
6234
|
Options:
|
|
5696
6235
|
--cache <path> Path to Granola cache JSON for transcript data
|
|
6236
|
+
--folder <query> Filter list to one folder id or name
|
|
5697
6237
|
--format <value> list/view: text, json, yaml; export: json, yaml; notes: markdown, json, yaml, raw; transcript: text, json, yaml, raw
|
|
5698
6238
|
--network <mode> open: local or lan (default: local)
|
|
5699
6239
|
--hostname <value> open: hostname to bind (overrides network default)
|
|
@@ -5767,6 +6307,7 @@ const meetingCommand = {
|
|
|
5767
6307
|
description: "Inspect and export individual Granola meetings",
|
|
5768
6308
|
flags: {
|
|
5769
6309
|
cache: { type: "string" },
|
|
6310
|
+
folder: { type: "string" },
|
|
5770
6311
|
format: { type: "string" },
|
|
5771
6312
|
help: { type: "boolean" },
|
|
5772
6313
|
hostname: { type: "string" },
|
|
@@ -5810,6 +6351,7 @@ const meetingCommand = {
|
|
|
5810
6351
|
async function list(commandFlags, globalFlags) {
|
|
5811
6352
|
const format = resolveListFormat(commandFlags.format);
|
|
5812
6353
|
const limit = parseLimit(commandFlags.limit);
|
|
6354
|
+
const folderQuery = typeof commandFlags.folder === "string" ? commandFlags.folder : void 0;
|
|
5813
6355
|
const search = typeof commandFlags.search === "string" ? commandFlags.search : void 0;
|
|
5814
6356
|
const config = await loadConfig({
|
|
5815
6357
|
globalFlags,
|
|
@@ -5822,11 +6364,15 @@ async function list(commandFlags, globalFlags) {
|
|
|
5822
6364
|
const app = await createGranolaApp(config);
|
|
5823
6365
|
debug(config.debug, "authMode", app.getState().auth.mode);
|
|
5824
6366
|
console.log("Loading meetings...");
|
|
6367
|
+
const folder = folderQuery ? await app.findFolder(folderQuery) : void 0;
|
|
6368
|
+
const folderId = folder?.id;
|
|
5825
6369
|
const result = await app.listMeetings({
|
|
6370
|
+
folderId,
|
|
5826
6371
|
limit,
|
|
5827
6372
|
search
|
|
5828
6373
|
});
|
|
5829
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})`);
|
|
5830
6376
|
console.log(renderMeetingList(result.meetings, format).trimEnd());
|
|
5831
6377
|
return 0;
|
|
5832
6378
|
}
|
|
@@ -6042,6 +6588,7 @@ const serveCommand = {
|
|
|
6042
6588
|
if (trustedOrigins.length > 0) console.log(`Trusted origins: ${trustedOrigins.join(", ")}`);
|
|
6043
6589
|
console.log("Endpoints:");
|
|
6044
6590
|
console.log(" GET /health");
|
|
6591
|
+
console.log(" GET /server/info");
|
|
6045
6592
|
console.log(" POST /auth/unlock");
|
|
6046
6593
|
console.log(" POST /auth/lock");
|
|
6047
6594
|
console.log(" GET /auth/status");
|
|
@@ -6188,6 +6735,7 @@ const commands = [
|
|
|
6188
6735
|
attachCommand,
|
|
6189
6736
|
authCommand,
|
|
6190
6737
|
exportsCommand,
|
|
6738
|
+
folderCommand,
|
|
6191
6739
|
meetingCommand,
|
|
6192
6740
|
notesCommand,
|
|
6193
6741
|
serveCommand,
|
|
@@ -6316,6 +6864,7 @@ Global options:
|
|
|
6316
6864
|
|
|
6317
6865
|
Examples:
|
|
6318
6866
|
granola attach http://127.0.0.1:4123
|
|
6867
|
+
granola folder list
|
|
6319
6868
|
granola notes --supabase "${granolaSupabaseCandidates()[0] ?? "/path/to/supabase.json"}"
|
|
6320
6869
|
granola transcripts --cache "${granolaCacheCandidates()[0] ?? "/path/to/cache-v3.json"}"
|
|
6321
6870
|
`;
|