granola-toolkit 0.22.0 → 0.24.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 +34 -1
- package/dist/cli.js +838 -151
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { existsSync } from "node:fs";
|
|
3
|
-
import { execFile } from "node:child_process";
|
|
4
3
|
import { mkdir, readFile, rm, stat, unlink, writeFile } from "node:fs/promises";
|
|
5
4
|
import { homedir, platform } from "node:os";
|
|
6
5
|
import { dirname, join } from "node:path";
|
|
7
|
-
import { promisify } from "node:util";
|
|
8
6
|
import { NodeHtmlMarkdown } from "node-html-markdown";
|
|
7
|
+
import { execFile } from "node:child_process";
|
|
8
|
+
import { promisify } from "node:util";
|
|
9
9
|
import { createHash, randomUUID } from "node:crypto";
|
|
10
10
|
import { createServer } from "node:http";
|
|
11
11
|
//#region src/utils.ts
|
|
@@ -153,6 +153,60 @@ function transcriptSpeakerLabel(segment) {
|
|
|
153
153
|
return segment.source === "microphone" ? "You" : "System";
|
|
154
154
|
}
|
|
155
155
|
//#endregion
|
|
156
|
+
//#region src/cache.ts
|
|
157
|
+
function parseCacheDocument(id, value) {
|
|
158
|
+
const record = asRecord(value);
|
|
159
|
+
if (!record) return;
|
|
160
|
+
return {
|
|
161
|
+
createdAt: stringValue(record.created_at),
|
|
162
|
+
id,
|
|
163
|
+
title: stringValue(record.title),
|
|
164
|
+
updatedAt: stringValue(record.updated_at)
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
function parseTranscriptSegments(value) {
|
|
168
|
+
if (!Array.isArray(value)) return;
|
|
169
|
+
return value.flatMap((segment) => {
|
|
170
|
+
const record = asRecord(segment);
|
|
171
|
+
if (!record) return [];
|
|
172
|
+
return [{
|
|
173
|
+
documentId: stringValue(record.document_id),
|
|
174
|
+
endTimestamp: stringValue(record.end_timestamp),
|
|
175
|
+
id: stringValue(record.id),
|
|
176
|
+
isFinal: Boolean(record.is_final),
|
|
177
|
+
source: stringValue(record.source),
|
|
178
|
+
startTimestamp: stringValue(record.start_timestamp),
|
|
179
|
+
text: stringValue(record.text)
|
|
180
|
+
}];
|
|
181
|
+
});
|
|
182
|
+
}
|
|
183
|
+
function parseCacheContents(contents) {
|
|
184
|
+
const outer = parseJsonString(contents);
|
|
185
|
+
if (!outer) throw new Error("failed to parse cache JSON");
|
|
186
|
+
const rawCache = outer.cache;
|
|
187
|
+
let cachePayload;
|
|
188
|
+
if (typeof rawCache === "string") cachePayload = parseJsonString(rawCache);
|
|
189
|
+
else cachePayload = asRecord(rawCache);
|
|
190
|
+
const state = cachePayload ? asRecord(cachePayload.state) : void 0;
|
|
191
|
+
if (!state) throw new Error("failed to parse cache state");
|
|
192
|
+
const rawDocuments = asRecord(state.documents) ?? {};
|
|
193
|
+
const rawTranscripts = asRecord(state.transcripts) ?? {};
|
|
194
|
+
const documents = {};
|
|
195
|
+
for (const [id, rawDocument] of Object.entries(rawDocuments)) {
|
|
196
|
+
const document = parseCacheDocument(id, rawDocument);
|
|
197
|
+
if (document) documents[id] = document;
|
|
198
|
+
}
|
|
199
|
+
const transcripts = {};
|
|
200
|
+
for (const [id, rawTranscript] of Object.entries(rawTranscripts)) {
|
|
201
|
+
const segments = parseTranscriptSegments(rawTranscript);
|
|
202
|
+
if (segments) transcripts[id] = segments;
|
|
203
|
+
}
|
|
204
|
+
return {
|
|
205
|
+
documents,
|
|
206
|
+
transcripts
|
|
207
|
+
};
|
|
208
|
+
}
|
|
209
|
+
//#endregion
|
|
156
210
|
//#region src/client/auth.ts
|
|
157
211
|
const execFileAsync$1 = promisify(execFile);
|
|
158
212
|
const DEFAULT_CLIENT_ID = "client_GranolaMac";
|
|
@@ -380,121 +434,126 @@ function createDefaultSessionStore() {
|
|
|
380
434
|
return platform() === "darwin" ? new KeychainSessionStore() : new FileSessionStore();
|
|
381
435
|
}
|
|
382
436
|
//#endregion
|
|
383
|
-
//#region src/
|
|
384
|
-
function
|
|
385
|
-
return
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
const
|
|
401
|
-
|
|
402
|
-
flags: { help: { type: "boolean" } },
|
|
403
|
-
help: authHelp,
|
|
404
|
-
name: "auth",
|
|
405
|
-
async run({ commandArgs, globalFlags }) {
|
|
406
|
-
const [action] = commandArgs;
|
|
407
|
-
switch (action) {
|
|
408
|
-
case "login": return await login(globalFlags.supabase);
|
|
409
|
-
case "logout": return await logout();
|
|
410
|
-
case "status": return await status();
|
|
411
|
-
case void 0:
|
|
412
|
-
console.log(authHelp());
|
|
413
|
-
return 1;
|
|
414
|
-
default: throw new Error("invalid auth command: expected login, status, or logout");
|
|
415
|
-
}
|
|
416
|
-
}
|
|
417
|
-
};
|
|
418
|
-
async function login(supabaseFlag) {
|
|
419
|
-
const supabasePath = typeof supabaseFlag === "string" && supabaseFlag.trim() || firstExistingPath(granolaSupabaseCandidates());
|
|
420
|
-
if (!supabasePath) throw new Error(`supabase.json not found. Pass --supabase or create .granola.toml. Expected locations include: ${granolaSupabaseCandidates().join(", ")}`);
|
|
421
|
-
if (!existsSync(supabasePath)) throw new Error(`supabase.json not found: ${supabasePath}`);
|
|
422
|
-
const sessionStore = createDefaultSessionStore();
|
|
423
|
-
const session = await new SupabaseFileSessionSource(supabasePath).loadSession();
|
|
424
|
-
await sessionStore.writeSession(session);
|
|
425
|
-
console.log(`Imported Granola session from ${supabasePath}`);
|
|
426
|
-
return 0;
|
|
427
|
-
}
|
|
428
|
-
async function status() {
|
|
429
|
-
const session = await createDefaultSessionStore().readSession();
|
|
430
|
-
if (!session) {
|
|
431
|
-
console.log("No stored Granola session");
|
|
432
|
-
return 1;
|
|
433
|
-
}
|
|
434
|
-
console.log("Stored Granola session");
|
|
435
|
-
console.log(`Client ID: ${session.clientId}`);
|
|
436
|
-
console.log(`Refresh token: ${session.refreshToken ? "available" : "missing"}`);
|
|
437
|
-
console.log(`Sign-in method: ${session.signInMethod ?? "unknown"}`);
|
|
438
|
-
return 0;
|
|
439
|
-
}
|
|
440
|
-
async function logout() {
|
|
441
|
-
await createDefaultSessionStore().clearSession();
|
|
442
|
-
console.log("Stored Granola session deleted");
|
|
443
|
-
return 0;
|
|
444
|
-
}
|
|
445
|
-
//#endregion
|
|
446
|
-
//#region src/cache.ts
|
|
447
|
-
function parseCacheDocument(id, value) {
|
|
448
|
-
const record = asRecord(value);
|
|
449
|
-
if (!record) return;
|
|
437
|
+
//#region src/client/default-auth.ts
|
|
438
|
+
function hasStoredSession(session) {
|
|
439
|
+
return Boolean(session?.accessToken.trim());
|
|
440
|
+
}
|
|
441
|
+
function resolveActiveMode(options) {
|
|
442
|
+
if (options.preferredMode === "stored-session" && options.storedSessionAvailable) return "stored-session";
|
|
443
|
+
if (options.preferredMode === "supabase-file" && options.supabaseAvailable) return "supabase-file";
|
|
444
|
+
if (options.storedSessionAvailable) return "stored-session";
|
|
445
|
+
return "supabase-file";
|
|
446
|
+
}
|
|
447
|
+
function missingSupabaseError() {
|
|
448
|
+
return /* @__PURE__ */ new Error(`supabase.json not found. Pass --supabase or create .granola.toml. Expected locations include: ${granolaSupabaseCandidates().join(", ")}`);
|
|
449
|
+
}
|
|
450
|
+
function buildDefaultGranolaAuthInfo(config, options = {}) {
|
|
451
|
+
const existsSyncImpl = options.existsSyncImpl ?? existsSync;
|
|
452
|
+
const session = options.session;
|
|
453
|
+
const storedSessionAvailable = hasStoredSession(session);
|
|
454
|
+
const supabasePath = config.supabase || void 0;
|
|
455
|
+
const supabaseAvailable = Boolean(supabasePath && existsSyncImpl(supabasePath));
|
|
450
456
|
return {
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
457
|
+
clientId: session?.clientId,
|
|
458
|
+
lastError: options.lastError,
|
|
459
|
+
mode: resolveActiveMode({
|
|
460
|
+
preferredMode: options.preferredMode,
|
|
461
|
+
storedSessionAvailable,
|
|
462
|
+
supabaseAvailable
|
|
463
|
+
}),
|
|
464
|
+
refreshAvailable: Boolean(session?.refreshToken?.trim()),
|
|
465
|
+
signInMethod: session?.signInMethod,
|
|
466
|
+
storedSessionAvailable,
|
|
467
|
+
supabaseAvailable,
|
|
468
|
+
supabasePath
|
|
455
469
|
};
|
|
456
470
|
}
|
|
457
|
-
function
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
id: stringValue(record.id),
|
|
466
|
-
isFinal: Boolean(record.is_final),
|
|
467
|
-
source: stringValue(record.source),
|
|
468
|
-
startTimestamp: stringValue(record.start_timestamp),
|
|
469
|
-
text: stringValue(record.text)
|
|
470
|
-
}];
|
|
471
|
+
async function inspectDefaultGranolaAuth(config, options = {}) {
|
|
472
|
+
const sessionStore = options.sessionStore ?? createDefaultSessionStore();
|
|
473
|
+
const session = options.session ?? await sessionStore.readSession();
|
|
474
|
+
return buildDefaultGranolaAuthInfo(config, {
|
|
475
|
+
existsSyncImpl: options.existsSyncImpl,
|
|
476
|
+
lastError: options.lastError,
|
|
477
|
+
preferredMode: options.preferredMode,
|
|
478
|
+
session
|
|
471
479
|
});
|
|
472
480
|
}
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
else cachePayload = asRecord(rawCache);
|
|
480
|
-
const state = cachePayload ? asRecord(cachePayload.state) : void 0;
|
|
481
|
-
if (!state) throw new Error("failed to parse cache state");
|
|
482
|
-
const rawDocuments = asRecord(state.documents) ?? {};
|
|
483
|
-
const rawTranscripts = asRecord(state.transcripts) ?? {};
|
|
484
|
-
const documents = {};
|
|
485
|
-
for (const [id, rawDocument] of Object.entries(rawDocuments)) {
|
|
486
|
-
const document = parseCacheDocument(id, rawDocument);
|
|
487
|
-
if (document) documents[id] = document;
|
|
481
|
+
var DefaultAuthController = class {
|
|
482
|
+
#lastError;
|
|
483
|
+
#preferredMode;
|
|
484
|
+
constructor(config, options = {}) {
|
|
485
|
+
this.config = config;
|
|
486
|
+
this.options = options;
|
|
488
487
|
}
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
const segments = parseTranscriptSegments(rawTranscript);
|
|
492
|
-
if (segments) transcripts[id] = segments;
|
|
488
|
+
sessionStore() {
|
|
489
|
+
return this.options.sessionStore ?? createDefaultSessionStore();
|
|
493
490
|
}
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
491
|
+
readSession() {
|
|
492
|
+
return this.sessionStore().readSession();
|
|
493
|
+
}
|
|
494
|
+
resolveSupabasePath(overridePath) {
|
|
495
|
+
const supabasePath = overridePath?.trim() || this.config.supabase || "";
|
|
496
|
+
if (!supabasePath) throw missingSupabaseError();
|
|
497
|
+
if (!(this.options.existsSyncImpl ?? existsSync)(supabasePath)) throw new Error(`supabase.json not found: ${supabasePath}`);
|
|
498
|
+
return supabasePath;
|
|
499
|
+
}
|
|
500
|
+
sessionSource(supabasePath) {
|
|
501
|
+
return this.options.sessionSourceFactory?.(supabasePath) ?? new SupabaseFileSessionSource(supabasePath);
|
|
502
|
+
}
|
|
503
|
+
async inspect() {
|
|
504
|
+
const session = await this.readSession();
|
|
505
|
+
return buildDefaultGranolaAuthInfo(this.config, {
|
|
506
|
+
existsSyncImpl: this.options.existsSyncImpl,
|
|
507
|
+
lastError: this.#lastError,
|
|
508
|
+
preferredMode: this.#preferredMode,
|
|
509
|
+
session
|
|
510
|
+
});
|
|
511
|
+
}
|
|
512
|
+
async login(options = {}) {
|
|
513
|
+
const supabasePath = this.resolveSupabasePath(options.supabasePath);
|
|
514
|
+
const session = await this.sessionSource(supabasePath).loadSession();
|
|
515
|
+
await this.sessionStore().writeSession(session);
|
|
516
|
+
this.#lastError = void 0;
|
|
517
|
+
this.#preferredMode = "stored-session";
|
|
518
|
+
return await this.inspect();
|
|
519
|
+
}
|
|
520
|
+
async logout() {
|
|
521
|
+
await this.sessionStore().clearSession();
|
|
522
|
+
this.#lastError = void 0;
|
|
523
|
+
this.#preferredMode = void 0;
|
|
524
|
+
return await this.inspect();
|
|
525
|
+
}
|
|
526
|
+
async refresh() {
|
|
527
|
+
const session = await this.readSession();
|
|
528
|
+
if (!hasStoredSession(session)) {
|
|
529
|
+
this.#lastError = "no stored Granola session found";
|
|
530
|
+
throw new Error(this.#lastError);
|
|
531
|
+
}
|
|
532
|
+
try {
|
|
533
|
+
const refreshed = await refreshGranolaSession(session, this.options.fetchImpl);
|
|
534
|
+
await this.sessionStore().writeSession(refreshed);
|
|
535
|
+
this.#lastError = void 0;
|
|
536
|
+
this.#preferredMode = "stored-session";
|
|
537
|
+
return await this.inspect();
|
|
538
|
+
} catch (error) {
|
|
539
|
+
this.#lastError = error instanceof Error ? error.message : String(error);
|
|
540
|
+
throw error;
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
async switchMode(mode) {
|
|
544
|
+
const state = await this.inspect();
|
|
545
|
+
if (mode === "stored-session" && !state.storedSessionAvailable) {
|
|
546
|
+
this.#lastError = "no stored Granola session found";
|
|
547
|
+
throw new Error(this.#lastError);
|
|
548
|
+
}
|
|
549
|
+
if (mode === "supabase-file") this.resolveSupabasePath();
|
|
550
|
+
this.#lastError = void 0;
|
|
551
|
+
this.#preferredMode = mode;
|
|
552
|
+
return await this.inspect();
|
|
553
|
+
}
|
|
554
|
+
};
|
|
555
|
+
function createDefaultGranolaAuthController(config, options = {}) {
|
|
556
|
+
return new DefaultAuthController(config, options);
|
|
498
557
|
}
|
|
499
558
|
//#endregion
|
|
500
559
|
//#region src/client/parsers.ts
|
|
@@ -690,26 +749,16 @@ var AuthenticatedHttpClient = class {
|
|
|
690
749
|
};
|
|
691
750
|
//#endregion
|
|
692
751
|
//#region src/client/default.ts
|
|
693
|
-
async function
|
|
694
|
-
const
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
mode: hasStoredSession ? "stored-session" : "supabase-file",
|
|
698
|
-
storedSessionAvailable: hasStoredSession,
|
|
699
|
-
supabasePath: config.supabase || void 0
|
|
700
|
-
};
|
|
701
|
-
}
|
|
702
|
-
async function createDefaultGranolaRuntime(config, logger = console) {
|
|
752
|
+
async function createDefaultGranolaRuntime(config, logger = console, options = {}) {
|
|
753
|
+
const auth = await inspectDefaultGranolaAuth(config, { preferredMode: options.preferredMode });
|
|
754
|
+
if (!auth.storedSessionAvailable && !config.supabase) throw new Error(`supabase.json not found. Pass --supabase or create .granola.toml. Expected locations include: ${granolaSupabaseCandidates().join(", ")}`);
|
|
755
|
+
if (!auth.storedSessionAvailable && config.supabase && !existsSync(config.supabase)) throw new Error(`supabase.json not found: ${config.supabase}`);
|
|
703
756
|
const sessionStore = createDefaultSessionStore();
|
|
704
|
-
const auth = await inspectDefaultGranolaAuth(config);
|
|
705
|
-
const hasStoredSession = auth.storedSessionAvailable;
|
|
706
|
-
if (!hasStoredSession && !config.supabase) throw new Error(`supabase.json not found. Pass --supabase or create .granola.toml. Expected locations include: ${granolaSupabaseCandidates().join(", ")}`);
|
|
707
|
-
if (!hasStoredSession && config.supabase && !existsSync(config.supabase)) throw new Error(`supabase.json not found: ${config.supabase}`);
|
|
708
757
|
return {
|
|
709
758
|
auth,
|
|
710
759
|
client: new GranolaApiClient(new AuthenticatedHttpClient({
|
|
711
760
|
logger,
|
|
712
|
-
tokenProvider:
|
|
761
|
+
tokenProvider: auth.mode === "stored-session" ? new StoredSessionTokenProvider(sessionStore, { source: config.supabase && existsSync(config.supabase) ? new SupabaseFileSessionSource(config.supabase) : void 0 }) : new CachedTokenProvider(new SupabaseFileTokenSource(config.supabase), new NoopTokenStore())
|
|
713
762
|
}))
|
|
714
763
|
};
|
|
715
764
|
}
|
|
@@ -1370,6 +1419,20 @@ function compareMeetingDocumentsBySort(left, right, sort) {
|
|
|
1370
1419
|
default: return compareMeetingDocuments(left, right);
|
|
1371
1420
|
}
|
|
1372
1421
|
}
|
|
1422
|
+
function compareMeetingSummariesByUpdated(left, right) {
|
|
1423
|
+
return compareTimestampsDescending(left.updatedAt, right.updatedAt) || compareTimestampsDescending(left.createdAt, right.createdAt) || compareStrings(left.title || left.id, right.title || right.id) || compareStrings(left.id, right.id);
|
|
1424
|
+
}
|
|
1425
|
+
function compareMeetingSummariesByTitle(left, right) {
|
|
1426
|
+
return compareStrings(left.title || left.id, right.title || right.id) || compareTimestampsDescending(left.updatedAt, right.updatedAt) || compareStrings(left.id, right.id);
|
|
1427
|
+
}
|
|
1428
|
+
function compareMeetingSummariesBySort(left, right, sort) {
|
|
1429
|
+
switch (sort) {
|
|
1430
|
+
case "title-asc": return compareMeetingSummariesByTitle(left, right);
|
|
1431
|
+
case "title-desc": return -compareMeetingSummariesByTitle(left, right);
|
|
1432
|
+
case "updated-asc": return -compareMeetingSummariesByUpdated(left, right);
|
|
1433
|
+
default: return compareMeetingSummariesByUpdated(left, right);
|
|
1434
|
+
}
|
|
1435
|
+
}
|
|
1373
1436
|
function serialiseNote(note) {
|
|
1374
1437
|
return {
|
|
1375
1438
|
content: note.content,
|
|
@@ -1433,6 +1496,15 @@ function matchesMeetingSearch(document, search) {
|
|
|
1433
1496
|
...document.tags
|
|
1434
1497
|
].some((value) => value.toLowerCase().includes(query));
|
|
1435
1498
|
}
|
|
1499
|
+
function matchesMeetingSummarySearch(meeting, search) {
|
|
1500
|
+
const query = search.trim().toLowerCase();
|
|
1501
|
+
if (!query) return true;
|
|
1502
|
+
return [
|
|
1503
|
+
meeting.id,
|
|
1504
|
+
meeting.title,
|
|
1505
|
+
...meeting.tags
|
|
1506
|
+
].some((value) => value.toLowerCase().includes(query));
|
|
1507
|
+
}
|
|
1436
1508
|
function parseDateFilter(value, label) {
|
|
1437
1509
|
const trimmed = value?.trim();
|
|
1438
1510
|
if (!trimmed) return;
|
|
@@ -1450,6 +1522,15 @@ function matchesUpdatedRange(document, updatedFrom, updatedTo) {
|
|
|
1450
1522
|
if (to != null && updatedAt > to) return false;
|
|
1451
1523
|
return true;
|
|
1452
1524
|
}
|
|
1525
|
+
function matchesMeetingSummaryUpdatedRange(meeting, updatedFrom, updatedTo) {
|
|
1526
|
+
const from = parseDateFilter(updatedFrom, "updatedFrom");
|
|
1527
|
+
const to = parseDateFilter(updatedTo, "updatedTo");
|
|
1528
|
+
const updatedAt = parseTimestamp(meeting.updatedAt || meeting.createdAt);
|
|
1529
|
+
if (updatedAt == null) return from == null && to == null;
|
|
1530
|
+
if (from != null && updatedAt < from) return false;
|
|
1531
|
+
if (to != null && updatedAt > to) return false;
|
|
1532
|
+
return true;
|
|
1533
|
+
}
|
|
1453
1534
|
function truncate(value, width) {
|
|
1454
1535
|
if (value.length <= width) return value.padEnd(width);
|
|
1455
1536
|
return `${value.slice(0, Math.max(0, width - 1))}…`;
|
|
@@ -1505,6 +1586,14 @@ function listMeetings(documents, options = {}) {
|
|
|
1505
1586
|
const sort = options.sort ?? "updated-desc";
|
|
1506
1587
|
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));
|
|
1507
1588
|
}
|
|
1589
|
+
function filterMeetingSummaries(meetings, options = {}) {
|
|
1590
|
+
const limit = options.limit ?? 20;
|
|
1591
|
+
const sort = options.sort ?? "updated-desc";
|
|
1592
|
+
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) => ({
|
|
1593
|
+
...meeting,
|
|
1594
|
+
tags: [...meeting.tags]
|
|
1595
|
+
}));
|
|
1596
|
+
}
|
|
1508
1597
|
function resolveMeetingQuery(documents, query) {
|
|
1509
1598
|
const trimmed = query.trim();
|
|
1510
1599
|
if (!trimmed) throw new Error("meeting query is required");
|
|
@@ -1591,6 +1680,48 @@ function renderMeetingTranscript(document, cacheData, format = "text") {
|
|
|
1591
1680
|
return renderTranscriptExport(transcript, format);
|
|
1592
1681
|
}
|
|
1593
1682
|
//#endregion
|
|
1683
|
+
//#region src/meeting-index.ts
|
|
1684
|
+
const MEETING_INDEX_VERSION = 1;
|
|
1685
|
+
var FileMeetingIndexStore = class {
|
|
1686
|
+
constructor(filePath = defaultMeetingIndexFilePath()) {
|
|
1687
|
+
this.filePath = filePath;
|
|
1688
|
+
}
|
|
1689
|
+
async readIndex() {
|
|
1690
|
+
try {
|
|
1691
|
+
const parsed = parseJsonString(await readFile(this.filePath, "utf8"));
|
|
1692
|
+
if (!parsed || parsed.version !== MEETING_INDEX_VERSION || !Array.isArray(parsed.meetings)) return [];
|
|
1693
|
+
return parsed.meetings.map((meeting) => ({
|
|
1694
|
+
...meeting,
|
|
1695
|
+
tags: [...meeting.tags]
|
|
1696
|
+
}));
|
|
1697
|
+
} catch {
|
|
1698
|
+
return [];
|
|
1699
|
+
}
|
|
1700
|
+
}
|
|
1701
|
+
async writeIndex(meetings) {
|
|
1702
|
+
await mkdir(dirname(this.filePath), { recursive: true });
|
|
1703
|
+
const payload = {
|
|
1704
|
+
meetings: meetings.map((meeting) => ({
|
|
1705
|
+
...meeting,
|
|
1706
|
+
tags: [...meeting.tags]
|
|
1707
|
+
})),
|
|
1708
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1709
|
+
version: MEETING_INDEX_VERSION
|
|
1710
|
+
};
|
|
1711
|
+
await writeFile(this.filePath, `${JSON.stringify(payload, null, 2)}\n`, {
|
|
1712
|
+
encoding: "utf8",
|
|
1713
|
+
mode: 384
|
|
1714
|
+
});
|
|
1715
|
+
}
|
|
1716
|
+
};
|
|
1717
|
+
function defaultMeetingIndexFilePath() {
|
|
1718
|
+
const home = homedir();
|
|
1719
|
+
return platform() === "darwin" ? join(home, "Library", "Application Support", "granola-toolkit", "meeting-index.json") : join(home, ".config", "granola-toolkit", "meeting-index.json");
|
|
1720
|
+
}
|
|
1721
|
+
function createDefaultMeetingIndexStore() {
|
|
1722
|
+
return new FileMeetingIndexStore();
|
|
1723
|
+
}
|
|
1724
|
+
//#endregion
|
|
1594
1725
|
//#region src/app/core.ts
|
|
1595
1726
|
function transcriptCount(cacheData) {
|
|
1596
1727
|
return Object.values(cacheData.transcripts).filter((segments) => segments.length > 0).length;
|
|
@@ -1616,6 +1747,7 @@ function cloneState(state) {
|
|
|
1616
1747
|
notes: cloneExportState(state.exports.notes),
|
|
1617
1748
|
transcripts: cloneExportState(state.exports.transcripts)
|
|
1618
1749
|
},
|
|
1750
|
+
index: { ...state.index },
|
|
1619
1751
|
ui: { ...state.ui }
|
|
1620
1752
|
};
|
|
1621
1753
|
}
|
|
@@ -1639,6 +1771,12 @@ function defaultState(config, auth, surface) {
|
|
|
1639
1771
|
loaded: false
|
|
1640
1772
|
},
|
|
1641
1773
|
exports: { jobs: [] },
|
|
1774
|
+
index: {
|
|
1775
|
+
available: false,
|
|
1776
|
+
filePath: defaultMeetingIndexFilePath(),
|
|
1777
|
+
loaded: false,
|
|
1778
|
+
meetingCount: 0
|
|
1779
|
+
},
|
|
1642
1780
|
ui: {
|
|
1643
1781
|
surface,
|
|
1644
1782
|
view: "idle"
|
|
@@ -1650,13 +1788,26 @@ var GranolaApp = class {
|
|
|
1650
1788
|
#cacheResolved = false;
|
|
1651
1789
|
#granolaClient;
|
|
1652
1790
|
#documents;
|
|
1791
|
+
#meetingIndex;
|
|
1653
1792
|
#listeners = /* @__PURE__ */ new Set();
|
|
1793
|
+
#refreshingMeetingIndex;
|
|
1654
1794
|
#state;
|
|
1655
1795
|
constructor(config, deps, options = {}) {
|
|
1656
1796
|
this.config = config;
|
|
1657
1797
|
this.deps = deps;
|
|
1658
1798
|
this.#state = defaultState(config, deps.auth, options.surface ?? "cli");
|
|
1659
1799
|
this.#state.exports.jobs = (deps.exportJobs ?? []).map((job) => cloneExportJob(job));
|
|
1800
|
+
this.#meetingIndex = (deps.meetingIndex ?? []).map((meeting) => ({
|
|
1801
|
+
...meeting,
|
|
1802
|
+
tags: [...meeting.tags]
|
|
1803
|
+
}));
|
|
1804
|
+
this.#state.index = {
|
|
1805
|
+
available: this.#meetingIndex.length > 0,
|
|
1806
|
+
filePath: defaultMeetingIndexFilePath(),
|
|
1807
|
+
loaded: this.#meetingIndex.length > 0,
|
|
1808
|
+
loadedAt: this.#meetingIndex.length > 0 ? this.nowIso() : void 0,
|
|
1809
|
+
meetingCount: this.#meetingIndex.length
|
|
1810
|
+
};
|
|
1660
1811
|
}
|
|
1661
1812
|
getState() {
|
|
1662
1813
|
return cloneState(this.#state);
|
|
@@ -1675,6 +1826,59 @@ var GranolaApp = class {
|
|
|
1675
1826
|
this.emitStateUpdate();
|
|
1676
1827
|
return this.getState();
|
|
1677
1828
|
}
|
|
1829
|
+
resetDocumentsState() {
|
|
1830
|
+
this.#granolaClient = void 0;
|
|
1831
|
+
this.#documents = void 0;
|
|
1832
|
+
this.#state.documents = {
|
|
1833
|
+
count: 0,
|
|
1834
|
+
loaded: false
|
|
1835
|
+
};
|
|
1836
|
+
}
|
|
1837
|
+
applyAuthState(auth, options = {}) {
|
|
1838
|
+
if (options.resetDocuments) this.resetDocumentsState();
|
|
1839
|
+
this.#state.auth = { ...auth };
|
|
1840
|
+
if (options.view) this.#state.ui = {
|
|
1841
|
+
...this.#state.ui,
|
|
1842
|
+
view: options.view
|
|
1843
|
+
};
|
|
1844
|
+
this.emitStateUpdate();
|
|
1845
|
+
return { ...auth };
|
|
1846
|
+
}
|
|
1847
|
+
async persistMeetingIndex(meetings) {
|
|
1848
|
+
this.#meetingIndex = meetings.map((meeting) => ({
|
|
1849
|
+
...meeting,
|
|
1850
|
+
tags: [...meeting.tags]
|
|
1851
|
+
}));
|
|
1852
|
+
this.#state.index = {
|
|
1853
|
+
available: this.#meetingIndex.length > 0,
|
|
1854
|
+
filePath: this.#state.index.filePath,
|
|
1855
|
+
loaded: this.#meetingIndex.length > 0,
|
|
1856
|
+
loadedAt: this.#meetingIndex.length > 0 ? this.nowIso() : void 0,
|
|
1857
|
+
meetingCount: this.#meetingIndex.length
|
|
1858
|
+
};
|
|
1859
|
+
if (this.deps.meetingIndexStore) await this.deps.meetingIndexStore.writeIndex(this.#meetingIndex);
|
|
1860
|
+
this.emitStateUpdate();
|
|
1861
|
+
}
|
|
1862
|
+
async refreshMeetingIndexFromLiveData() {
|
|
1863
|
+
const cacheData = await this.loadCache();
|
|
1864
|
+
const documents = await this.listDocuments();
|
|
1865
|
+
const meetings = listMeetings(documents, {
|
|
1866
|
+
cacheData,
|
|
1867
|
+
limit: documents.length || 1,
|
|
1868
|
+
sort: "updated-desc"
|
|
1869
|
+
});
|
|
1870
|
+
await this.persistMeetingIndex(meetings);
|
|
1871
|
+
}
|
|
1872
|
+
triggerMeetingIndexRefresh() {
|
|
1873
|
+
if (this.#refreshingMeetingIndex) return;
|
|
1874
|
+
this.#refreshingMeetingIndex = (async () => {
|
|
1875
|
+
try {
|
|
1876
|
+
await this.refreshMeetingIndexFromLiveData();
|
|
1877
|
+
} catch {} finally {
|
|
1878
|
+
this.#refreshingMeetingIndex = void 0;
|
|
1879
|
+
}
|
|
1880
|
+
})();
|
|
1881
|
+
}
|
|
1678
1882
|
nowIso() {
|
|
1679
1883
|
return (this.deps.now ?? (() => /* @__PURE__ */ new Date()))().toISOString();
|
|
1680
1884
|
}
|
|
@@ -1693,10 +1897,9 @@ var GranolaApp = class {
|
|
|
1693
1897
|
return this.#granolaClient;
|
|
1694
1898
|
}
|
|
1695
1899
|
if (!this.deps.createGranolaClient) throw new Error("Granola API client is not configured");
|
|
1696
|
-
const runtime = await this.deps.createGranolaClient();
|
|
1900
|
+
const runtime = await this.deps.createGranolaClient(this.#state.auth.mode);
|
|
1697
1901
|
this.#granolaClient = runtime.client;
|
|
1698
|
-
this
|
|
1699
|
-
this.emitStateUpdate();
|
|
1902
|
+
this.applyAuthState(runtime.auth);
|
|
1700
1903
|
return this.#granolaClient;
|
|
1701
1904
|
}
|
|
1702
1905
|
missingCacheError() {
|
|
@@ -1751,7 +1954,74 @@ var GranolaApp = class {
|
|
|
1751
1954
|
written: patch.written
|
|
1752
1955
|
});
|
|
1753
1956
|
}
|
|
1754
|
-
|
|
1957
|
+
requireAuthController() {
|
|
1958
|
+
if (!this.deps.authController) throw new Error("Granola auth control is not configured");
|
|
1959
|
+
return this.deps.authController;
|
|
1960
|
+
}
|
|
1961
|
+
async inspectAuth() {
|
|
1962
|
+
if (!this.deps.authController) return { ...this.#state.auth };
|
|
1963
|
+
const auth = await this.deps.authController.inspect();
|
|
1964
|
+
return this.applyAuthState(auth, { view: "auth" });
|
|
1965
|
+
}
|
|
1966
|
+
async loginAuth(options = {}) {
|
|
1967
|
+
const controller = this.requireAuthController();
|
|
1968
|
+
try {
|
|
1969
|
+
const auth = await controller.login(options);
|
|
1970
|
+
return this.applyAuthState(auth, {
|
|
1971
|
+
resetDocuments: true,
|
|
1972
|
+
view: "auth"
|
|
1973
|
+
});
|
|
1974
|
+
} catch (error) {
|
|
1975
|
+
const auth = await controller.inspect();
|
|
1976
|
+
this.applyAuthState(auth, { view: "auth" });
|
|
1977
|
+
throw error;
|
|
1978
|
+
}
|
|
1979
|
+
}
|
|
1980
|
+
async logoutAuth() {
|
|
1981
|
+
const auth = await this.requireAuthController().logout();
|
|
1982
|
+
return this.applyAuthState(auth, {
|
|
1983
|
+
resetDocuments: true,
|
|
1984
|
+
view: "auth"
|
|
1985
|
+
});
|
|
1986
|
+
}
|
|
1987
|
+
async refreshAuth() {
|
|
1988
|
+
const controller = this.requireAuthController();
|
|
1989
|
+
try {
|
|
1990
|
+
const auth = await controller.refresh();
|
|
1991
|
+
return this.applyAuthState(auth, {
|
|
1992
|
+
resetDocuments: true,
|
|
1993
|
+
view: "auth"
|
|
1994
|
+
});
|
|
1995
|
+
} catch (error) {
|
|
1996
|
+
const auth = await controller.inspect();
|
|
1997
|
+
this.applyAuthState(auth, { view: "auth" });
|
|
1998
|
+
throw error;
|
|
1999
|
+
}
|
|
2000
|
+
}
|
|
2001
|
+
async switchAuthMode(mode) {
|
|
2002
|
+
const controller = this.requireAuthController();
|
|
2003
|
+
try {
|
|
2004
|
+
const auth = await controller.switchMode(mode);
|
|
2005
|
+
return this.applyAuthState(auth, {
|
|
2006
|
+
resetDocuments: true,
|
|
2007
|
+
view: "auth"
|
|
2008
|
+
});
|
|
2009
|
+
} catch (error) {
|
|
2010
|
+
const auth = await controller.inspect();
|
|
2011
|
+
this.applyAuthState(auth, { view: "auth" });
|
|
2012
|
+
throw error;
|
|
2013
|
+
}
|
|
2014
|
+
}
|
|
2015
|
+
async listDocuments(options = {}) {
|
|
2016
|
+
if (options.forceRefresh) {
|
|
2017
|
+
this.#granolaClient = void 0;
|
|
2018
|
+
this.#documents = void 0;
|
|
2019
|
+
this.#state.documents = {
|
|
2020
|
+
count: 0,
|
|
2021
|
+
loaded: false
|
|
2022
|
+
};
|
|
2023
|
+
this.emitStateUpdate();
|
|
2024
|
+
}
|
|
1755
2025
|
if (this.#documents) return this.#documents;
|
|
1756
2026
|
const documents = await (await this.getGranolaClient()).listDocuments({ timeoutMs: this.config.notes.timeoutMs });
|
|
1757
2027
|
this.#documents = documents;
|
|
@@ -1791,15 +2061,41 @@ var GranolaApp = class {
|
|
|
1791
2061
|
return cacheData;
|
|
1792
2062
|
}
|
|
1793
2063
|
async listMeetings(options = {}) {
|
|
1794
|
-
const
|
|
1795
|
-
|
|
2064
|
+
const preferIndex = options.preferIndex ?? (this.#state.ui.surface === "web" || this.#state.ui.surface === "server");
|
|
2065
|
+
if (!options.forceRefresh && preferIndex && !this.#documents && this.#meetingIndex.length > 0) {
|
|
2066
|
+
const meetings = filterMeetingSummaries(this.#meetingIndex, options);
|
|
2067
|
+
this.setUiState({
|
|
2068
|
+
meetingListSource: "index",
|
|
2069
|
+
meetingSearch: options.search,
|
|
2070
|
+
meetingSort: options.sort,
|
|
2071
|
+
meetingUpdatedFrom: options.updatedFrom,
|
|
2072
|
+
meetingUpdatedTo: options.updatedTo,
|
|
2073
|
+
selectedMeetingId: void 0,
|
|
2074
|
+
view: "meeting-list"
|
|
2075
|
+
});
|
|
2076
|
+
this.triggerMeetingIndexRefresh();
|
|
2077
|
+
return {
|
|
2078
|
+
meetings,
|
|
2079
|
+
source: "index"
|
|
2080
|
+
};
|
|
2081
|
+
}
|
|
2082
|
+
const cacheData = await this.loadCache();
|
|
2083
|
+
const documents = await this.listDocuments({ forceRefresh: options.forceRefresh });
|
|
2084
|
+
const meetings = listMeetings(documents, {
|
|
2085
|
+
cacheData,
|
|
1796
2086
|
limit: options.limit,
|
|
1797
2087
|
search: options.search,
|
|
1798
2088
|
sort: options.sort,
|
|
1799
2089
|
updatedFrom: options.updatedFrom,
|
|
1800
2090
|
updatedTo: options.updatedTo
|
|
1801
2091
|
});
|
|
2092
|
+
await this.persistMeetingIndex(listMeetings(documents, {
|
|
2093
|
+
cacheData,
|
|
2094
|
+
limit: Math.max(documents.length, 1),
|
|
2095
|
+
sort: "updated-desc"
|
|
2096
|
+
}));
|
|
1802
2097
|
this.setUiState({
|
|
2098
|
+
meetingListSource: "live",
|
|
1803
2099
|
meetingSearch: options.search,
|
|
1804
2100
|
meetingSort: options.sort,
|
|
1805
2101
|
meetingUpdatedFrom: options.updatedFrom,
|
|
@@ -1807,7 +2103,10 @@ var GranolaApp = class {
|
|
|
1807
2103
|
selectedMeetingId: void 0,
|
|
1808
2104
|
view: "meeting-list"
|
|
1809
2105
|
});
|
|
1810
|
-
return
|
|
2106
|
+
return {
|
|
2107
|
+
meetings,
|
|
2108
|
+
source: "live"
|
|
2109
|
+
};
|
|
1811
2110
|
}
|
|
1812
2111
|
async getMeeting(id, options = {}) {
|
|
1813
2112
|
const documents = await this.listDocuments();
|
|
@@ -1950,13 +2249,19 @@ var GranolaApp = class {
|
|
|
1950
2249
|
};
|
|
1951
2250
|
async function createGranolaApp(config, options = {}) {
|
|
1952
2251
|
const auth = await inspectDefaultGranolaAuth(config);
|
|
2252
|
+
const authController = createDefaultGranolaAuthController(config);
|
|
1953
2253
|
const exportJobStore = createDefaultExportJobStore();
|
|
2254
|
+
const exportJobs = await exportJobStore.readJobs();
|
|
2255
|
+
const meetingIndexStore = createDefaultMeetingIndexStore();
|
|
1954
2256
|
return new GranolaApp(config, {
|
|
1955
2257
|
auth,
|
|
2258
|
+
authController,
|
|
1956
2259
|
cacheLoader: loadOptionalGranolaCache,
|
|
1957
|
-
createGranolaClient: async () => await createDefaultGranolaRuntime(config, options.logger),
|
|
1958
|
-
exportJobs
|
|
2260
|
+
createGranolaClient: async (mode) => await createDefaultGranolaRuntime(config, options.logger, { preferredMode: mode }),
|
|
2261
|
+
exportJobs,
|
|
1959
2262
|
exportJobStore,
|
|
2263
|
+
meetingIndex: await meetingIndexStore.readIndex(),
|
|
2264
|
+
meetingIndexStore,
|
|
1960
2265
|
now: options.now
|
|
1961
2266
|
}, { surface: options.surface });
|
|
1962
2267
|
}
|
|
@@ -2076,6 +2381,103 @@ async function waitForShutdown(close) {
|
|
|
2076
2381
|
});
|
|
2077
2382
|
}
|
|
2078
2383
|
//#endregion
|
|
2384
|
+
//#region src/commands/auth.ts
|
|
2385
|
+
function authHelp() {
|
|
2386
|
+
return `Granola auth
|
|
2387
|
+
|
|
2388
|
+
Usage:
|
|
2389
|
+
granola auth <login|status|logout|refresh|use> [options]
|
|
2390
|
+
|
|
2391
|
+
Subcommands:
|
|
2392
|
+
login Import credentials from the Granola desktop app
|
|
2393
|
+
status Show the current Granola auth state
|
|
2394
|
+
logout Delete the stored Granola session
|
|
2395
|
+
refresh Refresh the stored Granola session
|
|
2396
|
+
use <stored|supabase>
|
|
2397
|
+
Switch the active auth source for this toolkit instance
|
|
2398
|
+
|
|
2399
|
+
Options:
|
|
2400
|
+
--supabase <path> Path to supabase.json for auth login
|
|
2401
|
+
--config <path> Path to .granola.toml
|
|
2402
|
+
--debug Enable debug logging
|
|
2403
|
+
-h, --help Show help
|
|
2404
|
+
`;
|
|
2405
|
+
}
|
|
2406
|
+
function formatAuthSource(mode) {
|
|
2407
|
+
return mode === "stored-session" ? "stored session" : "supabase.json";
|
|
2408
|
+
}
|
|
2409
|
+
function printAuthState(state) {
|
|
2410
|
+
console.log(`Active source: ${formatAuthSource(state.mode)}`);
|
|
2411
|
+
console.log(`Stored session: ${state.storedSessionAvailable ? "available" : "missing"}`);
|
|
2412
|
+
console.log(`supabase.json: ${state.supabaseAvailable ? "available" : "missing"}`);
|
|
2413
|
+
if (state.supabasePath) console.log(`supabase path: ${state.supabasePath}`);
|
|
2414
|
+
if (state.clientId) console.log(`Client ID: ${state.clientId}`);
|
|
2415
|
+
console.log(`Refresh token: ${state.refreshAvailable ? "available" : "missing"}`);
|
|
2416
|
+
if (state.signInMethod) console.log(`Sign-in method: ${state.signInMethod}`);
|
|
2417
|
+
if (state.lastError) console.log(`Last error: ${state.lastError}`);
|
|
2418
|
+
}
|
|
2419
|
+
const authCommand = {
|
|
2420
|
+
description: "Manage stored Granola sessions",
|
|
2421
|
+
flags: { help: { type: "boolean" } },
|
|
2422
|
+
help: authHelp,
|
|
2423
|
+
name: "auth",
|
|
2424
|
+
async run({ commandArgs, globalFlags }) {
|
|
2425
|
+
const [action, value] = commandArgs;
|
|
2426
|
+
const config = await loadConfig({
|
|
2427
|
+
globalFlags,
|
|
2428
|
+
subcommandFlags: {}
|
|
2429
|
+
});
|
|
2430
|
+
debug(config.debug, "using config", config.configFileUsed ?? "(none)");
|
|
2431
|
+
debug(config.debug, "supabase", config.supabase);
|
|
2432
|
+
const app = await createGranolaApp(config);
|
|
2433
|
+
switch (action) {
|
|
2434
|
+
case "login": {
|
|
2435
|
+
const state = await app.loginAuth();
|
|
2436
|
+
console.log(`Imported Granola session from ${state.supabasePath ?? "desktop app defaults"}`);
|
|
2437
|
+
printAuthState(state);
|
|
2438
|
+
return 0;
|
|
2439
|
+
}
|
|
2440
|
+
case "logout": {
|
|
2441
|
+
const state = await app.logoutAuth();
|
|
2442
|
+
console.log("Stored Granola session deleted");
|
|
2443
|
+
printAuthState(state);
|
|
2444
|
+
return 0;
|
|
2445
|
+
}
|
|
2446
|
+
case "refresh": {
|
|
2447
|
+
const state = await app.refreshAuth();
|
|
2448
|
+
console.log("Stored Granola session refreshed");
|
|
2449
|
+
printAuthState(state);
|
|
2450
|
+
return 0;
|
|
2451
|
+
}
|
|
2452
|
+
case "status": {
|
|
2453
|
+
const state = await app.inspectAuth();
|
|
2454
|
+
printAuthState(state);
|
|
2455
|
+
return state.storedSessionAvailable ? 0 : 1;
|
|
2456
|
+
}
|
|
2457
|
+
case "use": {
|
|
2458
|
+
const mode = resolveAuthMode(value);
|
|
2459
|
+
const state = await app.switchAuthMode(mode);
|
|
2460
|
+
console.log(`Switched auth source to ${formatAuthSource(state.mode)}`);
|
|
2461
|
+
printAuthState(state);
|
|
2462
|
+
return 0;
|
|
2463
|
+
}
|
|
2464
|
+
case void 0:
|
|
2465
|
+
console.log(authHelp());
|
|
2466
|
+
return 1;
|
|
2467
|
+
default: throw new Error("invalid auth command: expected login, status, logout, refresh, or use");
|
|
2468
|
+
}
|
|
2469
|
+
}
|
|
2470
|
+
};
|
|
2471
|
+
function resolveAuthMode(value) {
|
|
2472
|
+
switch (value) {
|
|
2473
|
+
case "stored":
|
|
2474
|
+
case "stored-session": return "stored-session";
|
|
2475
|
+
case "supabase":
|
|
2476
|
+
case "supabase-file": return "supabase-file";
|
|
2477
|
+
default: throw new Error("invalid auth mode: expected stored or supabase");
|
|
2478
|
+
}
|
|
2479
|
+
}
|
|
2480
|
+
//#endregion
|
|
2079
2481
|
//#region src/commands/exports.ts
|
|
2080
2482
|
function exportsHelp() {
|
|
2081
2483
|
return `Granola exports
|
|
@@ -2304,12 +2706,13 @@ async function list(commandFlags, globalFlags) {
|
|
|
2304
2706
|
debug(config.debug, "timeoutMs", config.notes.timeoutMs);
|
|
2305
2707
|
const app = await createGranolaApp(config);
|
|
2306
2708
|
debug(config.debug, "authMode", app.getState().auth.mode);
|
|
2307
|
-
console.log("
|
|
2308
|
-
const
|
|
2709
|
+
console.log("Loading meetings...");
|
|
2710
|
+
const result = await app.listMeetings({
|
|
2309
2711
|
limit,
|
|
2310
2712
|
search
|
|
2311
2713
|
});
|
|
2312
|
-
console.log(
|
|
2714
|
+
console.log(result.source === "index" ? "Loaded meetings from the local index" : "Fetched meetings from Granola API");
|
|
2715
|
+
console.log(renderMeetingList(result.meetings, format).trimEnd());
|
|
2313
2716
|
return 0;
|
|
2314
2717
|
}
|
|
2315
2718
|
async function view(id, commandFlags, globalFlags) {
|
|
@@ -2452,6 +2855,7 @@ const state = {
|
|
|
2452
2855
|
selectedMeeting: null,
|
|
2453
2856
|
selectedMeetingBundle: null,
|
|
2454
2857
|
selectedMeetingId: null,
|
|
2858
|
+
meetingSource: "live",
|
|
2455
2859
|
sort: "updated-desc",
|
|
2456
2860
|
updatedFrom: "",
|
|
2457
2861
|
updatedTo: "",
|
|
@@ -2460,6 +2864,7 @@ const state = {
|
|
|
2460
2864
|
|
|
2461
2865
|
const els = {
|
|
2462
2866
|
appState: document.querySelector("[data-app-state]"),
|
|
2867
|
+
authPanel: document.querySelector("[data-auth-panel]"),
|
|
2463
2868
|
detailBody: document.querySelector("[data-detail-body]"),
|
|
2464
2869
|
detailMeta: document.querySelector("[data-detail-meta]"),
|
|
2465
2870
|
empty: document.querySelector("[data-empty]"),
|
|
@@ -2526,6 +2931,7 @@ function renderWorkspaceTabs() {
|
|
|
2526
2931
|
function renderAppState() {
|
|
2527
2932
|
if (!state.appState) {
|
|
2528
2933
|
els.appState.innerHTML = "<p>Waiting for server state…</p>";
|
|
2934
|
+
els.authPanel.innerHTML = "<p>Waiting for auth state…</p>";
|
|
2529
2935
|
return;
|
|
2530
2936
|
}
|
|
2531
2937
|
|
|
@@ -2537,6 +2943,11 @@ function renderAppState() {
|
|
|
2537
2943
|
: appState.cache.configured
|
|
2538
2944
|
? "configured"
|
|
2539
2945
|
: "not configured";
|
|
2946
|
+
const indexStatus = appState.index.loaded
|
|
2947
|
+
? appState.index.meetingCount + " meetings"
|
|
2948
|
+
: appState.index.available
|
|
2949
|
+
? "available"
|
|
2950
|
+
: "not built";
|
|
2540
2951
|
|
|
2541
2952
|
els.appState.innerHTML = [
|
|
2542
2953
|
'<div class="status-grid">',
|
|
@@ -2545,12 +2956,79 @@ function renderAppState() {
|
|
|
2545
2956
|
'<div><span class="status-label">Auth</span><strong>' + escapeHtml(authMode) + "</strong></div>",
|
|
2546
2957
|
'<div><span class="status-label">Documents</span><strong>' + escapeHtml(docs) + "</strong></div>",
|
|
2547
2958
|
'<div><span class="status-label">Cache</span><strong>' + escapeHtml(cache) + "</strong></div>",
|
|
2959
|
+
'<div><span class="status-label">Index</span><strong>' + escapeHtml(indexStatus) + "</strong></div>",
|
|
2548
2960
|
"</div>",
|
|
2549
2961
|
].join("");
|
|
2550
2962
|
|
|
2963
|
+
renderAuthPanel();
|
|
2551
2964
|
renderExportJobs();
|
|
2552
2965
|
}
|
|
2553
2966
|
|
|
2967
|
+
function authActionButton(label, action, disabled) {
|
|
2968
|
+
return (
|
|
2969
|
+
'<button class="button button--secondary" data-auth-action="' +
|
|
2970
|
+
escapeHtml(action) +
|
|
2971
|
+
'"' +
|
|
2972
|
+
(disabled ? " disabled" : "") +
|
|
2973
|
+
">" +
|
|
2974
|
+
escapeHtml(label) +
|
|
2975
|
+
"</button>"
|
|
2976
|
+
);
|
|
2977
|
+
}
|
|
2978
|
+
|
|
2979
|
+
function authModeButton(label, mode, disabled) {
|
|
2980
|
+
return (
|
|
2981
|
+
'<button class="button button--secondary" data-auth-mode="' +
|
|
2982
|
+
escapeHtml(mode) +
|
|
2983
|
+
'"' +
|
|
2984
|
+
(disabled ? " disabled" : "") +
|
|
2985
|
+
">" +
|
|
2986
|
+
escapeHtml(label) +
|
|
2987
|
+
"</button>"
|
|
2988
|
+
);
|
|
2989
|
+
}
|
|
2990
|
+
|
|
2991
|
+
function renderAuthPanel() {
|
|
2992
|
+
const auth = state.appState?.auth;
|
|
2993
|
+
if (!auth) {
|
|
2994
|
+
els.authPanel.innerHTML = '<div class="auth-card"><div class="auth-card__meta">Auth state unavailable.</div></div>';
|
|
2995
|
+
return;
|
|
2996
|
+
}
|
|
2997
|
+
|
|
2998
|
+
const activeSource = auth.mode === "stored-session" ? "Stored session" : "supabase.json";
|
|
2999
|
+
const lastError = auth.lastError
|
|
3000
|
+
? '<div class="auth-card__meta auth-card__error">' + escapeHtml(auth.lastError) + "</div>"
|
|
3001
|
+
: "";
|
|
3002
|
+
|
|
3003
|
+
els.authPanel.innerHTML = [
|
|
3004
|
+
'<div class="auth-card">',
|
|
3005
|
+
'<div class="status-grid">',
|
|
3006
|
+
'<div><span class="status-label">Active</span><strong>' + escapeHtml(activeSource) + "</strong></div>",
|
|
3007
|
+
'<div><span class="status-label">Stored</span><strong>' + escapeHtml(auth.storedSessionAvailable ? "available" : "missing") + "</strong></div>",
|
|
3008
|
+
'<div><span class="status-label">supabase.json</span><strong>' + escapeHtml(auth.supabaseAvailable ? "available" : "missing") + "</strong></div>",
|
|
3009
|
+
'<div><span class="status-label">Refresh</span><strong>' + escapeHtml(auth.refreshAvailable ? "available" : "missing") + "</strong></div>",
|
|
3010
|
+
"</div>",
|
|
3011
|
+
auth.clientId
|
|
3012
|
+
? '<div class="auth-card__meta">Client ID: ' + escapeHtml(auth.clientId) + "</div>"
|
|
3013
|
+
: "",
|
|
3014
|
+
auth.signInMethod
|
|
3015
|
+
? '<div class="auth-card__meta">Sign-in method: ' + escapeHtml(auth.signInMethod) + "</div>"
|
|
3016
|
+
: "",
|
|
3017
|
+
auth.supabasePath
|
|
3018
|
+
? '<div class="auth-card__meta">supabase path: ' + escapeHtml(auth.supabasePath) + "</div>"
|
|
3019
|
+
: "",
|
|
3020
|
+
lastError,
|
|
3021
|
+
'<div class="auth-card__actions">',
|
|
3022
|
+
authActionButton("Import desktop session", "login", !auth.supabaseAvailable),
|
|
3023
|
+
authActionButton("Refresh stored session", "refresh", !auth.storedSessionAvailable || !auth.refreshAvailable),
|
|
3024
|
+
authModeButton("Use stored session", "stored-session", !auth.storedSessionAvailable || auth.mode === "stored-session"),
|
|
3025
|
+
authModeButton("Use supabase.json", "supabase-file", !auth.supabaseAvailable || auth.mode === "supabase-file"),
|
|
3026
|
+
authActionButton("Sign out", "logout", !auth.storedSessionAvailable),
|
|
3027
|
+
"</div>",
|
|
3028
|
+
"</div>",
|
|
3029
|
+
].join("");
|
|
3030
|
+
}
|
|
3031
|
+
|
|
2554
3032
|
function renderMeetingList() {
|
|
2555
3033
|
if (state.listError) {
|
|
2556
3034
|
els.list.innerHTML =
|
|
@@ -2708,7 +3186,7 @@ async function fetchJson(path, init) {
|
|
|
2708
3186
|
return payload;
|
|
2709
3187
|
}
|
|
2710
3188
|
|
|
2711
|
-
function buildMeetingsQuery(limit = 100) {
|
|
3189
|
+
function buildMeetingsQuery(limit = 100, refresh = false) {
|
|
2712
3190
|
const params = new URLSearchParams();
|
|
2713
3191
|
params.set("limit", String(limit));
|
|
2714
3192
|
params.set("sort", state.sort);
|
|
@@ -2725,16 +3203,22 @@ function buildMeetingsQuery(limit = 100) {
|
|
|
2725
3203
|
params.set("updatedTo", state.updatedTo);
|
|
2726
3204
|
}
|
|
2727
3205
|
|
|
3206
|
+
if (refresh) {
|
|
3207
|
+
params.set("refresh", "true");
|
|
3208
|
+
}
|
|
3209
|
+
|
|
2728
3210
|
return "?" + params.toString();
|
|
2729
3211
|
}
|
|
2730
3212
|
|
|
2731
3213
|
async function loadMeetings(options = {}) {
|
|
2732
3214
|
const preferredMeetingId = options.preferredMeetingId || state.selectedMeetingId;
|
|
3215
|
+
const refresh = options.refresh === true;
|
|
2733
3216
|
|
|
2734
3217
|
try {
|
|
2735
3218
|
state.listError = "";
|
|
2736
|
-
const payload = await fetchJson("/meetings" + buildMeetingsQuery());
|
|
3219
|
+
const payload = await fetchJson("/meetings" + buildMeetingsQuery(100, refresh));
|
|
2737
3220
|
state.meetings = payload.meetings || [];
|
|
3221
|
+
state.meetingSource = payload.source || "live";
|
|
2738
3222
|
|
|
2739
3223
|
if (preferredMeetingId && state.meetings.some((meeting) => meeting.id === preferredMeetingId)) {
|
|
2740
3224
|
state.selectedMeetingId = preferredMeetingId;
|
|
@@ -2803,12 +3287,28 @@ async function quickOpenMeeting() {
|
|
|
2803
3287
|
}
|
|
2804
3288
|
}
|
|
2805
3289
|
|
|
2806
|
-
async function refreshAll() {
|
|
3290
|
+
async function refreshAll(forceLiveMeetings = false) {
|
|
2807
3291
|
setStatus("Refreshing…", "busy");
|
|
2808
|
-
const [appState] = await Promise.all([
|
|
2809
|
-
|
|
3292
|
+
const [appState, authState] = await Promise.all([
|
|
3293
|
+
fetchJson("/state"),
|
|
3294
|
+
fetchJson("/auth/status"),
|
|
3295
|
+
loadMeetings({ refresh: forceLiveMeetings }),
|
|
3296
|
+
]);
|
|
3297
|
+
state.appState = {
|
|
3298
|
+
...appState,
|
|
3299
|
+
auth: authState,
|
|
3300
|
+
};
|
|
3301
|
+
renderAppState();
|
|
3302
|
+
setStatus(forceLiveMeetings ? "Live data refreshed" : state.meetingSource === "index" ? "Loaded from index" : "Connected", "ok");
|
|
3303
|
+
}
|
|
3304
|
+
|
|
3305
|
+
async function syncAuthState() {
|
|
3306
|
+
const [appState, authState] = await Promise.all([fetchJson("/state"), fetchJson("/auth/status")]);
|
|
3307
|
+
state.appState = {
|
|
3308
|
+
...appState,
|
|
3309
|
+
auth: authState,
|
|
3310
|
+
};
|
|
2810
3311
|
renderAppState();
|
|
2811
|
-
setStatus("Connected", "ok");
|
|
2812
3312
|
}
|
|
2813
3313
|
|
|
2814
3314
|
async function exportNotes() {
|
|
@@ -2845,6 +3345,62 @@ async function rerunJob(id) {
|
|
|
2845
3345
|
}
|
|
2846
3346
|
}
|
|
2847
3347
|
|
|
3348
|
+
async function loginAuth() {
|
|
3349
|
+
setStatus("Importing desktop session…", "busy");
|
|
3350
|
+
try {
|
|
3351
|
+
await fetchJson("/auth/login", { method: "POST" });
|
|
3352
|
+
await refreshAll();
|
|
3353
|
+
} catch (error) {
|
|
3354
|
+
await syncAuthState();
|
|
3355
|
+
setStatus("Auth import failed", "error");
|
|
3356
|
+
state.detailError = error instanceof Error ? error.message : String(error);
|
|
3357
|
+
renderMeetingDetail();
|
|
3358
|
+
}
|
|
3359
|
+
}
|
|
3360
|
+
|
|
3361
|
+
async function logoutAuth() {
|
|
3362
|
+
setStatus("Signing out…", "busy");
|
|
3363
|
+
try {
|
|
3364
|
+
await fetchJson("/auth/logout", { method: "POST" });
|
|
3365
|
+
await refreshAll();
|
|
3366
|
+
} catch (error) {
|
|
3367
|
+
await syncAuthState();
|
|
3368
|
+
setStatus("Sign out failed", "error");
|
|
3369
|
+
state.detailError = error instanceof Error ? error.message : String(error);
|
|
3370
|
+
renderMeetingDetail();
|
|
3371
|
+
}
|
|
3372
|
+
}
|
|
3373
|
+
|
|
3374
|
+
async function refreshAuth() {
|
|
3375
|
+
setStatus("Refreshing session…", "busy");
|
|
3376
|
+
try {
|
|
3377
|
+
await fetchJson("/auth/refresh", { method: "POST" });
|
|
3378
|
+
await refreshAll();
|
|
3379
|
+
} catch (error) {
|
|
3380
|
+
await syncAuthState();
|
|
3381
|
+
setStatus("Refresh failed", "error");
|
|
3382
|
+
state.detailError = error instanceof Error ? error.message : String(error);
|
|
3383
|
+
renderMeetingDetail();
|
|
3384
|
+
}
|
|
3385
|
+
}
|
|
3386
|
+
|
|
3387
|
+
async function switchAuthMode(mode) {
|
|
3388
|
+
setStatus("Switching auth source…", "busy");
|
|
3389
|
+
try {
|
|
3390
|
+
await fetchJson("/auth/mode", {
|
|
3391
|
+
body: JSON.stringify({ mode }),
|
|
3392
|
+
headers: { "content-type": "application/json" },
|
|
3393
|
+
method: "POST",
|
|
3394
|
+
});
|
|
3395
|
+
await refreshAll();
|
|
3396
|
+
} catch (error) {
|
|
3397
|
+
await syncAuthState();
|
|
3398
|
+
setStatus("Switch failed", "error");
|
|
3399
|
+
state.detailError = error instanceof Error ? error.message : String(error);
|
|
3400
|
+
renderMeetingDetail();
|
|
3401
|
+
}
|
|
3402
|
+
}
|
|
3403
|
+
|
|
2848
3404
|
els.list.addEventListener("click", (event) => {
|
|
2849
3405
|
if (!(event.target instanceof Element)) {
|
|
2850
3406
|
return;
|
|
@@ -2868,8 +3424,38 @@ els.jobsList.addEventListener("click", (event) => {
|
|
|
2868
3424
|
void rerunJob(button.dataset.rerunJobId);
|
|
2869
3425
|
});
|
|
2870
3426
|
|
|
3427
|
+
els.authPanel.addEventListener("click", (event) => {
|
|
3428
|
+
if (!(event.target instanceof Element)) {
|
|
3429
|
+
return;
|
|
3430
|
+
}
|
|
3431
|
+
|
|
3432
|
+
const actionButton = event.target.closest("[data-auth-action]");
|
|
3433
|
+
if (actionButton) {
|
|
3434
|
+
switch (actionButton.dataset.authAction) {
|
|
3435
|
+
case "login":
|
|
3436
|
+
void loginAuth();
|
|
3437
|
+
return;
|
|
3438
|
+
case "logout":
|
|
3439
|
+
void logoutAuth();
|
|
3440
|
+
return;
|
|
3441
|
+
case "refresh":
|
|
3442
|
+
void refreshAuth();
|
|
3443
|
+
return;
|
|
3444
|
+
default:
|
|
3445
|
+
return;
|
|
3446
|
+
}
|
|
3447
|
+
}
|
|
3448
|
+
|
|
3449
|
+
const modeButton = event.target.closest("[data-auth-mode]");
|
|
3450
|
+
if (!modeButton) {
|
|
3451
|
+
return;
|
|
3452
|
+
}
|
|
3453
|
+
|
|
3454
|
+
void switchAuthMode(modeButton.dataset.authMode);
|
|
3455
|
+
});
|
|
3456
|
+
|
|
2871
3457
|
els.refreshButton.addEventListener("click", () => {
|
|
2872
|
-
void refreshAll();
|
|
3458
|
+
void refreshAll(true);
|
|
2873
3459
|
});
|
|
2874
3460
|
|
|
2875
3461
|
els.noteButton.addEventListener("click", () => {
|
|
@@ -2994,9 +3580,20 @@ document.addEventListener("keydown", (event) => {
|
|
|
2994
3580
|
|
|
2995
3581
|
const events = new EventSource("/events");
|
|
2996
3582
|
events.addEventListener("state.updated", (event) => {
|
|
3583
|
+
const previousLoadedAt = state.appState?.documents?.loadedAt;
|
|
2997
3584
|
const payload = JSON.parse(event.data);
|
|
2998
3585
|
state.appState = payload.state;
|
|
2999
3586
|
renderAppState();
|
|
3587
|
+
|
|
3588
|
+
if (
|
|
3589
|
+
state.meetingSource === "index" &&
|
|
3590
|
+
payload.state.documents?.loadedAt &&
|
|
3591
|
+
payload.state.documents.loadedAt !== previousLoadedAt
|
|
3592
|
+
) {
|
|
3593
|
+
void loadMeetings({
|
|
3594
|
+
preferredMeetingId: state.selectedMeetingId,
|
|
3595
|
+
});
|
|
3596
|
+
}
|
|
3000
3597
|
});
|
|
3001
3598
|
events.addEventListener("error", () => {
|
|
3002
3599
|
setStatus("Disconnected", "error");
|
|
@@ -3066,6 +3663,13 @@ const granolaWebMarkup = String.raw`
|
|
|
3066
3663
|
</div>
|
|
3067
3664
|
<p>Initial beta web client. It speaks to the same local API that future TUI and attach flows will use.</p>
|
|
3068
3665
|
</section>
|
|
3666
|
+
<section class="auth-panel">
|
|
3667
|
+
<div class="auth-panel__head">
|
|
3668
|
+
<h3>Auth Session</h3>
|
|
3669
|
+
<p>Inspect, refresh, and switch between stored session and <code>supabase.json</code>.</p>
|
|
3670
|
+
</div>
|
|
3671
|
+
<div class="auth-panel__body" data-auth-panel></div>
|
|
3672
|
+
</section>
|
|
3069
3673
|
<section class="jobs-panel">
|
|
3070
3674
|
<div class="jobs-panel__head">
|
|
3071
3675
|
<h3>Recent Export Jobs</h3>
|
|
@@ -3295,10 +3899,12 @@ body {
|
|
|
3295
3899
|
width: min(440px, 100%);
|
|
3296
3900
|
}
|
|
3297
3901
|
|
|
3902
|
+
.auth-panel,
|
|
3298
3903
|
.jobs-panel {
|
|
3299
3904
|
padding: 0 24px 18px;
|
|
3300
3905
|
}
|
|
3301
3906
|
|
|
3907
|
+
.auth-panel__head h3,
|
|
3302
3908
|
.jobs-panel__head h3 {
|
|
3303
3909
|
margin: 0;
|
|
3304
3910
|
font-size: 0.92rem;
|
|
@@ -3306,12 +3912,43 @@ body {
|
|
|
3306
3912
|
text-transform: uppercase;
|
|
3307
3913
|
}
|
|
3308
3914
|
|
|
3915
|
+
.auth-panel__head p,
|
|
3309
3916
|
.jobs-panel__head p {
|
|
3310
3917
|
margin: 6px 0 0;
|
|
3311
3918
|
color: var(--muted);
|
|
3312
3919
|
font-size: 0.9rem;
|
|
3313
3920
|
}
|
|
3314
3921
|
|
|
3922
|
+
.auth-panel__body {
|
|
3923
|
+
display: grid;
|
|
3924
|
+
gap: 12px;
|
|
3925
|
+
margin-top: 14px;
|
|
3926
|
+
}
|
|
3927
|
+
|
|
3928
|
+
.auth-card {
|
|
3929
|
+
display: grid;
|
|
3930
|
+
gap: 12px;
|
|
3931
|
+
padding: 14px 16px;
|
|
3932
|
+
border: 1px solid var(--line);
|
|
3933
|
+
border-radius: 18px;
|
|
3934
|
+
background: rgba(255, 255, 255, 0.72);
|
|
3935
|
+
}
|
|
3936
|
+
|
|
3937
|
+
.auth-card__meta {
|
|
3938
|
+
color: var(--muted);
|
|
3939
|
+
font-size: 0.9rem;
|
|
3940
|
+
}
|
|
3941
|
+
|
|
3942
|
+
.auth-card__actions {
|
|
3943
|
+
display: flex;
|
|
3944
|
+
flex-wrap: wrap;
|
|
3945
|
+
gap: 8px;
|
|
3946
|
+
}
|
|
3947
|
+
|
|
3948
|
+
.auth-card__error {
|
|
3949
|
+
color: var(--error);
|
|
3950
|
+
}
|
|
3951
|
+
|
|
3315
3952
|
.jobs-list {
|
|
3316
3953
|
display: grid;
|
|
3317
3954
|
gap: 10px;
|
|
@@ -3419,6 +4056,11 @@ body {
|
|
|
3419
4056
|
cursor: pointer;
|
|
3420
4057
|
}
|
|
3421
4058
|
|
|
4059
|
+
.button:disabled {
|
|
4060
|
+
cursor: not-allowed;
|
|
4061
|
+
opacity: 0.56;
|
|
4062
|
+
}
|
|
4063
|
+
|
|
3422
4064
|
.button--primary {
|
|
3423
4065
|
background: var(--ink);
|
|
3424
4066
|
color: white;
|
|
@@ -3566,6 +4208,13 @@ function parseMeetingSort(value) {
|
|
|
3566
4208
|
default: throw new Error("invalid sort: expected updated-desc, updated-asc, title-asc, or title-desc");
|
|
3567
4209
|
}
|
|
3568
4210
|
}
|
|
4211
|
+
function parseAuthMode(value) {
|
|
4212
|
+
switch (value) {
|
|
4213
|
+
case "stored-session":
|
|
4214
|
+
case "supabase-file": return value;
|
|
4215
|
+
default: throw new Error("invalid auth mode: expected stored-session or supabase-file");
|
|
4216
|
+
}
|
|
4217
|
+
}
|
|
3569
4218
|
function sendJson(response, body, init = {}) {
|
|
3570
4219
|
const payload = `${JSON.stringify(body, null, 2)}\n`;
|
|
3571
4220
|
response.writeHead(init.status ?? 200, {
|
|
@@ -3648,6 +4297,10 @@ async function startGranolaServer(app, options = {}) {
|
|
|
3648
4297
|
sendJson(response, app.getState());
|
|
3649
4298
|
return;
|
|
3650
4299
|
}
|
|
4300
|
+
if (method === "GET" && path === "/auth/status") {
|
|
4301
|
+
sendJson(response, await app.inspectAuth());
|
|
4302
|
+
return;
|
|
4303
|
+
}
|
|
3651
4304
|
if (method === "GET" && path === "/events") {
|
|
3652
4305
|
response.writeHead(200, {
|
|
3653
4306
|
"cache-control": "no-cache, no-transform",
|
|
@@ -3670,19 +4323,24 @@ async function startGranolaServer(app, options = {}) {
|
|
|
3670
4323
|
}
|
|
3671
4324
|
if (method === "GET" && path === "/meetings") {
|
|
3672
4325
|
const limit = parseInteger(url.searchParams.get("limit"));
|
|
4326
|
+
const refresh = url.searchParams.get("refresh") === "true";
|
|
3673
4327
|
const search = url.searchParams.get("search")?.trim() || void 0;
|
|
3674
4328
|
const sort = parseMeetingSort(url.searchParams.get("sort"));
|
|
3675
4329
|
const updatedFrom = url.searchParams.get("updatedFrom")?.trim() || void 0;
|
|
3676
4330
|
const updatedTo = url.searchParams.get("updatedTo")?.trim() || void 0;
|
|
4331
|
+
const result = await app.listMeetings({
|
|
4332
|
+
forceRefresh: refresh,
|
|
4333
|
+
limit,
|
|
4334
|
+
search,
|
|
4335
|
+
sort,
|
|
4336
|
+
updatedFrom,
|
|
4337
|
+
updatedTo
|
|
4338
|
+
});
|
|
3677
4339
|
sendJson(response, {
|
|
3678
|
-
meetings:
|
|
3679
|
-
|
|
3680
|
-
search,
|
|
3681
|
-
sort,
|
|
3682
|
-
updatedFrom,
|
|
3683
|
-
updatedTo
|
|
3684
|
-
}),
|
|
4340
|
+
meetings: result.meetings,
|
|
4341
|
+
refresh,
|
|
3685
4342
|
search,
|
|
4343
|
+
source: result.source,
|
|
3686
4344
|
sort,
|
|
3687
4345
|
updatedFrom,
|
|
3688
4346
|
updatedTo
|
|
@@ -3701,6 +4359,25 @@ async function startGranolaServer(app, options = {}) {
|
|
|
3701
4359
|
sendJson(response, await app.getMeeting(id, { requireCache: url.searchParams.get("includeTranscript") === "true" }));
|
|
3702
4360
|
return;
|
|
3703
4361
|
}
|
|
4362
|
+
if (method === "POST" && path === "/auth/login") {
|
|
4363
|
+
const body = await readJsonBody(request);
|
|
4364
|
+
const supabasePath = typeof body.supabasePath === "string" && body.supabasePath.trim() ? body.supabasePath.trim() : void 0;
|
|
4365
|
+
sendJson(response, await app.loginAuth({ supabasePath }));
|
|
4366
|
+
return;
|
|
4367
|
+
}
|
|
4368
|
+
if (method === "POST" && path === "/auth/logout") {
|
|
4369
|
+
sendJson(response, await app.logoutAuth());
|
|
4370
|
+
return;
|
|
4371
|
+
}
|
|
4372
|
+
if (method === "POST" && path === "/auth/refresh") {
|
|
4373
|
+
sendJson(response, await app.refreshAuth());
|
|
4374
|
+
return;
|
|
4375
|
+
}
|
|
4376
|
+
if (method === "POST" && path === "/auth/mode") {
|
|
4377
|
+
const body = await readJsonBody(request);
|
|
4378
|
+
sendJson(response, await app.switchAuthMode(parseAuthMode(body.mode)));
|
|
4379
|
+
return;
|
|
4380
|
+
}
|
|
3704
4381
|
if (method === "POST" && path === "/exports/notes") {
|
|
3705
4382
|
const body = await readJsonBody(request);
|
|
3706
4383
|
sendJson(response, await app.exportNotes(noteFormatFromBody(body.format)), { status: 202 });
|
|
@@ -3803,11 +4480,16 @@ const serveCommand = {
|
|
|
3803
4480
|
console.log(`Granola server listening on ${server.url.href}`);
|
|
3804
4481
|
console.log("Endpoints:");
|
|
3805
4482
|
console.log(" GET /health");
|
|
4483
|
+
console.log(" GET /auth/status");
|
|
3806
4484
|
console.log(" GET /state");
|
|
3807
4485
|
console.log(" GET /events");
|
|
3808
4486
|
console.log(" GET /meetings");
|
|
3809
4487
|
console.log(" GET /meetings/:id");
|
|
3810
4488
|
console.log(" GET /exports/jobs");
|
|
4489
|
+
console.log(" POST /auth/login");
|
|
4490
|
+
console.log(" POST /auth/logout");
|
|
4491
|
+
console.log(" POST /auth/mode");
|
|
4492
|
+
console.log(" POST /auth/refresh");
|
|
3811
4493
|
console.log(" POST /exports/notes");
|
|
3812
4494
|
console.log(" POST /exports/jobs/:id/rerun");
|
|
3813
4495
|
console.log(" POST /exports/transcripts");
|
|
@@ -3964,11 +4646,16 @@ const commands = [
|
|
|
3964
4646
|
console.log("Routes:");
|
|
3965
4647
|
console.log(" GET /");
|
|
3966
4648
|
console.log(" GET /health");
|
|
4649
|
+
console.log(" GET /auth/status");
|
|
3967
4650
|
console.log(" GET /state");
|
|
3968
4651
|
console.log(" GET /events");
|
|
3969
4652
|
console.log(" GET /meetings");
|
|
3970
4653
|
console.log(" GET /meetings/:id");
|
|
3971
4654
|
console.log(" GET /exports/jobs");
|
|
4655
|
+
console.log(" POST /auth/login");
|
|
4656
|
+
console.log(" POST /auth/logout");
|
|
4657
|
+
console.log(" POST /auth/mode");
|
|
4658
|
+
console.log(" POST /auth/refresh");
|
|
3972
4659
|
console.log(" POST /exports/notes");
|
|
3973
4660
|
console.log(" POST /exports/jobs/:id/rerun");
|
|
3974
4661
|
console.log(" POST /exports/transcripts");
|