granola-toolkit 0.2.0 → 0.3.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/dist/cli.js +132 -47
- package/package.json +5 -2
package/dist/cli.js
CHANGED
|
@@ -171,10 +171,56 @@ function transcriptSpeakerLabel(segment) {
|
|
|
171
171
|
return segment.source === "microphone" ? "You" : "System";
|
|
172
172
|
}
|
|
173
173
|
//#endregion
|
|
174
|
-
//#region src/
|
|
175
|
-
|
|
176
|
-
const
|
|
177
|
-
|
|
174
|
+
//#region src/client/auth.ts
|
|
175
|
+
function getAccessTokenFromSupabaseContents(supabaseContents) {
|
|
176
|
+
const wrapper = parseJsonString(supabaseContents);
|
|
177
|
+
if (!wrapper) throw new Error("failed to parse supabase.json");
|
|
178
|
+
const workosTokens = wrapper.workos_tokens;
|
|
179
|
+
let tokenPayload;
|
|
180
|
+
if (typeof workosTokens === "string") tokenPayload = parseJsonString(workosTokens);
|
|
181
|
+
else tokenPayload = asRecord(workosTokens);
|
|
182
|
+
const accessToken = tokenPayload ? stringValue(tokenPayload.access_token) : "";
|
|
183
|
+
if (!accessToken.trim()) throw new Error("access token not found in supabase.json");
|
|
184
|
+
return accessToken;
|
|
185
|
+
}
|
|
186
|
+
var SupabaseFileTokenSource = class {
|
|
187
|
+
constructor(filePath) {
|
|
188
|
+
this.filePath = filePath;
|
|
189
|
+
}
|
|
190
|
+
async loadAccessToken() {
|
|
191
|
+
return getAccessTokenFromSupabaseContents(await readFile(this.filePath, "utf8"));
|
|
192
|
+
}
|
|
193
|
+
};
|
|
194
|
+
var NoopTokenStore = class {
|
|
195
|
+
async clearToken() {}
|
|
196
|
+
async readToken() {}
|
|
197
|
+
async writeToken(_token) {}
|
|
198
|
+
};
|
|
199
|
+
var CachedTokenProvider = class {
|
|
200
|
+
#token;
|
|
201
|
+
constructor(source, store = new NoopTokenStore()) {
|
|
202
|
+
this.source = source;
|
|
203
|
+
this.store = store;
|
|
204
|
+
}
|
|
205
|
+
async getAccessToken() {
|
|
206
|
+
if (this.#token) return this.#token;
|
|
207
|
+
const storedToken = await this.store.readToken();
|
|
208
|
+
if (storedToken?.trim()) {
|
|
209
|
+
this.#token = storedToken;
|
|
210
|
+
return storedToken;
|
|
211
|
+
}
|
|
212
|
+
const token = await this.source.loadAccessToken();
|
|
213
|
+
this.#token = token;
|
|
214
|
+
await this.store.writeToken(token);
|
|
215
|
+
return token;
|
|
216
|
+
}
|
|
217
|
+
async invalidate() {
|
|
218
|
+
this.#token = void 0;
|
|
219
|
+
await this.store.clearToken();
|
|
220
|
+
}
|
|
221
|
+
};
|
|
222
|
+
//#endregion
|
|
223
|
+
//#region src/client/parsers.ts
|
|
178
224
|
function parseProseMirrorDoc(value, options = {}) {
|
|
179
225
|
if (value == null) return;
|
|
180
226
|
if (typeof value === "string") {
|
|
@@ -209,17 +255,6 @@ function parseLastViewedPanel(value) {
|
|
|
209
255
|
updatedAt: stringValue(panel.updated_at)
|
|
210
256
|
};
|
|
211
257
|
}
|
|
212
|
-
function getAccessToken(supabaseContents) {
|
|
213
|
-
const wrapper = parseJsonString(supabaseContents);
|
|
214
|
-
if (!wrapper) throw new Error("failed to parse supabase.json");
|
|
215
|
-
const workosTokens = wrapper.workos_tokens;
|
|
216
|
-
let tokenPayload;
|
|
217
|
-
if (typeof workosTokens === "string") tokenPayload = parseJsonString(workosTokens);
|
|
218
|
-
else tokenPayload = asRecord(workosTokens);
|
|
219
|
-
const accessToken = tokenPayload ? stringValue(tokenPayload.access_token) : "";
|
|
220
|
-
if (!accessToken.trim()) throw new Error("access token not found in supabase.json");
|
|
221
|
-
return accessToken;
|
|
222
|
-
}
|
|
223
258
|
function parseDocument(value) {
|
|
224
259
|
const record = asRecord(value);
|
|
225
260
|
if (!record) throw new Error("document payload is not an object");
|
|
@@ -235,44 +270,93 @@ function parseDocument(value) {
|
|
|
235
270
|
updatedAt: stringValue(record.updated_at)
|
|
236
271
|
};
|
|
237
272
|
}
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
273
|
+
//#endregion
|
|
274
|
+
//#region src/client/granola.ts
|
|
275
|
+
const USER_AGENT = "Granola/5.354.0";
|
|
276
|
+
const CLIENT_VERSION = "5.354.0";
|
|
277
|
+
const DOCUMENTS_URL = "https://api.granola.ai/v2/get-documents";
|
|
278
|
+
var GranolaApiClient = class {
|
|
279
|
+
constructor(httpClient, documentsUrl = DOCUMENTS_URL) {
|
|
280
|
+
this.httpClient = httpClient;
|
|
281
|
+
this.documentsUrl = documentsUrl;
|
|
282
|
+
}
|
|
283
|
+
async listDocuments(options) {
|
|
284
|
+
const documents = [];
|
|
285
|
+
const limit = options.limit ?? 100;
|
|
286
|
+
let offset = 0;
|
|
287
|
+
for (;;) {
|
|
288
|
+
const response = await this.httpClient.postJson(this.documentsUrl, {
|
|
249
289
|
include_last_viewed_panel: true,
|
|
250
290
|
limit,
|
|
251
291
|
offset
|
|
252
|
-
}
|
|
292
|
+
}, {
|
|
293
|
+
headers: {
|
|
294
|
+
"User-Agent": USER_AGENT,
|
|
295
|
+
"X-Client-Version": CLIENT_VERSION
|
|
296
|
+
},
|
|
297
|
+
timeoutMs: options.timeoutMs
|
|
298
|
+
});
|
|
299
|
+
if (!response.ok) {
|
|
300
|
+
const body = (await response.text()).slice(0, 500);
|
|
301
|
+
throw new Error(`failed to get documents: ${response.status} ${response.statusText}${body ? `: ${body}` : ""}`);
|
|
302
|
+
}
|
|
303
|
+
const payload = await response.json();
|
|
304
|
+
if (!Array.isArray(payload.docs)) throw new Error("failed to parse documents response");
|
|
305
|
+
const page = payload.docs.map(parseDocument);
|
|
306
|
+
documents.push(...page);
|
|
307
|
+
if (page.length < limit) break;
|
|
308
|
+
offset += limit;
|
|
309
|
+
}
|
|
310
|
+
return documents;
|
|
311
|
+
}
|
|
312
|
+
};
|
|
313
|
+
//#endregion
|
|
314
|
+
//#region src/client/http.ts
|
|
315
|
+
var AuthenticatedHttpClient = class {
|
|
316
|
+
fetchImpl;
|
|
317
|
+
constructor(options) {
|
|
318
|
+
this.fetchImpl = options.fetchImpl ?? fetch;
|
|
319
|
+
this.logger = options.logger;
|
|
320
|
+
this.tokenProvider = options.tokenProvider;
|
|
321
|
+
}
|
|
322
|
+
logger;
|
|
323
|
+
tokenProvider;
|
|
324
|
+
async request(options) {
|
|
325
|
+
const { retryOnUnauthorized = true, timeoutMs, url } = options;
|
|
326
|
+
const accessToken = await this.tokenProvider.getAccessToken();
|
|
327
|
+
const response = await this.fetchImpl(url, {
|
|
328
|
+
body: options.body,
|
|
329
|
+
headers: {
|
|
330
|
+
...options.headers,
|
|
331
|
+
Authorization: `Bearer ${accessToken}`
|
|
332
|
+
},
|
|
333
|
+
method: options.method ?? "GET",
|
|
334
|
+
signal: AbortSignal.timeout(timeoutMs)
|
|
335
|
+
});
|
|
336
|
+
if (response.status === 401 && retryOnUnauthorized) {
|
|
337
|
+
this.logger?.warn?.("request returned 401; invalidating token provider and retrying once");
|
|
338
|
+
await this.tokenProvider.invalidate();
|
|
339
|
+
return this.request({
|
|
340
|
+
...options,
|
|
341
|
+
retryOnUnauthorized: false
|
|
342
|
+
});
|
|
343
|
+
}
|
|
344
|
+
return response;
|
|
345
|
+
}
|
|
346
|
+
async postJson(url, body, options = { timeoutMs: 3e4 }) {
|
|
347
|
+
return this.request({
|
|
348
|
+
...options,
|
|
349
|
+
body: JSON.stringify(body),
|
|
253
350
|
headers: {
|
|
254
351
|
Accept: "*/*",
|
|
255
|
-
Authorization: `Bearer ${accessToken}`,
|
|
256
352
|
"Content-Type": "application/json",
|
|
257
|
-
|
|
258
|
-
"X-Client-Version": CLIENT_VERSION
|
|
353
|
+
...options.headers
|
|
259
354
|
},
|
|
260
355
|
method: "POST",
|
|
261
|
-
|
|
356
|
+
url
|
|
262
357
|
});
|
|
263
|
-
if (!response.ok) {
|
|
264
|
-
const body = (await response.text()).slice(0, 500);
|
|
265
|
-
throw new Error(`failed to get documents: ${response.status} ${response.statusText}${body ? `: ${body}` : ""}`);
|
|
266
|
-
}
|
|
267
|
-
const payload = await response.json();
|
|
268
|
-
if (!Array.isArray(payload.docs)) throw new Error("failed to parse documents response");
|
|
269
|
-
const page = payload.docs.map(parseDocument);
|
|
270
|
-
documents.push(...page);
|
|
271
|
-
if (page.length < limit) break;
|
|
272
|
-
offset += limit;
|
|
273
358
|
}
|
|
274
|
-
|
|
275
|
-
}
|
|
359
|
+
};
|
|
276
360
|
//#endregion
|
|
277
361
|
//#region src/config.ts
|
|
278
362
|
function pickString(value) {
|
|
@@ -516,10 +600,11 @@ const notesCommand = {
|
|
|
516
600
|
debug(config.debug, "timeoutMs", config.notes.timeoutMs);
|
|
517
601
|
debug(config.debug, "output", config.notes.output);
|
|
518
602
|
console.log("Fetching documents from Granola API...");
|
|
519
|
-
const
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
603
|
+
const tokenProvider = new CachedTokenProvider(new SupabaseFileTokenSource(config.supabase), new NoopTokenStore());
|
|
604
|
+
const documents = await new GranolaApiClient(new AuthenticatedHttpClient({
|
|
605
|
+
logger: console,
|
|
606
|
+
tokenProvider
|
|
607
|
+
})).listDocuments({ timeoutMs: config.notes.timeoutMs });
|
|
523
608
|
console.log(`Exporting ${documents.length} notes to ${config.notes.output}...`);
|
|
524
609
|
const written = await writeNotes(documents, config.notes.output);
|
|
525
610
|
console.log("✓ Export completed successfully");
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "granola-toolkit",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"description": "CLI toolkit for exporting and working with Granola notes and transcripts",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"cli",
|
|
@@ -37,8 +37,11 @@
|
|
|
37
37
|
"fmt": "vp fmt",
|
|
38
38
|
"lint": "vp lint",
|
|
39
39
|
"pack:dry-run": "npm pack --dry-run",
|
|
40
|
-
"prepublishOnly": "
|
|
40
|
+
"prepublishOnly": "node scripts/prepublish.mjs",
|
|
41
41
|
"release": "node scripts/release.mjs",
|
|
42
|
+
"release:major": "node scripts/release.mjs major",
|
|
43
|
+
"release:minor": "node scripts/release.mjs minor",
|
|
44
|
+
"release:patch": "node scripts/release.mjs patch",
|
|
42
45
|
"start": "node dist/cli.js",
|
|
43
46
|
"notes": "node dist/cli.js notes",
|
|
44
47
|
"transcripts": "node dist/cli.js transcripts",
|