granola-toolkit 0.37.0 → 0.39.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 +4 -1
- package/dist/cli.js +407 -32
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -20,7 +20,7 @@ The published package exposes both `granola` and `granola-toolkit` as executable
|
|
|
20
20
|
## Quick Start
|
|
21
21
|
|
|
22
22
|
```bash
|
|
23
|
-
granola auth login
|
|
23
|
+
granola auth login --api-key grn_...
|
|
24
24
|
granola sync
|
|
25
25
|
granola sync --watch
|
|
26
26
|
granola folder list
|
|
@@ -30,6 +30,9 @@ granola web
|
|
|
30
30
|
granola tui
|
|
31
31
|
```
|
|
32
32
|
|
|
33
|
+
If you prefer to reuse the desktop app session instead, `granola auth login` still imports it from
|
|
34
|
+
`supabase.json`.
|
|
35
|
+
|
|
33
36
|
## Documentation
|
|
34
37
|
|
|
35
38
|
The detailed documentation now lives at
|
package/dist/cli.js
CHANGED
|
@@ -1088,14 +1088,14 @@ function cacheDocumentForMeeting(document, cacheData) {
|
|
|
1088
1088
|
};
|
|
1089
1089
|
}
|
|
1090
1090
|
function buildMeetingTranscript(document, cacheData) {
|
|
1091
|
-
|
|
1091
|
+
const rawSegments = cacheData?.transcripts[document.id] ?? document.transcriptSegments ?? [];
|
|
1092
|
+
if (!(Boolean(cacheData) || Array.isArray(document.transcriptSegments))) return {
|
|
1092
1093
|
loaded: false,
|
|
1093
1094
|
segmentCount: 0,
|
|
1094
1095
|
transcript: null,
|
|
1095
1096
|
transcriptRecord: null,
|
|
1096
1097
|
transcriptText: null
|
|
1097
1098
|
};
|
|
1098
|
-
const rawSegments = cacheData.transcripts[document.id] ?? [];
|
|
1099
1099
|
const normalisedSegments = normaliseTranscriptSegments(rawSegments);
|
|
1100
1100
|
if (normalisedSegments.length === 0) return {
|
|
1101
1101
|
loaded: true,
|
|
@@ -1383,7 +1383,7 @@ function renderGranolaTuiMeetingTab(bundle, tab) {
|
|
|
1383
1383
|
}
|
|
1384
1384
|
}
|
|
1385
1385
|
function buildGranolaTuiSummary(state, meetingSource) {
|
|
1386
|
-
return `auth ${state.auth.mode === "stored-session" ? "stored" : "supabase"} | ${state.documents.loaded ? `${state.documents.count} docs` : "docs pending"} | ${state.folders.loaded ? `${state.folders.count} folders` : "folders pending"} | ${state.cache.loaded ? `${state.cache.transcriptCount} transcript sets` : state.cache.configured ? "cache configured" : "cache missing"} | ${state.index.loaded ? `${state.index.meetingCount} indexed` : "index pending"} | ${state.sync.running ? "sync running" : state.sync.lastError ? "sync error" : state.sync.lastCompletedAt ? `sync ${state.sync.lastCompletedAt.slice(11, 16)}` : "sync idle"} | list ${meetingSource}`;
|
|
1386
|
+
return `auth ${state.auth.mode === "api-key" ? "key" : state.auth.mode === "stored-session" ? "stored" : "supabase"} | ${state.documents.loaded ? `${state.documents.count} docs` : "docs pending"} | ${state.folders.loaded ? `${state.folders.count} folders` : "folders pending"} | ${state.cache.loaded ? `${state.cache.transcriptCount} transcript sets` : state.cache.configured ? "cache configured" : "cache missing"} | ${state.index.loaded ? `${state.index.meetingCount} indexed` : "index pending"} | ${state.sync.running ? "sync running" : state.sync.lastError ? "sync error" : state.sync.lastCompletedAt ? `sync ${state.sync.lastCompletedAt.slice(11, 16)}` : "sync idle"} | list ${meetingSource}`;
|
|
1387
1387
|
}
|
|
1388
1388
|
//#endregion
|
|
1389
1389
|
//#region src/tui/theme.ts
|
|
@@ -1429,13 +1429,16 @@ function actionDisabledReason(auth, actionId) {
|
|
|
1429
1429
|
case "refresh":
|
|
1430
1430
|
if (!auth.storedSessionAvailable) return "stored session missing";
|
|
1431
1431
|
return auth.refreshAvailable ? "" : "refresh unavailable";
|
|
1432
|
+
case "use-api-key":
|
|
1433
|
+
if (!auth.apiKeyAvailable) return "API key missing";
|
|
1434
|
+
return auth.mode === "api-key" ? "already active" : "";
|
|
1432
1435
|
case "use-stored":
|
|
1433
1436
|
if (!auth.storedSessionAvailable) return "stored session missing";
|
|
1434
1437
|
return auth.mode === "stored-session" ? "already active" : "";
|
|
1435
1438
|
case "use-supabase":
|
|
1436
1439
|
if (!auth.supabaseAvailable) return "supabase.json unavailable";
|
|
1437
1440
|
return auth.mode === "supabase-file" ? "already active" : "";
|
|
1438
|
-
case "logout": return auth.storedSessionAvailable ? "" : "stored
|
|
1441
|
+
case "logout": return auth.apiKeyAvailable || auth.storedSessionAvailable ? "" : "no stored credentials";
|
|
1439
1442
|
}
|
|
1440
1443
|
}
|
|
1441
1444
|
function buildGranolaTuiAuthActions(auth) {
|
|
@@ -1452,22 +1455,28 @@ function buildGranolaTuiAuthActions(auth) {
|
|
|
1452
1455
|
key: "2",
|
|
1453
1456
|
label: "Refresh stored session"
|
|
1454
1457
|
},
|
|
1458
|
+
{
|
|
1459
|
+
description: "Switch the active auth source to the stored API key",
|
|
1460
|
+
id: "use-api-key",
|
|
1461
|
+
key: "3",
|
|
1462
|
+
label: "Use API key"
|
|
1463
|
+
},
|
|
1455
1464
|
{
|
|
1456
1465
|
description: "Switch the active auth source to the stored session",
|
|
1457
1466
|
id: "use-stored",
|
|
1458
|
-
key: "
|
|
1467
|
+
key: "4",
|
|
1459
1468
|
label: "Use stored session"
|
|
1460
1469
|
},
|
|
1461
1470
|
{
|
|
1462
1471
|
description: "Switch the active auth source to supabase.json",
|
|
1463
1472
|
id: "use-supabase",
|
|
1464
|
-
key: "
|
|
1473
|
+
key: "5",
|
|
1465
1474
|
label: "Use supabase.json"
|
|
1466
1475
|
},
|
|
1467
1476
|
{
|
|
1468
|
-
description: "Delete
|
|
1477
|
+
description: "Delete stored credentials and fall back to configured sources",
|
|
1469
1478
|
id: "logout",
|
|
1470
|
-
key: "
|
|
1479
|
+
key: "6",
|
|
1471
1480
|
label: "Sign out"
|
|
1472
1481
|
}
|
|
1473
1482
|
].map((action) => {
|
|
@@ -1481,7 +1490,8 @@ function buildGranolaTuiAuthActions(auth) {
|
|
|
1481
1490
|
}
|
|
1482
1491
|
function renderGranolaTuiAuthState(auth) {
|
|
1483
1492
|
const lines = [
|
|
1484
|
-
`Active source: ${auth.mode === "stored-session" ? "Stored session" : "supabase.json"}`,
|
|
1493
|
+
`Active source: ${auth.mode === "api-key" ? "API key" : auth.mode === "stored-session" ? "Stored session" : "supabase.json"}`,
|
|
1494
|
+
`API key: ${auth.apiKeyAvailable ? "available" : "missing"}`,
|
|
1485
1495
|
`Stored session: ${auth.storedSessionAvailable ? "available" : "missing"}`,
|
|
1486
1496
|
`supabase.json: ${auth.supabaseAvailable ? "available" : "missing"}`,
|
|
1487
1497
|
`Refresh: ${auth.refreshAvailable ? "available" : "missing"}`
|
|
@@ -1977,6 +1987,11 @@ var GranolaTuiWorkspace = class {
|
|
|
1977
1987
|
await this.app.refreshAuth();
|
|
1978
1988
|
successMessage = "Stored session refreshed";
|
|
1979
1989
|
break;
|
|
1990
|
+
case "use-api-key":
|
|
1991
|
+
this.setStatus("Switching to stored API key…");
|
|
1992
|
+
await this.app.switchAuthMode("api-key");
|
|
1993
|
+
successMessage = "Using stored API key";
|
|
1994
|
+
break;
|
|
1980
1995
|
case "use-stored":
|
|
1981
1996
|
this.setStatus("Switching to stored session…");
|
|
1982
1997
|
await this.app.switchAuthMode("stored-session");
|
|
@@ -1990,7 +2005,7 @@ var GranolaTuiWorkspace = class {
|
|
|
1990
2005
|
case "logout":
|
|
1991
2006
|
this.setStatus("Signing out…");
|
|
1992
2007
|
await this.app.logoutAuth();
|
|
1993
|
-
successMessage = "Stored
|
|
2008
|
+
successMessage = "Stored credentials removed";
|
|
1994
2009
|
break;
|
|
1995
2010
|
}
|
|
1996
2011
|
await this.reloadAfterAuthChange();
|
|
@@ -2388,6 +2403,7 @@ function defaultGranolaToolkitPersistenceLayout(options = {}) {
|
|
|
2388
2403
|
const targetPlatform = options.platform ?? platform();
|
|
2389
2404
|
const dataDirectory = defaultGranolaToolkitDataDirectory(targetPlatform, options.homeDirectory ?? homedir());
|
|
2390
2405
|
return {
|
|
2406
|
+
apiKeyFile: join(dataDirectory, "api-key.txt"),
|
|
2391
2407
|
dataDirectory,
|
|
2392
2408
|
exportJobsFile: join(dataDirectory, "export-jobs.json"),
|
|
2393
2409
|
meetingIndexFile: join(dataDirectory, "meeting-index.json"),
|
|
@@ -2402,6 +2418,7 @@ function defaultGranolaToolkitPersistenceLayout(options = {}) {
|
|
|
2402
2418
|
const execFileAsync$1 = promisify(execFile);
|
|
2403
2419
|
const DEFAULT_CLIENT_ID = "client_GranolaMac";
|
|
2404
2420
|
const KEYCHAIN_SERVICE_NAME = "com.granola.toolkit";
|
|
2421
|
+
const KEYCHAIN_ACCOUNT_NAME_API_KEY = "api-key";
|
|
2405
2422
|
const KEYCHAIN_ACCOUNT_NAME = "session";
|
|
2406
2423
|
const WORKOS_AUTH_URL = "https://api.workos.com/user_management/authenticate";
|
|
2407
2424
|
function numberValue(value) {
|
|
@@ -2486,6 +2503,32 @@ var FileSessionStore = class {
|
|
|
2486
2503
|
});
|
|
2487
2504
|
}
|
|
2488
2505
|
};
|
|
2506
|
+
var FileApiKeyStore = class {
|
|
2507
|
+
constructor(filePath = defaultApiKeyFilePath()) {
|
|
2508
|
+
this.filePath = filePath;
|
|
2509
|
+
}
|
|
2510
|
+
async clearApiKey() {
|
|
2511
|
+
try {
|
|
2512
|
+
await unlink(this.filePath);
|
|
2513
|
+
} catch {}
|
|
2514
|
+
}
|
|
2515
|
+
async readApiKey() {
|
|
2516
|
+
try {
|
|
2517
|
+
return (await readFile(this.filePath, "utf8")).trim() || void 0;
|
|
2518
|
+
} catch {
|
|
2519
|
+
return;
|
|
2520
|
+
}
|
|
2521
|
+
}
|
|
2522
|
+
async writeApiKey(apiKey) {
|
|
2523
|
+
const trimmed = apiKey.trim();
|
|
2524
|
+
if (!trimmed) throw new Error("Granola API key is required");
|
|
2525
|
+
await mkdir(dirname(this.filePath), { recursive: true });
|
|
2526
|
+
await writeFile(this.filePath, `${trimmed}\n`, {
|
|
2527
|
+
encoding: "utf8",
|
|
2528
|
+
mode: 384
|
|
2529
|
+
});
|
|
2530
|
+
}
|
|
2531
|
+
};
|
|
2489
2532
|
var KeychainSessionStore = class {
|
|
2490
2533
|
async clearSession() {
|
|
2491
2534
|
try {
|
|
@@ -2527,6 +2570,48 @@ var KeychainSessionStore = class {
|
|
|
2527
2570
|
]);
|
|
2528
2571
|
}
|
|
2529
2572
|
};
|
|
2573
|
+
var KeychainApiKeyStore = class {
|
|
2574
|
+
async clearApiKey() {
|
|
2575
|
+
try {
|
|
2576
|
+
await execFileAsync$1("security", [
|
|
2577
|
+
"delete-generic-password",
|
|
2578
|
+
"-s",
|
|
2579
|
+
KEYCHAIN_SERVICE_NAME,
|
|
2580
|
+
"-a",
|
|
2581
|
+
KEYCHAIN_ACCOUNT_NAME_API_KEY
|
|
2582
|
+
]);
|
|
2583
|
+
} catch {}
|
|
2584
|
+
}
|
|
2585
|
+
async readApiKey() {
|
|
2586
|
+
try {
|
|
2587
|
+
const { stdout } = await execFileAsync$1("security", [
|
|
2588
|
+
"find-generic-password",
|
|
2589
|
+
"-s",
|
|
2590
|
+
KEYCHAIN_SERVICE_NAME,
|
|
2591
|
+
"-a",
|
|
2592
|
+
KEYCHAIN_ACCOUNT_NAME_API_KEY,
|
|
2593
|
+
"-w"
|
|
2594
|
+
]);
|
|
2595
|
+
return stdout.trim() || void 0;
|
|
2596
|
+
} catch {
|
|
2597
|
+
return;
|
|
2598
|
+
}
|
|
2599
|
+
}
|
|
2600
|
+
async writeApiKey(apiKey) {
|
|
2601
|
+
const trimmed = apiKey.trim();
|
|
2602
|
+
if (!trimmed) throw new Error("Granola API key is required");
|
|
2603
|
+
await execFileAsync$1("security", [
|
|
2604
|
+
"add-generic-password",
|
|
2605
|
+
"-U",
|
|
2606
|
+
"-s",
|
|
2607
|
+
KEYCHAIN_SERVICE_NAME,
|
|
2608
|
+
"-a",
|
|
2609
|
+
KEYCHAIN_ACCOUNT_NAME_API_KEY,
|
|
2610
|
+
"-w",
|
|
2611
|
+
trimmed
|
|
2612
|
+
]);
|
|
2613
|
+
}
|
|
2614
|
+
};
|
|
2530
2615
|
var CachedTokenProvider = class {
|
|
2531
2616
|
#token;
|
|
2532
2617
|
constructor(source, store = new NoopTokenStore()) {
|
|
@@ -2620,33 +2705,45 @@ async function refreshGranolaSession(session, fetchImpl = fetch) {
|
|
|
2620
2705
|
function defaultSessionFilePath() {
|
|
2621
2706
|
return defaultGranolaToolkitPersistenceLayout().sessionFile;
|
|
2622
2707
|
}
|
|
2708
|
+
function defaultApiKeyFilePath() {
|
|
2709
|
+
return defaultGranolaToolkitPersistenceLayout().apiKeyFile;
|
|
2710
|
+
}
|
|
2623
2711
|
function createDefaultSessionStore() {
|
|
2624
2712
|
return defaultGranolaToolkitPersistenceLayout().sessionStoreKind === "keychain" ? new KeychainSessionStore() : new FileSessionStore();
|
|
2625
2713
|
}
|
|
2714
|
+
function createDefaultApiKeyStore() {
|
|
2715
|
+
return defaultGranolaToolkitPersistenceLayout().sessionStoreKind === "keychain" ? new KeychainApiKeyStore() : new FileApiKeyStore();
|
|
2716
|
+
}
|
|
2626
2717
|
//#endregion
|
|
2627
2718
|
//#region src/client/default-auth.ts
|
|
2628
2719
|
function hasStoredSession(session) {
|
|
2629
2720
|
return Boolean(session?.accessToken.trim());
|
|
2630
2721
|
}
|
|
2631
2722
|
function resolveActiveMode(options) {
|
|
2723
|
+
if (options.preferredMode === "api-key" && options.apiKeyAvailable) return "api-key";
|
|
2632
2724
|
if (options.preferredMode === "stored-session" && options.storedSessionAvailable) return "stored-session";
|
|
2633
2725
|
if (options.preferredMode === "supabase-file" && options.supabaseAvailable) return "supabase-file";
|
|
2726
|
+
if (options.apiKeyAvailable) return "api-key";
|
|
2634
2727
|
if (options.storedSessionAvailable) return "stored-session";
|
|
2635
|
-
return "supabase-file";
|
|
2728
|
+
if (options.supabaseAvailable) return "supabase-file";
|
|
2729
|
+
return options.preferredMode ?? "api-key";
|
|
2636
2730
|
}
|
|
2637
2731
|
function missingSupabaseError() {
|
|
2638
2732
|
return /* @__PURE__ */ new Error(`supabase.json not found. Pass --supabase or create .granola.toml. Expected locations include: ${granolaSupabaseCandidates().join(", ")}`);
|
|
2639
2733
|
}
|
|
2640
2734
|
function buildDefaultGranolaAuthInfo(config, options = {}) {
|
|
2641
2735
|
const existsSyncImpl = options.existsSyncImpl ?? existsSync;
|
|
2736
|
+
const apiKeyAvailable = Boolean(options.apiKey?.trim());
|
|
2642
2737
|
const session = options.session;
|
|
2643
2738
|
const storedSessionAvailable = hasStoredSession(session);
|
|
2644
2739
|
const supabasePath = config.supabase || void 0;
|
|
2645
2740
|
const supabaseAvailable = Boolean(supabasePath && existsSyncImpl(supabasePath));
|
|
2646
2741
|
return {
|
|
2742
|
+
apiKeyAvailable,
|
|
2647
2743
|
clientId: session?.clientId,
|
|
2648
2744
|
lastError: options.lastError,
|
|
2649
2745
|
mode: resolveActiveMode({
|
|
2746
|
+
apiKeyAvailable,
|
|
2650
2747
|
preferredMode: options.preferredMode,
|
|
2651
2748
|
storedSessionAvailable,
|
|
2652
2749
|
supabaseAvailable
|
|
@@ -2659,9 +2756,12 @@ function buildDefaultGranolaAuthInfo(config, options = {}) {
|
|
|
2659
2756
|
};
|
|
2660
2757
|
}
|
|
2661
2758
|
async function inspectDefaultGranolaAuth(config, options = {}) {
|
|
2759
|
+
const apiKeyStore = options.apiKeyStore ?? createDefaultApiKeyStore();
|
|
2662
2760
|
const sessionStore = options.sessionStore ?? createDefaultSessionStore();
|
|
2761
|
+
const apiKey = options.apiKey ?? config.apiKey ?? await apiKeyStore.readApiKey();
|
|
2663
2762
|
const session = options.session ?? await sessionStore.readSession();
|
|
2664
2763
|
return buildDefaultGranolaAuthInfo(config, {
|
|
2764
|
+
apiKey,
|
|
2665
2765
|
existsSyncImpl: options.existsSyncImpl,
|
|
2666
2766
|
lastError: options.lastError,
|
|
2667
2767
|
preferredMode: options.preferredMode,
|
|
@@ -2678,6 +2778,12 @@ var DefaultAuthController = class {
|
|
|
2678
2778
|
sessionStore() {
|
|
2679
2779
|
return this.options.sessionStore ?? createDefaultSessionStore();
|
|
2680
2780
|
}
|
|
2781
|
+
apiKeyStore() {
|
|
2782
|
+
return this.options.apiKeyStore ?? createDefaultApiKeyStore();
|
|
2783
|
+
}
|
|
2784
|
+
async readApiKey() {
|
|
2785
|
+
return this.config.apiKey?.trim() || await this.apiKeyStore().readApiKey();
|
|
2786
|
+
}
|
|
2681
2787
|
readSession() {
|
|
2682
2788
|
return this.sessionStore().readSession();
|
|
2683
2789
|
}
|
|
@@ -2691,8 +2797,10 @@ var DefaultAuthController = class {
|
|
|
2691
2797
|
return this.options.sessionSourceFactory?.(supabasePath) ?? new SupabaseFileSessionSource(supabasePath);
|
|
2692
2798
|
}
|
|
2693
2799
|
async inspect() {
|
|
2800
|
+
const apiKey = await this.readApiKey();
|
|
2694
2801
|
const session = await this.readSession();
|
|
2695
2802
|
return buildDefaultGranolaAuthInfo(this.config, {
|
|
2803
|
+
apiKey,
|
|
2696
2804
|
existsSyncImpl: this.options.existsSyncImpl,
|
|
2697
2805
|
lastError: this.#lastError,
|
|
2698
2806
|
preferredMode: this.#preferredMode,
|
|
@@ -2700,6 +2808,13 @@ var DefaultAuthController = class {
|
|
|
2700
2808
|
});
|
|
2701
2809
|
}
|
|
2702
2810
|
async login(options = {}) {
|
|
2811
|
+
const apiKey = options.apiKey?.trim();
|
|
2812
|
+
if (apiKey) {
|
|
2813
|
+
await this.apiKeyStore().writeApiKey(apiKey);
|
|
2814
|
+
this.#lastError = void 0;
|
|
2815
|
+
this.#preferredMode = "api-key";
|
|
2816
|
+
return await this.inspect();
|
|
2817
|
+
}
|
|
2703
2818
|
const supabasePath = this.resolveSupabasePath(options.supabasePath);
|
|
2704
2819
|
const session = await this.sessionSource(supabasePath).loadSession();
|
|
2705
2820
|
await this.sessionStore().writeSession(session);
|
|
@@ -2708,6 +2823,7 @@ var DefaultAuthController = class {
|
|
|
2708
2823
|
return await this.inspect();
|
|
2709
2824
|
}
|
|
2710
2825
|
async logout() {
|
|
2826
|
+
await this.apiKeyStore().clearApiKey();
|
|
2711
2827
|
await this.sessionStore().clearSession();
|
|
2712
2828
|
this.#lastError = void 0;
|
|
2713
2829
|
this.#preferredMode = void 0;
|
|
@@ -2732,6 +2848,10 @@ var DefaultAuthController = class {
|
|
|
2732
2848
|
}
|
|
2733
2849
|
async switchMode(mode) {
|
|
2734
2850
|
const state = await this.inspect();
|
|
2851
|
+
if (mode === "api-key" && !state.apiKeyAvailable) {
|
|
2852
|
+
this.#lastError = "no Granola API key found";
|
|
2853
|
+
throw new Error(this.#lastError);
|
|
2854
|
+
}
|
|
2735
2855
|
if (mode === "stored-session" && !state.storedSessionAvailable) {
|
|
2736
2856
|
this.#lastError = "no stored Granola session found";
|
|
2737
2857
|
throw new Error(this.#lastError);
|
|
@@ -2796,6 +2916,63 @@ function parseDocument(value) {
|
|
|
2796
2916
|
updatedAt: stringValue(record.updated_at)
|
|
2797
2917
|
};
|
|
2798
2918
|
}
|
|
2919
|
+
function parseFolderMembership(value) {
|
|
2920
|
+
const record = asRecord(value);
|
|
2921
|
+
if (!record) return;
|
|
2922
|
+
const id = stringValue(record.id);
|
|
2923
|
+
const name = stringValue(record.name);
|
|
2924
|
+
if (!id || !name) return;
|
|
2925
|
+
return {
|
|
2926
|
+
id,
|
|
2927
|
+
name
|
|
2928
|
+
};
|
|
2929
|
+
}
|
|
2930
|
+
function parsePublicTranscriptSegment(documentId, value, index) {
|
|
2931
|
+
const record = asRecord(value);
|
|
2932
|
+
if (!record) return;
|
|
2933
|
+
const text = stringValue(record.text);
|
|
2934
|
+
const startTimestamp = stringValue(record.start_time);
|
|
2935
|
+
const endTimestamp = stringValue(record.end_time) || startTimestamp;
|
|
2936
|
+
if (!text || !startTimestamp) return;
|
|
2937
|
+
const speaker = asRecord(record.speaker);
|
|
2938
|
+
return {
|
|
2939
|
+
documentId,
|
|
2940
|
+
endTimestamp,
|
|
2941
|
+
id: stringValue(record.id) || `${documentId}:transcript:${index + 1}`,
|
|
2942
|
+
isFinal: true,
|
|
2943
|
+
source: stringValue(speaker?.name) || stringValue(speaker?.source) || "unknown",
|
|
2944
|
+
startTimestamp,
|
|
2945
|
+
text
|
|
2946
|
+
};
|
|
2947
|
+
}
|
|
2948
|
+
function parsePublicNoteSummary(value) {
|
|
2949
|
+
const record = asRecord(value);
|
|
2950
|
+
if (!record) throw new Error("public note payload is not an object");
|
|
2951
|
+
return {
|
|
2952
|
+
createdAt: stringValue(record.created_at),
|
|
2953
|
+
id: stringValue(record.id),
|
|
2954
|
+
title: stringValue(record.title),
|
|
2955
|
+
updatedAt: stringValue(record.updated_at)
|
|
2956
|
+
};
|
|
2957
|
+
}
|
|
2958
|
+
function parsePublicNote(value) {
|
|
2959
|
+
const record = asRecord(value);
|
|
2960
|
+
if (!record) throw new Error("public note payload is not an object");
|
|
2961
|
+
const id = stringValue(record.id);
|
|
2962
|
+
const summaryMarkdown = stringValue(record.summary_markdown);
|
|
2963
|
+
const summaryText = stringValue(record.summary_text);
|
|
2964
|
+
return {
|
|
2965
|
+
content: summaryMarkdown || summaryText,
|
|
2966
|
+
createdAt: stringValue(record.created_at),
|
|
2967
|
+
folderMemberships: Array.isArray(record.folder_membership) ? record.folder_membership.map(parseFolderMembership).filter((membership) => Boolean(membership)) : [],
|
|
2968
|
+
id,
|
|
2969
|
+
notesPlain: summaryText,
|
|
2970
|
+
tags: [],
|
|
2971
|
+
title: stringValue(record.title),
|
|
2972
|
+
transcriptSegments: Array.isArray(record.transcript) ? record.transcript.map((segment, index) => parsePublicTranscriptSegment(id, segment, index)).filter((segment) => Boolean(segment)) : [],
|
|
2973
|
+
updatedAt: stringValue(record.updated_at)
|
|
2974
|
+
};
|
|
2975
|
+
}
|
|
2799
2976
|
function parseFolderDocumentIds(value) {
|
|
2800
2977
|
if (!Array.isArray(value)) return [];
|
|
2801
2978
|
return value.map((item) => {
|
|
@@ -2899,6 +3076,98 @@ var GranolaApiClient = class {
|
|
|
2899
3076
|
}
|
|
2900
3077
|
};
|
|
2901
3078
|
//#endregion
|
|
3079
|
+
//#region src/client/granola-public.ts
|
|
3080
|
+
const PUBLIC_NOTES_URL = "https://public-api.granola.ai/v1/notes";
|
|
3081
|
+
const MAX_PAGE_SIZE = 30;
|
|
3082
|
+
const DETAIL_BATCH_SIZE = 5;
|
|
3083
|
+
function cloneDocument(document) {
|
|
3084
|
+
return {
|
|
3085
|
+
...document,
|
|
3086
|
+
folderMemberships: document.folderMemberships?.map((membership) => ({ ...membership })),
|
|
3087
|
+
tags: [...document.tags],
|
|
3088
|
+
transcriptSegments: document.transcriptSegments?.map((segment) => ({ ...segment }))
|
|
3089
|
+
};
|
|
3090
|
+
}
|
|
3091
|
+
async function mapInBatches(items, batchSize, mapper) {
|
|
3092
|
+
const results = [];
|
|
3093
|
+
for (let index = 0; index < items.length; index += batchSize) {
|
|
3094
|
+
const batch = items.slice(index, index + batchSize);
|
|
3095
|
+
results.push(...await Promise.all(batch.map((item) => mapper(item))));
|
|
3096
|
+
}
|
|
3097
|
+
return results;
|
|
3098
|
+
}
|
|
3099
|
+
var GranolaPublicApiClient = class {
|
|
3100
|
+
#documentsById = /* @__PURE__ */ new Map();
|
|
3101
|
+
constructor(httpClient) {
|
|
3102
|
+
this.httpClient = httpClient;
|
|
3103
|
+
}
|
|
3104
|
+
async listNoteSummaries(options) {
|
|
3105
|
+
const notes = [];
|
|
3106
|
+
const pageSize = Math.max(1, Math.min(options.limit ?? MAX_PAGE_SIZE, MAX_PAGE_SIZE));
|
|
3107
|
+
let cursor;
|
|
3108
|
+
for (;;) {
|
|
3109
|
+
const url = new URL(PUBLIC_NOTES_URL);
|
|
3110
|
+
url.searchParams.set("page_size", String(pageSize));
|
|
3111
|
+
if (cursor) url.searchParams.set("cursor", cursor);
|
|
3112
|
+
const response = await this.httpClient.request({
|
|
3113
|
+
headers: { Accept: "application/json" },
|
|
3114
|
+
timeoutMs: options.timeoutMs,
|
|
3115
|
+
url: url.toString()
|
|
3116
|
+
});
|
|
3117
|
+
if (!response.ok) {
|
|
3118
|
+
const body = (await response.text()).slice(0, 500);
|
|
3119
|
+
throw new Error(`failed to list notes: ${response.status} ${response.statusText}${body ? `: ${body}` : ""}`);
|
|
3120
|
+
}
|
|
3121
|
+
const payload = await response.json();
|
|
3122
|
+
if (!Array.isArray(payload.notes)) throw new Error("failed to parse public notes response");
|
|
3123
|
+
notes.push(...payload.notes.map(parsePublicNoteSummary));
|
|
3124
|
+
cursor = payload.cursor ?? void 0;
|
|
3125
|
+
if (!payload.hasMore || !cursor) break;
|
|
3126
|
+
}
|
|
3127
|
+
return notes;
|
|
3128
|
+
}
|
|
3129
|
+
async fetchNoteDetail(noteId, timeoutMs) {
|
|
3130
|
+
const url = new URL(`${PUBLIC_NOTES_URL}/${noteId}`);
|
|
3131
|
+
url.searchParams.append("include", "transcript");
|
|
3132
|
+
const response = await this.httpClient.request({
|
|
3133
|
+
headers: { Accept: "application/json" },
|
|
3134
|
+
timeoutMs,
|
|
3135
|
+
url: url.toString()
|
|
3136
|
+
});
|
|
3137
|
+
if (!response.ok) {
|
|
3138
|
+
const body = (await response.text()).slice(0, 500);
|
|
3139
|
+
throw new Error(`failed to get note ${noteId}: ${response.status} ${response.statusText}${body ? `: ${body}` : ""}`);
|
|
3140
|
+
}
|
|
3141
|
+
const document = parsePublicNote(await response.json());
|
|
3142
|
+
this.#documentsById.set(document.id, cloneDocument(document));
|
|
3143
|
+
return document;
|
|
3144
|
+
}
|
|
3145
|
+
async listDocuments(options) {
|
|
3146
|
+
const summaries = await this.listNoteSummaries(options);
|
|
3147
|
+
const nextDocuments = [];
|
|
3148
|
+
const toFetch = [];
|
|
3149
|
+
for (const summary of summaries) {
|
|
3150
|
+
const cached = this.#documentsById.get(summary.id);
|
|
3151
|
+
if (cached && cached.updatedAt === summary.updatedAt) {
|
|
3152
|
+
nextDocuments.push(cloneDocument(cached));
|
|
3153
|
+
continue;
|
|
3154
|
+
}
|
|
3155
|
+
toFetch.push(summary);
|
|
3156
|
+
}
|
|
3157
|
+
const fetchedDocuments = await mapInBatches(toFetch, DETAIL_BATCH_SIZE, async (summary) => await this.fetchNoteDetail(summary.id, options.timeoutMs));
|
|
3158
|
+
const fetchedById = new Map(fetchedDocuments.map((document) => [document.id, document]));
|
|
3159
|
+
const ids = /* @__PURE__ */ new Set();
|
|
3160
|
+
for (const summary of summaries) {
|
|
3161
|
+
const document = fetchedById.get(summary.id) ?? this.#documentsById.get(summary.id);
|
|
3162
|
+
if (!document) throw new Error(`failed to load note detail: ${summary.id}`);
|
|
3163
|
+
ids.add(summary.id);
|
|
3164
|
+
nextDocuments.push(cloneDocument(document));
|
|
3165
|
+
}
|
|
3166
|
+
for (const cachedId of this.#documentsById.keys()) if (!ids.has(cachedId)) this.#documentsById.delete(cachedId);
|
|
3167
|
+
return nextDocuments;
|
|
3168
|
+
}
|
|
3169
|
+
};
|
|
3170
|
+
//#endregion
|
|
2902
3171
|
//#region src/client/http.ts
|
|
2903
3172
|
const RETRYABLE_STATUS_CODES = new Set([
|
|
2904
3173
|
429,
|
|
@@ -2993,8 +3262,23 @@ var AuthenticatedHttpClient = class {
|
|
|
2993
3262
|
//#region src/client/default.ts
|
|
2994
3263
|
async function createDefaultGranolaRuntime(config, logger = console, options = {}) {
|
|
2995
3264
|
const auth = await inspectDefaultGranolaAuth(config, { preferredMode: options.preferredMode });
|
|
2996
|
-
if (!auth.storedSessionAvailable && !config.supabase) throw new Error(`
|
|
2997
|
-
if (
|
|
3265
|
+
if (!auth.apiKeyAvailable && !auth.storedSessionAvailable && !config.supabase) throw new Error(`Granola credentials not found. Set --api-key or GRANOLA_API_KEY, use granola auth login --api-key, or fall back to --supabase. Expected supabase locations include: ${granolaSupabaseCandidates().join(", ")}`);
|
|
3266
|
+
if (config.supabase && !existsSync(config.supabase) && !auth.apiKeyAvailable && !auth.storedSessionAvailable) throw new Error(`supabase.json not found: ${config.supabase}`);
|
|
3267
|
+
if (auth.mode !== "api-key" && !auth.storedSessionAvailable && config.supabase && !existsSync(config.supabase)) throw new Error(`supabase.json not found: ${config.supabase}`);
|
|
3268
|
+
if (auth.mode === "api-key") {
|
|
3269
|
+
const apiKeyStore = createDefaultApiKeyStore();
|
|
3270
|
+
const apiKey = config.apiKey?.trim() || await apiKeyStore.readApiKey();
|
|
3271
|
+
if (!apiKey) throw new Error("Granola API key not found. Set --api-key or GRANOLA_API_KEY, or run granola auth login --api-key <token>.");
|
|
3272
|
+
return {
|
|
3273
|
+
auth,
|
|
3274
|
+
client: new GranolaPublicApiClient(new AuthenticatedHttpClient({
|
|
3275
|
+
logger,
|
|
3276
|
+
tokenProvider: new CachedTokenProvider({ async loadAccessToken() {
|
|
3277
|
+
return apiKey;
|
|
3278
|
+
} })
|
|
3279
|
+
}))
|
|
3280
|
+
};
|
|
3281
|
+
}
|
|
2998
3282
|
const sessionStore = createDefaultSessionStore();
|
|
2999
3283
|
return {
|
|
3000
3284
|
auth,
|
|
@@ -3763,6 +4047,28 @@ var GranolaApp = class {
|
|
|
3763
4047
|
for (const [documentId, summaries] of byDocumentId.entries()) byDocumentId.set(documentId, summaries.slice().sort((left, right) => left.name.localeCompare(right.name)).map((folder) => cloneFolderSummary(folder)));
|
|
3764
4048
|
return byDocumentId;
|
|
3765
4049
|
}
|
|
4050
|
+
deriveFoldersFromDocuments(documents) {
|
|
4051
|
+
const byFolderId = /* @__PURE__ */ new Map();
|
|
4052
|
+
for (const document of documents) for (const membership of document.folderMemberships ?? []) {
|
|
4053
|
+
const existing = byFolderId.get(membership.id);
|
|
4054
|
+
if (existing) {
|
|
4055
|
+
existing.documentIds = [...new Set([...existing.documentIds, document.id])];
|
|
4056
|
+
existing.updatedAt = existing.updatedAt.localeCompare(document.updatedAt) >= 0 ? existing.updatedAt : document.updatedAt;
|
|
4057
|
+
if (!existing.createdAt || existing.createdAt.localeCompare(document.createdAt) > 0) existing.createdAt = document.createdAt;
|
|
4058
|
+
continue;
|
|
4059
|
+
}
|
|
4060
|
+
byFolderId.set(membership.id, {
|
|
4061
|
+
createdAt: document.createdAt,
|
|
4062
|
+
documentIds: [document.id],
|
|
4063
|
+
id: membership.id,
|
|
4064
|
+
isFavourite: false,
|
|
4065
|
+
name: membership.name,
|
|
4066
|
+
updatedAt: document.updatedAt
|
|
4067
|
+
});
|
|
4068
|
+
}
|
|
4069
|
+
if (byFolderId.size === 0) return;
|
|
4070
|
+
return [...byFolderId.values()].sort((left, right) => left.name.localeCompare(right.name));
|
|
4071
|
+
}
|
|
3766
4072
|
async loadFolders(options = {}) {
|
|
3767
4073
|
if (options.forceRefresh) {
|
|
3768
4074
|
this.resetFoldersState();
|
|
@@ -3774,6 +4080,24 @@ var GranolaApp = class {
|
|
|
3774
4080
|
}));
|
|
3775
4081
|
const client = await this.getGranolaClient();
|
|
3776
4082
|
if (!client.listFolders) {
|
|
4083
|
+
const documents = await this.listDocuments({ forceRefresh: options.forceRefresh });
|
|
4084
|
+
const folders = this.deriveFoldersFromDocuments(documents);
|
|
4085
|
+
if (folders) {
|
|
4086
|
+
this.#folders = folders.map((folder) => ({
|
|
4087
|
+
...folder,
|
|
4088
|
+
documentIds: [...folder.documentIds]
|
|
4089
|
+
}));
|
|
4090
|
+
this.#state.folders = {
|
|
4091
|
+
count: folders.length,
|
|
4092
|
+
loaded: true,
|
|
4093
|
+
loadedAt: this.nowIso()
|
|
4094
|
+
};
|
|
4095
|
+
this.emitStateUpdate();
|
|
4096
|
+
return this.#folders.map((folder) => ({
|
|
4097
|
+
...folder,
|
|
4098
|
+
documentIds: [...folder.documentIds]
|
|
4099
|
+
}));
|
|
4100
|
+
}
|
|
3777
4101
|
if (options.required) throw new Error("Granola folder API is not configured");
|
|
3778
4102
|
return;
|
|
3779
4103
|
}
|
|
@@ -4368,6 +4692,7 @@ async function loadConfig(options) {
|
|
|
4368
4692
|
const defaultCache = firstExistingPath(granolaCacheCandidates());
|
|
4369
4693
|
const timeoutValue = pickString(options.subcommandFlags.timeout) ?? pickString(env.TIMEOUT) ?? pickString(configValues.timeout) ?? "2m";
|
|
4370
4694
|
return {
|
|
4695
|
+
apiKey: pickString(options.globalFlags["api-key"]) ?? pickString(env.GRANOLA_API_KEY) ?? pickString(configValues["api-key"]) ?? pickString(configValues.apiKey),
|
|
4371
4696
|
configFileUsed: config.path,
|
|
4372
4697
|
debug: pickBoolean(options.globalFlags.debug) ?? envFlag(env.DEBUG_MODE) ?? pickBoolean(configValues.debug) ?? false,
|
|
4373
4698
|
notes: {
|
|
@@ -4455,13 +4780,15 @@ Usage:
|
|
|
4455
4780
|
|
|
4456
4781
|
Subcommands:
|
|
4457
4782
|
login Import credentials from the Granola desktop app
|
|
4783
|
+
Or store a Granola API key with --api-key
|
|
4458
4784
|
status Show the current Granola auth state
|
|
4459
|
-
logout Delete
|
|
4785
|
+
logout Delete stored Granola credentials
|
|
4460
4786
|
refresh Refresh the stored Granola session
|
|
4461
|
-
use <stored|supabase>
|
|
4787
|
+
use <api-key|stored|supabase>
|
|
4462
4788
|
Switch the active auth source for this toolkit instance
|
|
4463
4789
|
|
|
4464
4790
|
Options:
|
|
4791
|
+
--api-key <token> Store a Granola Personal API key
|
|
4465
4792
|
--supabase <path> Path to supabase.json for auth login
|
|
4466
4793
|
--config <path> Path to .granola.toml
|
|
4467
4794
|
--debug Enable debug logging
|
|
@@ -4469,10 +4796,15 @@ Options:
|
|
|
4469
4796
|
`;
|
|
4470
4797
|
}
|
|
4471
4798
|
function formatAuthSource(mode) {
|
|
4472
|
-
|
|
4799
|
+
switch (mode) {
|
|
4800
|
+
case "api-key": return "API key";
|
|
4801
|
+
case "stored-session": return "stored session";
|
|
4802
|
+
default: return "supabase.json";
|
|
4803
|
+
}
|
|
4473
4804
|
}
|
|
4474
4805
|
function printAuthState(state) {
|
|
4475
4806
|
console.log(`Active source: ${formatAuthSource(state.mode)}`);
|
|
4807
|
+
console.log(`API key: ${state.apiKeyAvailable ? "available" : "missing"}`);
|
|
4476
4808
|
console.log(`Stored session: ${state.storedSessionAvailable ? "available" : "missing"}`);
|
|
4477
4809
|
console.log(`supabase.json: ${state.supabaseAvailable ? "available" : "missing"}`);
|
|
4478
4810
|
if (state.supabasePath) console.log(`supabase path: ${state.supabasePath}`);
|
|
@@ -4483,10 +4815,13 @@ function printAuthState(state) {
|
|
|
4483
4815
|
}
|
|
4484
4816
|
const authCommand = {
|
|
4485
4817
|
description: "Manage stored Granola sessions",
|
|
4486
|
-
flags: {
|
|
4818
|
+
flags: {
|
|
4819
|
+
"api-key": { type: "string" },
|
|
4820
|
+
help: { type: "boolean" }
|
|
4821
|
+
},
|
|
4487
4822
|
help: authHelp,
|
|
4488
4823
|
name: "auth",
|
|
4489
|
-
async run({ commandArgs, globalFlags }) {
|
|
4824
|
+
async run({ commandArgs, commandFlags, globalFlags }) {
|
|
4490
4825
|
const [action, value] = commandArgs;
|
|
4491
4826
|
const config = await loadConfig({
|
|
4492
4827
|
globalFlags,
|
|
@@ -4497,14 +4832,14 @@ const authCommand = {
|
|
|
4497
4832
|
const app = await createGranolaApp(config);
|
|
4498
4833
|
switch (action) {
|
|
4499
4834
|
case "login": {
|
|
4500
|
-
const state = await app.loginAuth();
|
|
4501
|
-
console.log(`Imported Granola session from ${state.supabasePath ?? "desktop app defaults"}`);
|
|
4835
|
+
const state = await app.loginAuth({ apiKey: typeof commandFlags["api-key"] === "string" ? commandFlags["api-key"] : void 0 });
|
|
4836
|
+
console.log(typeof commandFlags["api-key"] === "string" ? "Stored Granola API key" : `Imported Granola session from ${state.supabasePath ?? "desktop app defaults"}`);
|
|
4502
4837
|
printAuthState(state);
|
|
4503
4838
|
return 0;
|
|
4504
4839
|
}
|
|
4505
4840
|
case "logout": {
|
|
4506
4841
|
const state = await app.logoutAuth();
|
|
4507
|
-
console.log("Stored Granola
|
|
4842
|
+
console.log("Stored Granola credentials deleted");
|
|
4508
4843
|
printAuthState(state);
|
|
4509
4844
|
return 0;
|
|
4510
4845
|
}
|
|
@@ -4517,7 +4852,7 @@ const authCommand = {
|
|
|
4517
4852
|
case "status": {
|
|
4518
4853
|
const state = await app.inspectAuth();
|
|
4519
4854
|
printAuthState(state);
|
|
4520
|
-
return state.storedSessionAvailable ? 0 : 1;
|
|
4855
|
+
return state.apiKeyAvailable || state.storedSessionAvailable || state.supabaseAvailable ? 0 : 1;
|
|
4521
4856
|
}
|
|
4522
4857
|
case "use": {
|
|
4523
4858
|
const mode = resolveAuthMode(value);
|
|
@@ -4535,11 +4870,13 @@ const authCommand = {
|
|
|
4535
4870
|
};
|
|
4536
4871
|
function resolveAuthMode(value) {
|
|
4537
4872
|
switch (value) {
|
|
4873
|
+
case "api":
|
|
4874
|
+
case "api-key": return "api-key";
|
|
4538
4875
|
case "stored":
|
|
4539
4876
|
case "stored-session": return "stored-session";
|
|
4540
4877
|
case "supabase":
|
|
4541
4878
|
case "supabase-file": return "supabase-file";
|
|
4542
|
-
default: throw new Error("invalid auth mode: expected stored or supabase");
|
|
4879
|
+
default: throw new Error("invalid auth mode: expected api-key, stored, or supabase");
|
|
4543
4880
|
}
|
|
4544
4881
|
}
|
|
4545
4882
|
//#endregion
|
|
@@ -4983,7 +5320,12 @@ function renderAppState() {
|
|
|
4983
5320
|
}
|
|
4984
5321
|
|
|
4985
5322
|
const appState = state.appState;
|
|
4986
|
-
const authMode =
|
|
5323
|
+
const authMode =
|
|
5324
|
+
appState.auth.mode === "api-key"
|
|
5325
|
+
? "API key"
|
|
5326
|
+
: appState.auth.mode === "stored-session"
|
|
5327
|
+
? "Stored session"
|
|
5328
|
+
: "supabase.json";
|
|
4987
5329
|
const docs = appState.documents.loaded ? String(appState.documents.count) : "not loaded";
|
|
4988
5330
|
const cache = appState.cache.loaded
|
|
4989
5331
|
? appState.cache.transcriptCount + " transcript sets"
|
|
@@ -5103,7 +5445,12 @@ function renderAuthPanel() {
|
|
|
5103
5445
|
return;
|
|
5104
5446
|
}
|
|
5105
5447
|
|
|
5106
|
-
const activeSource =
|
|
5448
|
+
const activeSource =
|
|
5449
|
+
auth.mode === "api-key"
|
|
5450
|
+
? "API key"
|
|
5451
|
+
: auth.mode === "stored-session"
|
|
5452
|
+
? "Stored session"
|
|
5453
|
+
: "supabase.json";
|
|
5107
5454
|
const lastError = auth.lastError
|
|
5108
5455
|
? '<div class="auth-card__meta auth-card__error">' + escapeHtml(auth.lastError) + "</div>"
|
|
5109
5456
|
: "";
|
|
@@ -5112,6 +5459,7 @@ function renderAuthPanel() {
|
|
|
5112
5459
|
'<div class="auth-card">',
|
|
5113
5460
|
'<div class="status-grid">',
|
|
5114
5461
|
'<div><span class="status-label">Active</span><strong>' + escapeHtml(activeSource) + "</strong></div>",
|
|
5462
|
+
'<div><span class="status-label">API key</span><strong>' + escapeHtml(auth.apiKeyAvailable ? "available" : "missing") + "</strong></div>",
|
|
5115
5463
|
'<div><span class="status-label">Stored</span><strong>' + escapeHtml(auth.storedSessionAvailable ? "available" : "missing") + "</strong></div>",
|
|
5116
5464
|
'<div><span class="status-label">supabase.json</span><strong>' + escapeHtml(auth.supabaseAvailable ? "available" : "missing") + "</strong></div>",
|
|
5117
5465
|
'<div><span class="status-label">Refresh</span><strong>' + escapeHtml(auth.refreshAvailable ? "available" : "missing") + "</strong></div>",
|
|
@@ -5126,12 +5474,16 @@ function renderAuthPanel() {
|
|
|
5126
5474
|
? '<div class="auth-card__meta">supabase path: ' + escapeHtml(auth.supabasePath) + "</div>"
|
|
5127
5475
|
: "",
|
|
5128
5476
|
lastError,
|
|
5477
|
+
'<div class="auth-card__meta">Store a Granola Personal API key here or use <code>granola auth login --api-key <token></code>.</div>',
|
|
5129
5478
|
'<div class="auth-card__actions">',
|
|
5479
|
+
'<input class="input" type="password" placeholder="grn_..." data-auth-api-key />',
|
|
5480
|
+
authActionButton("Save API key", "login-api-key", false),
|
|
5130
5481
|
authActionButton("Import desktop session", "login", !auth.supabaseAvailable),
|
|
5131
5482
|
authActionButton("Refresh stored session", "refresh", !auth.storedSessionAvailable || !auth.refreshAvailable),
|
|
5483
|
+
authModeButton("Use API key", "api-key", !auth.apiKeyAvailable || auth.mode === "api-key"),
|
|
5132
5484
|
authModeButton("Use stored session", "stored-session", !auth.storedSessionAvailable || auth.mode === "stored-session"),
|
|
5133
5485
|
authModeButton("Use supabase.json", "supabase-file", !auth.supabaseAvailable || auth.mode === "supabase-file"),
|
|
5134
|
-
authActionButton("Sign out", "logout", !auth.storedSessionAvailable),
|
|
5486
|
+
authActionButton("Sign out", "logout", !auth.apiKeyAvailable && !auth.storedSessionAvailable),
|
|
5135
5487
|
"</div>",
|
|
5136
5488
|
"</div>",
|
|
5137
5489
|
].join("");
|
|
@@ -5521,14 +5873,19 @@ async function rerunJob(id) {
|
|
|
5521
5873
|
}
|
|
5522
5874
|
}
|
|
5523
5875
|
|
|
5524
|
-
async function loginAuth() {
|
|
5525
|
-
setStatus("Importing desktop session…", "busy");
|
|
5876
|
+
async function loginAuth(apiKey) {
|
|
5877
|
+
setStatus(apiKey ? "Saving API key…" : "Importing desktop session…", "busy");
|
|
5526
5878
|
try {
|
|
5527
|
-
|
|
5879
|
+
const body = apiKey ? { apiKey } : undefined;
|
|
5880
|
+
await fetchJson("/auth/login", {
|
|
5881
|
+
body: body ? JSON.stringify(body) : undefined,
|
|
5882
|
+
headers: body ? { "content-type": "application/json" } : undefined,
|
|
5883
|
+
method: "POST",
|
|
5884
|
+
});
|
|
5528
5885
|
await refreshAll();
|
|
5529
5886
|
} catch (error) {
|
|
5530
5887
|
await syncAuthState();
|
|
5531
|
-
setStatus("Auth import failed", "error");
|
|
5888
|
+
setStatus(apiKey ? "API key save failed" : "Auth import failed", "error");
|
|
5532
5889
|
state.detailError = error instanceof Error ? error.message : String(error);
|
|
5533
5890
|
renderMeetingDetail();
|
|
5534
5891
|
}
|
|
@@ -5675,6 +6032,17 @@ els.authPanel.addEventListener("click", (event) => {
|
|
|
5675
6032
|
const actionButton = event.target.closest("[data-auth-action]");
|
|
5676
6033
|
if (actionButton) {
|
|
5677
6034
|
switch (actionButton.dataset.authAction) {
|
|
6035
|
+
case "login-api-key": {
|
|
6036
|
+
const apiKeyInput = els.authPanel.querySelector("[data-auth-api-key]");
|
|
6037
|
+
const apiKey =
|
|
6038
|
+
apiKeyInput && "value" in apiKeyInput ? String(apiKeyInput.value || "").trim() : "";
|
|
6039
|
+
if (!apiKey) {
|
|
6040
|
+
setStatus("Enter a Granola API key", "error");
|
|
6041
|
+
return;
|
|
6042
|
+
}
|
|
6043
|
+
void loginAuth(apiKey);
|
|
6044
|
+
return;
|
|
6045
|
+
}
|
|
5678
6046
|
case "login":
|
|
5679
6047
|
void loginAuth();
|
|
5680
6048
|
return;
|
|
@@ -6542,9 +6910,10 @@ function parseMeetingSort(value) {
|
|
|
6542
6910
|
}
|
|
6543
6911
|
function parseAuthMode(value) {
|
|
6544
6912
|
switch (value) {
|
|
6913
|
+
case "api-key":
|
|
6545
6914
|
case "stored-session":
|
|
6546
6915
|
case "supabase-file": return value;
|
|
6547
|
-
default: throw new Error("invalid auth mode: expected stored-session or supabase-file");
|
|
6916
|
+
default: throw new Error("invalid auth mode: expected api-key, stored-session, or supabase-file");
|
|
6548
6917
|
}
|
|
6549
6918
|
}
|
|
6550
6919
|
function folderIdFromBody(value) {
|
|
@@ -6891,8 +7260,12 @@ async function startGranolaServer(app, options = {}) {
|
|
|
6891
7260
|
}
|
|
6892
7261
|
if (method === "POST" && path === granolaTransportPaths.authLogin) {
|
|
6893
7262
|
const body = await readJsonBody(request);
|
|
7263
|
+
const apiKey = typeof body.apiKey === "string" && body.apiKey.trim() ? body.apiKey.trim() : void 0;
|
|
6894
7264
|
const supabasePath = typeof body.supabasePath === "string" && body.supabasePath.trim() ? body.supabasePath.trim() : void 0;
|
|
6895
|
-
sendJson(response, await app.loginAuth({
|
|
7265
|
+
sendJson(response, await app.loginAuth({
|
|
7266
|
+
apiKey,
|
|
7267
|
+
supabasePath
|
|
7268
|
+
}), { headers: originHeaders });
|
|
6896
7269
|
return;
|
|
6897
7270
|
}
|
|
6898
7271
|
if (method === "POST" && path === granolaTransportPaths.authLogout) {
|
|
@@ -7911,6 +8284,7 @@ Commands:
|
|
|
7911
8284
|
${commands.map((command) => ` ${command.name.padEnd(commandWidth)} ${command.description}`).join("\n")}
|
|
7912
8285
|
|
|
7913
8286
|
Global options:
|
|
8287
|
+
--api-key <token> Granola Personal API key
|
|
7914
8288
|
--config <path> Path to .granola.toml
|
|
7915
8289
|
--debug Enable debug logging
|
|
7916
8290
|
--supabase <path> Path to supabase.json
|
|
@@ -7928,6 +8302,7 @@ async function runCli(argv) {
|
|
|
7928
8302
|
try {
|
|
7929
8303
|
const { command, rest } = splitCommand(argv);
|
|
7930
8304
|
const global = parseFlags(rest, {
|
|
8305
|
+
"api-key": { type: "string" },
|
|
7931
8306
|
config: { type: "string" },
|
|
7932
8307
|
debug: { type: "boolean" },
|
|
7933
8308
|
help: { type: "boolean" },
|