granola-toolkit 0.36.0 → 0.38.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 +516 -40
- 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
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { Input, ProcessTerminal, TUI, matchesKey, truncateToWidth, visibleWidth, wrapTextWithAnsi } from "@mariozechner/pi-tui";
|
|
3
3
|
import { createHash, randomUUID } from "node:crypto";
|
|
4
|
-
import { mkdir, readFile, rm, stat, unlink, writeFile } from "node:fs/promises";
|
|
4
|
+
import { appendFile, mkdir, readFile, rm, stat, unlink, writeFile } from "node:fs/promises";
|
|
5
5
|
import { dirname, join } from "node:path";
|
|
6
6
|
import { existsSync } from "node:fs";
|
|
7
7
|
import { homedir, platform } from "node:os";
|
|
@@ -30,6 +30,7 @@ const granolaTransportPaths = {
|
|
|
30
30
|
root: "/",
|
|
31
31
|
serverInfo: "/server/info",
|
|
32
32
|
syncRun: "/sync",
|
|
33
|
+
syncEvents: "/sync/events",
|
|
33
34
|
state: "/state"
|
|
34
35
|
};
|
|
35
36
|
function appendSearchParams(path, params) {
|
|
@@ -181,6 +182,10 @@ var GranolaServerClient = class GranolaServerClient {
|
|
|
181
182
|
async inspectSync() {
|
|
182
183
|
return cloneValue(this.#state.sync);
|
|
183
184
|
}
|
|
185
|
+
async listSyncEvents(options = {}) {
|
|
186
|
+
const path = options.limit ? `${granolaTransportPaths.syncEvents}?limit=${encodeURIComponent(String(options.limit))}` : granolaTransportPaths.syncEvents;
|
|
187
|
+
return await this.requestJson(path);
|
|
188
|
+
}
|
|
184
189
|
async loginAuth(options = {}) {
|
|
185
190
|
return await this.requestJson(granolaTransportPaths.authLogin, {
|
|
186
191
|
body: JSON.stringify(options),
|
|
@@ -1083,14 +1088,14 @@ function cacheDocumentForMeeting(document, cacheData) {
|
|
|
1083
1088
|
};
|
|
1084
1089
|
}
|
|
1085
1090
|
function buildMeetingTranscript(document, cacheData) {
|
|
1086
|
-
|
|
1091
|
+
const rawSegments = cacheData?.transcripts[document.id] ?? document.transcriptSegments ?? [];
|
|
1092
|
+
if (!(Boolean(cacheData) || Array.isArray(document.transcriptSegments))) return {
|
|
1087
1093
|
loaded: false,
|
|
1088
1094
|
segmentCount: 0,
|
|
1089
1095
|
transcript: null,
|
|
1090
1096
|
transcriptRecord: null,
|
|
1091
1097
|
transcriptText: null
|
|
1092
1098
|
};
|
|
1093
|
-
const rawSegments = cacheData.transcripts[document.id] ?? [];
|
|
1094
1099
|
const normalisedSegments = normaliseTranscriptSegments(rawSegments);
|
|
1095
1100
|
if (normalisedSegments.length === 0) return {
|
|
1096
1101
|
loaded: true,
|
|
@@ -1378,7 +1383,7 @@ function renderGranolaTuiMeetingTab(bundle, tab) {
|
|
|
1378
1383
|
}
|
|
1379
1384
|
}
|
|
1380
1385
|
function buildGranolaTuiSummary(state, meetingSource) {
|
|
1381
|
-
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}`;
|
|
1382
1387
|
}
|
|
1383
1388
|
//#endregion
|
|
1384
1389
|
//#region src/tui/theme.ts
|
|
@@ -1424,13 +1429,16 @@ function actionDisabledReason(auth, actionId) {
|
|
|
1424
1429
|
case "refresh":
|
|
1425
1430
|
if (!auth.storedSessionAvailable) return "stored session missing";
|
|
1426
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" : "";
|
|
1427
1435
|
case "use-stored":
|
|
1428
1436
|
if (!auth.storedSessionAvailable) return "stored session missing";
|
|
1429
1437
|
return auth.mode === "stored-session" ? "already active" : "";
|
|
1430
1438
|
case "use-supabase":
|
|
1431
1439
|
if (!auth.supabaseAvailable) return "supabase.json unavailable";
|
|
1432
1440
|
return auth.mode === "supabase-file" ? "already active" : "";
|
|
1433
|
-
case "logout": return auth.storedSessionAvailable ? "" : "stored
|
|
1441
|
+
case "logout": return auth.apiKeyAvailable || auth.storedSessionAvailable ? "" : "no stored credentials";
|
|
1434
1442
|
}
|
|
1435
1443
|
}
|
|
1436
1444
|
function buildGranolaTuiAuthActions(auth) {
|
|
@@ -1447,22 +1455,28 @@ function buildGranolaTuiAuthActions(auth) {
|
|
|
1447
1455
|
key: "2",
|
|
1448
1456
|
label: "Refresh stored session"
|
|
1449
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
|
+
},
|
|
1450
1464
|
{
|
|
1451
1465
|
description: "Switch the active auth source to the stored session",
|
|
1452
1466
|
id: "use-stored",
|
|
1453
|
-
key: "
|
|
1467
|
+
key: "4",
|
|
1454
1468
|
label: "Use stored session"
|
|
1455
1469
|
},
|
|
1456
1470
|
{
|
|
1457
1471
|
description: "Switch the active auth source to supabase.json",
|
|
1458
1472
|
id: "use-supabase",
|
|
1459
|
-
key: "
|
|
1473
|
+
key: "5",
|
|
1460
1474
|
label: "Use supabase.json"
|
|
1461
1475
|
},
|
|
1462
1476
|
{
|
|
1463
|
-
description: "Delete
|
|
1477
|
+
description: "Delete stored credentials and fall back to configured sources",
|
|
1464
1478
|
id: "logout",
|
|
1465
|
-
key: "
|
|
1479
|
+
key: "6",
|
|
1466
1480
|
label: "Sign out"
|
|
1467
1481
|
}
|
|
1468
1482
|
].map((action) => {
|
|
@@ -1476,7 +1490,8 @@ function buildGranolaTuiAuthActions(auth) {
|
|
|
1476
1490
|
}
|
|
1477
1491
|
function renderGranolaTuiAuthState(auth) {
|
|
1478
1492
|
const lines = [
|
|
1479
|
-
`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"}`,
|
|
1480
1495
|
`Stored session: ${auth.storedSessionAvailable ? "available" : "missing"}`,
|
|
1481
1496
|
`supabase.json: ${auth.supabaseAvailable ? "available" : "missing"}`,
|
|
1482
1497
|
`Refresh: ${auth.refreshAvailable ? "available" : "missing"}`
|
|
@@ -1972,6 +1987,11 @@ var GranolaTuiWorkspace = class {
|
|
|
1972
1987
|
await this.app.refreshAuth();
|
|
1973
1988
|
successMessage = "Stored session refreshed";
|
|
1974
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;
|
|
1975
1995
|
case "use-stored":
|
|
1976
1996
|
this.setStatus("Switching to stored session…");
|
|
1977
1997
|
await this.app.switchAuthMode("stored-session");
|
|
@@ -1985,7 +2005,7 @@ var GranolaTuiWorkspace = class {
|
|
|
1985
2005
|
case "logout":
|
|
1986
2006
|
this.setStatus("Signing out…");
|
|
1987
2007
|
await this.app.logoutAuth();
|
|
1988
|
-
successMessage = "Stored
|
|
2008
|
+
successMessage = "Stored credentials removed";
|
|
1989
2009
|
break;
|
|
1990
2010
|
}
|
|
1991
2011
|
await this.reloadAfterAuthChange();
|
|
@@ -2383,11 +2403,13 @@ function defaultGranolaToolkitPersistenceLayout(options = {}) {
|
|
|
2383
2403
|
const targetPlatform = options.platform ?? platform();
|
|
2384
2404
|
const dataDirectory = defaultGranolaToolkitDataDirectory(targetPlatform, options.homeDirectory ?? homedir());
|
|
2385
2405
|
return {
|
|
2406
|
+
apiKeyFile: join(dataDirectory, "api-key.txt"),
|
|
2386
2407
|
dataDirectory,
|
|
2387
2408
|
exportJobsFile: join(dataDirectory, "export-jobs.json"),
|
|
2388
2409
|
meetingIndexFile: join(dataDirectory, "meeting-index.json"),
|
|
2389
2410
|
sessionFile: join(dataDirectory, "session.json"),
|
|
2390
2411
|
sessionStoreKind: targetPlatform === "darwin" ? "keychain" : "file",
|
|
2412
|
+
syncEventsFile: join(dataDirectory, "sync-events.jsonl"),
|
|
2391
2413
|
syncStateFile: join(dataDirectory, "sync-state.json")
|
|
2392
2414
|
};
|
|
2393
2415
|
}
|
|
@@ -2396,6 +2418,7 @@ function defaultGranolaToolkitPersistenceLayout(options = {}) {
|
|
|
2396
2418
|
const execFileAsync$1 = promisify(execFile);
|
|
2397
2419
|
const DEFAULT_CLIENT_ID = "client_GranolaMac";
|
|
2398
2420
|
const KEYCHAIN_SERVICE_NAME = "com.granola.toolkit";
|
|
2421
|
+
const KEYCHAIN_ACCOUNT_NAME_API_KEY = "api-key";
|
|
2399
2422
|
const KEYCHAIN_ACCOUNT_NAME = "session";
|
|
2400
2423
|
const WORKOS_AUTH_URL = "https://api.workos.com/user_management/authenticate";
|
|
2401
2424
|
function numberValue(value) {
|
|
@@ -2480,6 +2503,32 @@ var FileSessionStore = class {
|
|
|
2480
2503
|
});
|
|
2481
2504
|
}
|
|
2482
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
|
+
};
|
|
2483
2532
|
var KeychainSessionStore = class {
|
|
2484
2533
|
async clearSession() {
|
|
2485
2534
|
try {
|
|
@@ -2521,6 +2570,48 @@ var KeychainSessionStore = class {
|
|
|
2521
2570
|
]);
|
|
2522
2571
|
}
|
|
2523
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
|
+
};
|
|
2524
2615
|
var CachedTokenProvider = class {
|
|
2525
2616
|
#token;
|
|
2526
2617
|
constructor(source, store = new NoopTokenStore()) {
|
|
@@ -2614,33 +2705,45 @@ async function refreshGranolaSession(session, fetchImpl = fetch) {
|
|
|
2614
2705
|
function defaultSessionFilePath() {
|
|
2615
2706
|
return defaultGranolaToolkitPersistenceLayout().sessionFile;
|
|
2616
2707
|
}
|
|
2708
|
+
function defaultApiKeyFilePath() {
|
|
2709
|
+
return defaultGranolaToolkitPersistenceLayout().apiKeyFile;
|
|
2710
|
+
}
|
|
2617
2711
|
function createDefaultSessionStore() {
|
|
2618
2712
|
return defaultGranolaToolkitPersistenceLayout().sessionStoreKind === "keychain" ? new KeychainSessionStore() : new FileSessionStore();
|
|
2619
2713
|
}
|
|
2714
|
+
function createDefaultApiKeyStore() {
|
|
2715
|
+
return defaultGranolaToolkitPersistenceLayout().sessionStoreKind === "keychain" ? new KeychainApiKeyStore() : new FileApiKeyStore();
|
|
2716
|
+
}
|
|
2620
2717
|
//#endregion
|
|
2621
2718
|
//#region src/client/default-auth.ts
|
|
2622
2719
|
function hasStoredSession(session) {
|
|
2623
2720
|
return Boolean(session?.accessToken.trim());
|
|
2624
2721
|
}
|
|
2625
2722
|
function resolveActiveMode(options) {
|
|
2723
|
+
if (options.preferredMode === "api-key" && options.apiKeyAvailable) return "api-key";
|
|
2626
2724
|
if (options.preferredMode === "stored-session" && options.storedSessionAvailable) return "stored-session";
|
|
2627
2725
|
if (options.preferredMode === "supabase-file" && options.supabaseAvailable) return "supabase-file";
|
|
2726
|
+
if (options.apiKeyAvailable) return "api-key";
|
|
2628
2727
|
if (options.storedSessionAvailable) return "stored-session";
|
|
2629
|
-
return "supabase-file";
|
|
2728
|
+
if (options.supabaseAvailable) return "supabase-file";
|
|
2729
|
+
return options.preferredMode ?? "api-key";
|
|
2630
2730
|
}
|
|
2631
2731
|
function missingSupabaseError() {
|
|
2632
2732
|
return /* @__PURE__ */ new Error(`supabase.json not found. Pass --supabase or create .granola.toml. Expected locations include: ${granolaSupabaseCandidates().join(", ")}`);
|
|
2633
2733
|
}
|
|
2634
2734
|
function buildDefaultGranolaAuthInfo(config, options = {}) {
|
|
2635
2735
|
const existsSyncImpl = options.existsSyncImpl ?? existsSync;
|
|
2736
|
+
const apiKeyAvailable = Boolean(options.apiKey?.trim());
|
|
2636
2737
|
const session = options.session;
|
|
2637
2738
|
const storedSessionAvailable = hasStoredSession(session);
|
|
2638
2739
|
const supabasePath = config.supabase || void 0;
|
|
2639
2740
|
const supabaseAvailable = Boolean(supabasePath && existsSyncImpl(supabasePath));
|
|
2640
2741
|
return {
|
|
2742
|
+
apiKeyAvailable,
|
|
2641
2743
|
clientId: session?.clientId,
|
|
2642
2744
|
lastError: options.lastError,
|
|
2643
2745
|
mode: resolveActiveMode({
|
|
2746
|
+
apiKeyAvailable,
|
|
2644
2747
|
preferredMode: options.preferredMode,
|
|
2645
2748
|
storedSessionAvailable,
|
|
2646
2749
|
supabaseAvailable
|
|
@@ -2653,9 +2756,12 @@ function buildDefaultGranolaAuthInfo(config, options = {}) {
|
|
|
2653
2756
|
};
|
|
2654
2757
|
}
|
|
2655
2758
|
async function inspectDefaultGranolaAuth(config, options = {}) {
|
|
2759
|
+
const apiKeyStore = options.apiKeyStore ?? createDefaultApiKeyStore();
|
|
2656
2760
|
const sessionStore = options.sessionStore ?? createDefaultSessionStore();
|
|
2761
|
+
const apiKey = options.apiKey ?? config.apiKey ?? await apiKeyStore.readApiKey();
|
|
2657
2762
|
const session = options.session ?? await sessionStore.readSession();
|
|
2658
2763
|
return buildDefaultGranolaAuthInfo(config, {
|
|
2764
|
+
apiKey,
|
|
2659
2765
|
existsSyncImpl: options.existsSyncImpl,
|
|
2660
2766
|
lastError: options.lastError,
|
|
2661
2767
|
preferredMode: options.preferredMode,
|
|
@@ -2672,6 +2778,12 @@ var DefaultAuthController = class {
|
|
|
2672
2778
|
sessionStore() {
|
|
2673
2779
|
return this.options.sessionStore ?? createDefaultSessionStore();
|
|
2674
2780
|
}
|
|
2781
|
+
apiKeyStore() {
|
|
2782
|
+
return this.options.apiKeyStore ?? createDefaultApiKeyStore();
|
|
2783
|
+
}
|
|
2784
|
+
async readApiKey() {
|
|
2785
|
+
return this.config.apiKey?.trim() || await this.apiKeyStore().readApiKey();
|
|
2786
|
+
}
|
|
2675
2787
|
readSession() {
|
|
2676
2788
|
return this.sessionStore().readSession();
|
|
2677
2789
|
}
|
|
@@ -2685,8 +2797,10 @@ var DefaultAuthController = class {
|
|
|
2685
2797
|
return this.options.sessionSourceFactory?.(supabasePath) ?? new SupabaseFileSessionSource(supabasePath);
|
|
2686
2798
|
}
|
|
2687
2799
|
async inspect() {
|
|
2800
|
+
const apiKey = await this.readApiKey();
|
|
2688
2801
|
const session = await this.readSession();
|
|
2689
2802
|
return buildDefaultGranolaAuthInfo(this.config, {
|
|
2803
|
+
apiKey,
|
|
2690
2804
|
existsSyncImpl: this.options.existsSyncImpl,
|
|
2691
2805
|
lastError: this.#lastError,
|
|
2692
2806
|
preferredMode: this.#preferredMode,
|
|
@@ -2694,6 +2808,13 @@ var DefaultAuthController = class {
|
|
|
2694
2808
|
});
|
|
2695
2809
|
}
|
|
2696
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
|
+
}
|
|
2697
2818
|
const supabasePath = this.resolveSupabasePath(options.supabasePath);
|
|
2698
2819
|
const session = await this.sessionSource(supabasePath).loadSession();
|
|
2699
2820
|
await this.sessionStore().writeSession(session);
|
|
@@ -2702,6 +2823,7 @@ var DefaultAuthController = class {
|
|
|
2702
2823
|
return await this.inspect();
|
|
2703
2824
|
}
|
|
2704
2825
|
async logout() {
|
|
2826
|
+
await this.apiKeyStore().clearApiKey();
|
|
2705
2827
|
await this.sessionStore().clearSession();
|
|
2706
2828
|
this.#lastError = void 0;
|
|
2707
2829
|
this.#preferredMode = void 0;
|
|
@@ -2726,6 +2848,10 @@ var DefaultAuthController = class {
|
|
|
2726
2848
|
}
|
|
2727
2849
|
async switchMode(mode) {
|
|
2728
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
|
+
}
|
|
2729
2855
|
if (mode === "stored-session" && !state.storedSessionAvailable) {
|
|
2730
2856
|
this.#lastError = "no stored Granola session found";
|
|
2731
2857
|
throw new Error(this.#lastError);
|
|
@@ -2790,6 +2916,63 @@ function parseDocument(value) {
|
|
|
2790
2916
|
updatedAt: stringValue(record.updated_at)
|
|
2791
2917
|
};
|
|
2792
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
|
+
}
|
|
2793
2976
|
function parseFolderDocumentIds(value) {
|
|
2794
2977
|
if (!Array.isArray(value)) return [];
|
|
2795
2978
|
return value.map((item) => {
|
|
@@ -2893,6 +3076,98 @@ var GranolaApiClient = class {
|
|
|
2893
3076
|
}
|
|
2894
3077
|
};
|
|
2895
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
|
|
2896
3171
|
//#region src/client/http.ts
|
|
2897
3172
|
const RETRYABLE_STATUS_CODES = new Set([
|
|
2898
3173
|
429,
|
|
@@ -2987,8 +3262,23 @@ var AuthenticatedHttpClient = class {
|
|
|
2987
3262
|
//#region src/client/default.ts
|
|
2988
3263
|
async function createDefaultGranolaRuntime(config, logger = console, options = {}) {
|
|
2989
3264
|
const auth = await inspectDefaultGranolaAuth(config, { preferredMode: options.preferredMode });
|
|
2990
|
-
if (!auth.storedSessionAvailable && !config.supabase) throw new Error(`
|
|
2991
|
-
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
|
+
}
|
|
2992
3282
|
const sessionStore = createDefaultSessionStore();
|
|
2993
3283
|
return {
|
|
2994
3284
|
auth,
|
|
@@ -3281,14 +3571,17 @@ function cloneSyncSummary(summary) {
|
|
|
3281
3571
|
}
|
|
3282
3572
|
function normaliseSyncState(filePath, file) {
|
|
3283
3573
|
return {
|
|
3574
|
+
eventCount: file?.eventCount ?? 0,
|
|
3575
|
+
eventsFile: file?.eventsFile ?? defaultSyncEventsFilePath$1(),
|
|
3284
3576
|
filePath,
|
|
3285
3577
|
lastChanges: (file?.lastChanges ?? []).slice(0, MAX_STORED_CHANGES).map(cloneSyncChange$1),
|
|
3286
|
-
lastCompletedAt: file?.lastCompletedAt,
|
|
3287
|
-
lastError: file?.lastError,
|
|
3288
|
-
lastFailedAt: file?.lastFailedAt,
|
|
3289
|
-
lastStartedAt: file?.lastStartedAt,
|
|
3290
3578
|
running: false,
|
|
3291
|
-
|
|
3579
|
+
...file?.lastCompletedAt ? { lastCompletedAt: file.lastCompletedAt } : {},
|
|
3580
|
+
...file?.lastError ? { lastError: file.lastError } : {},
|
|
3581
|
+
...file?.lastFailedAt ? { lastFailedAt: file.lastFailedAt } : {},
|
|
3582
|
+
...file?.lastRunId ? { lastRunId: file.lastRunId } : {},
|
|
3583
|
+
...file?.lastStartedAt ? { lastStartedAt: file.lastStartedAt } : {},
|
|
3584
|
+
...file?.summary ? { summary: cloneSyncSummary(file.summary) } : {}
|
|
3292
3585
|
};
|
|
3293
3586
|
}
|
|
3294
3587
|
var FileSyncStateStore = class {
|
|
@@ -3307,10 +3600,13 @@ var FileSyncStateStore = class {
|
|
|
3307
3600
|
async writeState(state) {
|
|
3308
3601
|
await mkdir(dirname(this.filePath), { recursive: true });
|
|
3309
3602
|
const payload = {
|
|
3603
|
+
eventCount: state.eventCount,
|
|
3604
|
+
eventsFile: state.eventsFile,
|
|
3310
3605
|
lastChanges: state.lastChanges.slice(0, MAX_STORED_CHANGES).map(cloneSyncChange$1),
|
|
3311
3606
|
lastCompletedAt: state.lastCompletedAt,
|
|
3312
3607
|
lastError: state.lastError,
|
|
3313
3608
|
lastFailedAt: state.lastFailedAt,
|
|
3609
|
+
lastRunId: state.lastRunId,
|
|
3314
3610
|
lastStartedAt: state.lastStartedAt,
|
|
3315
3611
|
summary: cloneSyncSummary(state.summary),
|
|
3316
3612
|
version: SYNC_STATE_VERSION
|
|
@@ -3324,10 +3620,45 @@ var FileSyncStateStore = class {
|
|
|
3324
3620
|
function defaultSyncStateFilePath() {
|
|
3325
3621
|
return defaultGranolaToolkitPersistenceLayout().syncStateFile;
|
|
3326
3622
|
}
|
|
3623
|
+
function defaultSyncEventsFilePath$1() {
|
|
3624
|
+
return defaultGranolaToolkitPersistenceLayout().syncEventsFile;
|
|
3625
|
+
}
|
|
3327
3626
|
function createDefaultSyncStateStore() {
|
|
3328
3627
|
return new FileSyncStateStore();
|
|
3329
3628
|
}
|
|
3330
3629
|
//#endregion
|
|
3630
|
+
//#region src/sync-events.ts
|
|
3631
|
+
function cloneSyncEvent$1(event) {
|
|
3632
|
+
return { ...event };
|
|
3633
|
+
}
|
|
3634
|
+
var FileSyncEventStore = class {
|
|
3635
|
+
constructor(filePath = defaultSyncEventsFilePath()) {
|
|
3636
|
+
this.filePath = filePath;
|
|
3637
|
+
}
|
|
3638
|
+
async appendEvents(events) {
|
|
3639
|
+
if (events.length === 0) return;
|
|
3640
|
+
await mkdir(dirname(this.filePath), { recursive: true });
|
|
3641
|
+
const payload = events.map((event) => JSON.stringify(event)).join("\n");
|
|
3642
|
+
await appendFile(this.filePath, `${payload}\n`, {
|
|
3643
|
+
encoding: "utf8",
|
|
3644
|
+
mode: 384
|
|
3645
|
+
});
|
|
3646
|
+
}
|
|
3647
|
+
async readEvents(limit = 50) {
|
|
3648
|
+
try {
|
|
3649
|
+
return (await readFile(this.filePath, "utf8")).split("\n").map((line) => line.trim()).filter(Boolean).map((line) => parseJsonString(line)).filter((event) => Boolean(event)).map(cloneSyncEvent$1).slice(-limit).reverse();
|
|
3650
|
+
} catch {
|
|
3651
|
+
return [];
|
|
3652
|
+
}
|
|
3653
|
+
}
|
|
3654
|
+
};
|
|
3655
|
+
function defaultSyncEventsFilePath() {
|
|
3656
|
+
return defaultGranolaToolkitPersistenceLayout().syncEventsFile;
|
|
3657
|
+
}
|
|
3658
|
+
function createDefaultSyncEventStore() {
|
|
3659
|
+
return new FileSyncEventStore();
|
|
3660
|
+
}
|
|
3661
|
+
//#endregion
|
|
3331
3662
|
//#region src/sync.ts
|
|
3332
3663
|
function normaliseMeeting(meeting) {
|
|
3333
3664
|
return {
|
|
@@ -3425,6 +3756,18 @@ function diffMeetingSummaries(previous, next, folderCount) {
|
|
|
3425
3756
|
}
|
|
3426
3757
|
};
|
|
3427
3758
|
}
|
|
3759
|
+
function buildSyncEvents(runId, occurredAt, changes) {
|
|
3760
|
+
return changes.map((change, index) => ({
|
|
3761
|
+
id: `${runId}:${index + 1}`,
|
|
3762
|
+
kind: change.kind === "created" ? "meeting.created" : change.kind === "changed" ? "meeting.changed" : change.kind === "removed" ? "meeting.removed" : "transcript.ready",
|
|
3763
|
+
meetingId: change.meetingId,
|
|
3764
|
+
occurredAt,
|
|
3765
|
+
previousUpdatedAt: change.previousUpdatedAt,
|
|
3766
|
+
runId,
|
|
3767
|
+
title: change.title,
|
|
3768
|
+
updatedAt: change.updatedAt
|
|
3769
|
+
}));
|
|
3770
|
+
}
|
|
3428
3771
|
//#endregion
|
|
3429
3772
|
//#region src/app/core.ts
|
|
3430
3773
|
function transcriptCount(cacheData) {
|
|
@@ -3455,6 +3798,9 @@ function cloneSyncState(state) {
|
|
|
3455
3798
|
summary: state.summary ? { ...state.summary } : void 0
|
|
3456
3799
|
};
|
|
3457
3800
|
}
|
|
3801
|
+
function cloneSyncEvent(event) {
|
|
3802
|
+
return { ...event };
|
|
3803
|
+
}
|
|
3458
3804
|
function cloneMeetingSummary(meeting) {
|
|
3459
3805
|
return {
|
|
3460
3806
|
...meeting,
|
|
@@ -3514,6 +3860,8 @@ function defaultState(config, auth, surface) {
|
|
|
3514
3860
|
meetingCount: 0
|
|
3515
3861
|
},
|
|
3516
3862
|
sync: {
|
|
3863
|
+
eventCount: 0,
|
|
3864
|
+
eventsFile: defaultSyncEventsFilePath$1(),
|
|
3517
3865
|
filePath: defaultSyncStateFilePath(),
|
|
3518
3866
|
lastChanges: [],
|
|
3519
3867
|
running: false
|
|
@@ -3550,6 +3898,8 @@ var GranolaApp = class {
|
|
|
3550
3898
|
this.#state.sync = {
|
|
3551
3899
|
...this.#state.sync,
|
|
3552
3900
|
...cloneSyncState(deps.syncState ?? {
|
|
3901
|
+
eventCount: 0,
|
|
3902
|
+
eventsFile: defaultSyncEventsFilePath$1(),
|
|
3553
3903
|
filePath: defaultSyncStateFilePath(),
|
|
3554
3904
|
lastChanges: [],
|
|
3555
3905
|
running: false
|
|
@@ -3609,6 +3959,9 @@ var GranolaApp = class {
|
|
|
3609
3959
|
if (!this.deps.syncStateStore) return;
|
|
3610
3960
|
await this.deps.syncStateStore.writeState(this.#state.sync);
|
|
3611
3961
|
}
|
|
3962
|
+
createSyncRunId() {
|
|
3963
|
+
return `sync-${this.nowIso().replaceAll(/[-:.]/g, "").replace("T", "").replace("Z", "")}`;
|
|
3964
|
+
}
|
|
3612
3965
|
applyAuthState(auth, options = {}) {
|
|
3613
3966
|
if (options.resetDocuments) this.resetRemoteState();
|
|
3614
3967
|
this.#state.auth = { ...auth };
|
|
@@ -3694,6 +4047,28 @@ var GranolaApp = class {
|
|
|
3694
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)));
|
|
3695
4048
|
return byDocumentId;
|
|
3696
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
|
+
}
|
|
3697
4072
|
async loadFolders(options = {}) {
|
|
3698
4073
|
if (options.forceRefresh) {
|
|
3699
4074
|
this.resetFoldersState();
|
|
@@ -3705,6 +4080,24 @@ var GranolaApp = class {
|
|
|
3705
4080
|
}));
|
|
3706
4081
|
const client = await this.getGranolaClient();
|
|
3707
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
|
+
}
|
|
3708
4101
|
if (options.required) throw new Error("Granola folder API is not configured");
|
|
3709
4102
|
return;
|
|
3710
4103
|
}
|
|
@@ -3793,6 +4186,10 @@ var GranolaApp = class {
|
|
|
3793
4186
|
async inspectSync() {
|
|
3794
4187
|
return cloneSyncState(this.#state.sync);
|
|
3795
4188
|
}
|
|
4189
|
+
async listSyncEvents(options = {}) {
|
|
4190
|
+
if (!this.deps.syncEventStore) return { events: [] };
|
|
4191
|
+
return { events: (await this.deps.syncEventStore.readEvents(options.limit)).map(cloneSyncEvent) };
|
|
4192
|
+
}
|
|
3796
4193
|
async loginAuth(options = {}) {
|
|
3797
4194
|
const controller = this.requireAuthController();
|
|
3798
4195
|
try {
|
|
@@ -3856,11 +4253,17 @@ var GranolaApp = class {
|
|
|
3856
4253
|
const snapshot = await this.liveMeetingSnapshot({ forceRefresh: options.forceRefresh ?? true });
|
|
3857
4254
|
await this.persistMeetingIndex(snapshot.meetings);
|
|
3858
4255
|
const { changes, summary } = diffMeetingSummaries(previousMeetings, snapshot.meetings, snapshot.folders?.length ?? 0);
|
|
4256
|
+
const completedAt = this.nowIso();
|
|
4257
|
+
const runId = this.createSyncRunId();
|
|
4258
|
+
const events = buildSyncEvents(runId, completedAt, changes);
|
|
4259
|
+
if (events.length > 0 && this.deps.syncEventStore) await this.deps.syncEventStore.appendEvents(events);
|
|
3859
4260
|
this.#state.sync = {
|
|
3860
4261
|
...this.#state.sync,
|
|
4262
|
+
eventCount: this.#state.sync.eventCount + events.length,
|
|
3861
4263
|
lastChanges: changes.slice(0, 50).map(cloneSyncChange),
|
|
3862
|
-
lastCompletedAt:
|
|
4264
|
+
lastCompletedAt: completedAt,
|
|
3863
4265
|
lastError: void 0,
|
|
4266
|
+
lastRunId: runId,
|
|
3864
4267
|
running: false,
|
|
3865
4268
|
summary: { ...summary }
|
|
3866
4269
|
};
|
|
@@ -4209,6 +4612,7 @@ async function createGranolaApp(config, options = {}) {
|
|
|
4209
4612
|
const exportJobs = await exportJobStore.readJobs();
|
|
4210
4613
|
const meetingIndexStore = createDefaultMeetingIndexStore();
|
|
4211
4614
|
const meetingIndex = await meetingIndexStore.readIndex();
|
|
4615
|
+
const syncEventStore = createDefaultSyncEventStore();
|
|
4212
4616
|
const syncStateStore = createDefaultSyncStateStore();
|
|
4213
4617
|
const syncState = await syncStateStore.readState();
|
|
4214
4618
|
return new GranolaApp(config, {
|
|
@@ -4221,6 +4625,7 @@ async function createGranolaApp(config, options = {}) {
|
|
|
4221
4625
|
meetingIndex,
|
|
4222
4626
|
meetingIndexStore,
|
|
4223
4627
|
now: options.now,
|
|
4628
|
+
syncEventStore,
|
|
4224
4629
|
syncState,
|
|
4225
4630
|
syncStateStore
|
|
4226
4631
|
}, { surface: options.surface });
|
|
@@ -4287,6 +4692,7 @@ async function loadConfig(options) {
|
|
|
4287
4692
|
const defaultCache = firstExistingPath(granolaCacheCandidates());
|
|
4288
4693
|
const timeoutValue = pickString(options.subcommandFlags.timeout) ?? pickString(env.TIMEOUT) ?? pickString(configValues.timeout) ?? "2m";
|
|
4289
4694
|
return {
|
|
4695
|
+
apiKey: pickString(options.globalFlags["api-key"]) ?? pickString(env.GRANOLA_API_KEY) ?? pickString(configValues["api-key"]) ?? pickString(configValues.apiKey),
|
|
4290
4696
|
configFileUsed: config.path,
|
|
4291
4697
|
debug: pickBoolean(options.globalFlags.debug) ?? envFlag(env.DEBUG_MODE) ?? pickBoolean(configValues.debug) ?? false,
|
|
4292
4698
|
notes: {
|
|
@@ -4374,13 +4780,15 @@ Usage:
|
|
|
4374
4780
|
|
|
4375
4781
|
Subcommands:
|
|
4376
4782
|
login Import credentials from the Granola desktop app
|
|
4783
|
+
Or store a Granola API key with --api-key
|
|
4377
4784
|
status Show the current Granola auth state
|
|
4378
|
-
logout Delete
|
|
4785
|
+
logout Delete stored Granola credentials
|
|
4379
4786
|
refresh Refresh the stored Granola session
|
|
4380
|
-
use <stored|supabase>
|
|
4787
|
+
use <api-key|stored|supabase>
|
|
4381
4788
|
Switch the active auth source for this toolkit instance
|
|
4382
4789
|
|
|
4383
4790
|
Options:
|
|
4791
|
+
--api-key <token> Store a Granola Personal API key
|
|
4384
4792
|
--supabase <path> Path to supabase.json for auth login
|
|
4385
4793
|
--config <path> Path to .granola.toml
|
|
4386
4794
|
--debug Enable debug logging
|
|
@@ -4388,10 +4796,15 @@ Options:
|
|
|
4388
4796
|
`;
|
|
4389
4797
|
}
|
|
4390
4798
|
function formatAuthSource(mode) {
|
|
4391
|
-
|
|
4799
|
+
switch (mode) {
|
|
4800
|
+
case "api-key": return "API key";
|
|
4801
|
+
case "stored-session": return "stored session";
|
|
4802
|
+
default: return "supabase.json";
|
|
4803
|
+
}
|
|
4392
4804
|
}
|
|
4393
4805
|
function printAuthState(state) {
|
|
4394
4806
|
console.log(`Active source: ${formatAuthSource(state.mode)}`);
|
|
4807
|
+
console.log(`API key: ${state.apiKeyAvailable ? "available" : "missing"}`);
|
|
4395
4808
|
console.log(`Stored session: ${state.storedSessionAvailable ? "available" : "missing"}`);
|
|
4396
4809
|
console.log(`supabase.json: ${state.supabaseAvailable ? "available" : "missing"}`);
|
|
4397
4810
|
if (state.supabasePath) console.log(`supabase path: ${state.supabasePath}`);
|
|
@@ -4402,10 +4815,13 @@ function printAuthState(state) {
|
|
|
4402
4815
|
}
|
|
4403
4816
|
const authCommand = {
|
|
4404
4817
|
description: "Manage stored Granola sessions",
|
|
4405
|
-
flags: {
|
|
4818
|
+
flags: {
|
|
4819
|
+
"api-key": { type: "string" },
|
|
4820
|
+
help: { type: "boolean" }
|
|
4821
|
+
},
|
|
4406
4822
|
help: authHelp,
|
|
4407
4823
|
name: "auth",
|
|
4408
|
-
async run({ commandArgs, globalFlags }) {
|
|
4824
|
+
async run({ commandArgs, commandFlags, globalFlags }) {
|
|
4409
4825
|
const [action, value] = commandArgs;
|
|
4410
4826
|
const config = await loadConfig({
|
|
4411
4827
|
globalFlags,
|
|
@@ -4416,14 +4832,14 @@ const authCommand = {
|
|
|
4416
4832
|
const app = await createGranolaApp(config);
|
|
4417
4833
|
switch (action) {
|
|
4418
4834
|
case "login": {
|
|
4419
|
-
const state = await app.loginAuth();
|
|
4420
|
-
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"}`);
|
|
4421
4837
|
printAuthState(state);
|
|
4422
4838
|
return 0;
|
|
4423
4839
|
}
|
|
4424
4840
|
case "logout": {
|
|
4425
4841
|
const state = await app.logoutAuth();
|
|
4426
|
-
console.log("Stored Granola
|
|
4842
|
+
console.log("Stored Granola credentials deleted");
|
|
4427
4843
|
printAuthState(state);
|
|
4428
4844
|
return 0;
|
|
4429
4845
|
}
|
|
@@ -4436,7 +4852,7 @@ const authCommand = {
|
|
|
4436
4852
|
case "status": {
|
|
4437
4853
|
const state = await app.inspectAuth();
|
|
4438
4854
|
printAuthState(state);
|
|
4439
|
-
return state.storedSessionAvailable ? 0 : 1;
|
|
4855
|
+
return state.apiKeyAvailable || state.storedSessionAvailable || state.supabaseAvailable ? 0 : 1;
|
|
4440
4856
|
}
|
|
4441
4857
|
case "use": {
|
|
4442
4858
|
const mode = resolveAuthMode(value);
|
|
@@ -4454,11 +4870,13 @@ const authCommand = {
|
|
|
4454
4870
|
};
|
|
4455
4871
|
function resolveAuthMode(value) {
|
|
4456
4872
|
switch (value) {
|
|
4873
|
+
case "api":
|
|
4874
|
+
case "api-key": return "api-key";
|
|
4457
4875
|
case "stored":
|
|
4458
4876
|
case "stored-session": return "stored-session";
|
|
4459
4877
|
case "supabase":
|
|
4460
4878
|
case "supabase-file": return "supabase-file";
|
|
4461
|
-
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");
|
|
4462
4880
|
}
|
|
4463
4881
|
}
|
|
4464
4882
|
//#endregion
|
|
@@ -4902,7 +5320,12 @@ function renderAppState() {
|
|
|
4902
5320
|
}
|
|
4903
5321
|
|
|
4904
5322
|
const appState = state.appState;
|
|
4905
|
-
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";
|
|
4906
5329
|
const docs = appState.documents.loaded ? String(appState.documents.count) : "not loaded";
|
|
4907
5330
|
const cache = appState.cache.loaded
|
|
4908
5331
|
? appState.cache.transcriptCount + " transcript sets"
|
|
@@ -5022,7 +5445,12 @@ function renderAuthPanel() {
|
|
|
5022
5445
|
return;
|
|
5023
5446
|
}
|
|
5024
5447
|
|
|
5025
|
-
const activeSource =
|
|
5448
|
+
const activeSource =
|
|
5449
|
+
auth.mode === "api-key"
|
|
5450
|
+
? "API key"
|
|
5451
|
+
: auth.mode === "stored-session"
|
|
5452
|
+
? "Stored session"
|
|
5453
|
+
: "supabase.json";
|
|
5026
5454
|
const lastError = auth.lastError
|
|
5027
5455
|
? '<div class="auth-card__meta auth-card__error">' + escapeHtml(auth.lastError) + "</div>"
|
|
5028
5456
|
: "";
|
|
@@ -5031,6 +5459,7 @@ function renderAuthPanel() {
|
|
|
5031
5459
|
'<div class="auth-card">',
|
|
5032
5460
|
'<div class="status-grid">',
|
|
5033
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>",
|
|
5034
5463
|
'<div><span class="status-label">Stored</span><strong>' + escapeHtml(auth.storedSessionAvailable ? "available" : "missing") + "</strong></div>",
|
|
5035
5464
|
'<div><span class="status-label">supabase.json</span><strong>' + escapeHtml(auth.supabaseAvailable ? "available" : "missing") + "</strong></div>",
|
|
5036
5465
|
'<div><span class="status-label">Refresh</span><strong>' + escapeHtml(auth.refreshAvailable ? "available" : "missing") + "</strong></div>",
|
|
@@ -5045,12 +5474,16 @@ function renderAuthPanel() {
|
|
|
5045
5474
|
? '<div class="auth-card__meta">supabase path: ' + escapeHtml(auth.supabasePath) + "</div>"
|
|
5046
5475
|
: "",
|
|
5047
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>',
|
|
5048
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),
|
|
5049
5481
|
authActionButton("Import desktop session", "login", !auth.supabaseAvailable),
|
|
5050
5482
|
authActionButton("Refresh stored session", "refresh", !auth.storedSessionAvailable || !auth.refreshAvailable),
|
|
5483
|
+
authModeButton("Use API key", "api-key", !auth.apiKeyAvailable || auth.mode === "api-key"),
|
|
5051
5484
|
authModeButton("Use stored session", "stored-session", !auth.storedSessionAvailable || auth.mode === "stored-session"),
|
|
5052
5485
|
authModeButton("Use supabase.json", "supabase-file", !auth.supabaseAvailable || auth.mode === "supabase-file"),
|
|
5053
|
-
authActionButton("Sign out", "logout", !auth.storedSessionAvailable),
|
|
5486
|
+
authActionButton("Sign out", "logout", !auth.apiKeyAvailable && !auth.storedSessionAvailable),
|
|
5054
5487
|
"</div>",
|
|
5055
5488
|
"</div>",
|
|
5056
5489
|
].join("");
|
|
@@ -5440,14 +5873,19 @@ async function rerunJob(id) {
|
|
|
5440
5873
|
}
|
|
5441
5874
|
}
|
|
5442
5875
|
|
|
5443
|
-
async function loginAuth() {
|
|
5444
|
-
setStatus("Importing desktop session…", "busy");
|
|
5876
|
+
async function loginAuth(apiKey) {
|
|
5877
|
+
setStatus(apiKey ? "Saving API key…" : "Importing desktop session…", "busy");
|
|
5445
5878
|
try {
|
|
5446
|
-
|
|
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
|
+
});
|
|
5447
5885
|
await refreshAll();
|
|
5448
5886
|
} catch (error) {
|
|
5449
5887
|
await syncAuthState();
|
|
5450
|
-
setStatus("Auth import failed", "error");
|
|
5888
|
+
setStatus(apiKey ? "API key save failed" : "Auth import failed", "error");
|
|
5451
5889
|
state.detailError = error instanceof Error ? error.message : String(error);
|
|
5452
5890
|
renderMeetingDetail();
|
|
5453
5891
|
}
|
|
@@ -5594,6 +6032,17 @@ els.authPanel.addEventListener("click", (event) => {
|
|
|
5594
6032
|
const actionButton = event.target.closest("[data-auth-action]");
|
|
5595
6033
|
if (actionButton) {
|
|
5596
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
|
+
}
|
|
5597
6046
|
case "login":
|
|
5598
6047
|
void loginAuth();
|
|
5599
6048
|
return;
|
|
@@ -6461,9 +6910,10 @@ function parseMeetingSort(value) {
|
|
|
6461
6910
|
}
|
|
6462
6911
|
function parseAuthMode(value) {
|
|
6463
6912
|
switch (value) {
|
|
6913
|
+
case "api-key":
|
|
6464
6914
|
case "stored-session":
|
|
6465
6915
|
case "supabase-file": return value;
|
|
6466
|
-
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");
|
|
6467
6917
|
}
|
|
6468
6918
|
}
|
|
6469
6919
|
function folderIdFromBody(value) {
|
|
@@ -6603,6 +7053,7 @@ async function startGranolaServer(app, options = {}) {
|
|
|
6603
7053
|
exportJobs: true,
|
|
6604
7054
|
meetingIndex: true,
|
|
6605
7055
|
sessionStore: defaultGranolaToolkitPersistenceLayout().sessionStoreKind,
|
|
7056
|
+
syncEvents: true,
|
|
6606
7057
|
syncState: true
|
|
6607
7058
|
},
|
|
6608
7059
|
product: "granola-toolkit",
|
|
@@ -6707,6 +7158,10 @@ async function startGranolaServer(app, options = {}) {
|
|
|
6707
7158
|
}), { headers: originHeaders });
|
|
6708
7159
|
return;
|
|
6709
7160
|
}
|
|
7161
|
+
if (method === "GET" && path === granolaTransportPaths.syncEvents) {
|
|
7162
|
+
sendJson(response, await app.listSyncEvents({ limit: parseInteger(url.searchParams.get("limit")) ?? 20 }), { headers: originHeaders });
|
|
7163
|
+
return;
|
|
7164
|
+
}
|
|
6710
7165
|
if (method === "POST" && path === granolaTransportPaths.authLock) {
|
|
6711
7166
|
sendJson(response, { ok: true }, { headers: {
|
|
6712
7167
|
...originHeaders,
|
|
@@ -6805,8 +7260,12 @@ async function startGranolaServer(app, options = {}) {
|
|
|
6805
7260
|
}
|
|
6806
7261
|
if (method === "POST" && path === granolaTransportPaths.authLogin) {
|
|
6807
7262
|
const body = await readJsonBody(request);
|
|
7263
|
+
const apiKey = typeof body.apiKey === "string" && body.apiKey.trim() ? body.apiKey.trim() : void 0;
|
|
6808
7264
|
const supabasePath = typeof body.supabasePath === "string" && body.supabasePath.trim() ? body.supabasePath.trim() : void 0;
|
|
6809
|
-
sendJson(response, await app.loginAuth({
|
|
7265
|
+
sendJson(response, await app.loginAuth({
|
|
7266
|
+
apiKey,
|
|
7267
|
+
supabasePath
|
|
7268
|
+
}), { headers: originHeaders });
|
|
6810
7269
|
return;
|
|
6811
7270
|
}
|
|
6812
7271
|
if (method === "POST" && path === granolaTransportPaths.authLogout) {
|
|
@@ -6991,6 +7450,7 @@ function printWebRoutes() {
|
|
|
6991
7450
|
console.log(" POST /exports/notes");
|
|
6992
7451
|
console.log(" POST /exports/jobs/:id/rerun");
|
|
6993
7452
|
console.log(" POST /exports/transcripts");
|
|
7453
|
+
console.log(" GET /sync/events");
|
|
6994
7454
|
console.log(" POST /sync");
|
|
6995
7455
|
}
|
|
6996
7456
|
async function runGranolaWebWorkspace(app, options) {
|
|
@@ -7443,6 +7903,7 @@ const serveCommand = {
|
|
|
7443
7903
|
console.log(" POST /exports/notes");
|
|
7444
7904
|
console.log(" POST /exports/jobs/:id/rerun");
|
|
7445
7905
|
console.log(" POST /exports/transcripts");
|
|
7906
|
+
console.log(" GET /sync/events");
|
|
7446
7907
|
console.log(" POST /sync");
|
|
7447
7908
|
console.log(`Attach: granola attach ${server.url.href}`);
|
|
7448
7909
|
if (password) console.log("Attach password: add --password <value>");
|
|
@@ -7460,10 +7921,12 @@ function syncHelp() {
|
|
|
7460
7921
|
|
|
7461
7922
|
Usage:
|
|
7462
7923
|
granola sync [options]
|
|
7924
|
+
granola sync events [options]
|
|
7463
7925
|
|
|
7464
7926
|
Options:
|
|
7465
7927
|
--watch Keep syncing in the background until interrupted
|
|
7466
7928
|
--interval <value> Poll interval for --watch, e.g. 60s or 5m (default: 60s)
|
|
7929
|
+
--limit <value> Event count for sync events output (default: 20)
|
|
7467
7930
|
--cache <path> Path to Granola cache JSON
|
|
7468
7931
|
--timeout <value> Request timeout, e.g. 2m, 30s, 120000 (default: 2m)
|
|
7469
7932
|
--supabase <path> Path to supabase.json
|
|
@@ -7489,12 +7952,13 @@ const syncCommand = {
|
|
|
7489
7952
|
cache: { type: "string" },
|
|
7490
7953
|
help: { type: "boolean" },
|
|
7491
7954
|
interval: { type: "string" },
|
|
7955
|
+
limit: { type: "string" },
|
|
7492
7956
|
timeout: { type: "string" },
|
|
7493
7957
|
watch: { type: "boolean" }
|
|
7494
7958
|
},
|
|
7495
7959
|
help: syncHelp,
|
|
7496
7960
|
name: "sync",
|
|
7497
|
-
async run({ commandFlags, globalFlags }) {
|
|
7961
|
+
async run({ commandArgs, commandFlags, globalFlags }) {
|
|
7498
7962
|
const config = await loadConfig({
|
|
7499
7963
|
globalFlags,
|
|
7500
7964
|
subcommandFlags: commandFlags
|
|
@@ -7505,6 +7969,16 @@ const syncCommand = {
|
|
|
7505
7969
|
debug(config.debug, "timeoutMs", config.notes.timeoutMs);
|
|
7506
7970
|
const app = await createGranolaApp(config);
|
|
7507
7971
|
debug(config.debug, "authMode", app.getState().auth.mode);
|
|
7972
|
+
if (commandArgs[0] === "events") {
|
|
7973
|
+
const limit = typeof commandFlags.limit === "string" && /^\d+$/.test(commandFlags.limit) ? Number(commandFlags.limit) : 20;
|
|
7974
|
+
const result = await app.listSyncEvents({ limit });
|
|
7975
|
+
if (result.events.length === 0) {
|
|
7976
|
+
console.log("No sync events yet.");
|
|
7977
|
+
return 0;
|
|
7978
|
+
}
|
|
7979
|
+
for (const event of result.events) console.log(`${event.occurredAt} ${event.kind.padEnd(18)} ${event.title} (${event.meetingId})`);
|
|
7980
|
+
return 0;
|
|
7981
|
+
}
|
|
7508
7982
|
const result = await app.sync();
|
|
7509
7983
|
printSyncResult(result);
|
|
7510
7984
|
if (result.state.lastCompletedAt) debug(config.debug, "syncCompletedAt", result.state.lastCompletedAt);
|
|
@@ -7810,6 +8284,7 @@ Commands:
|
|
|
7810
8284
|
${commands.map((command) => ` ${command.name.padEnd(commandWidth)} ${command.description}`).join("\n")}
|
|
7811
8285
|
|
|
7812
8286
|
Global options:
|
|
8287
|
+
--api-key <token> Granola Personal API key
|
|
7813
8288
|
--config <path> Path to .granola.toml
|
|
7814
8289
|
--debug Enable debug logging
|
|
7815
8290
|
--supabase <path> Path to supabase.json
|
|
@@ -7827,6 +8302,7 @@ async function runCli(argv) {
|
|
|
7827
8302
|
try {
|
|
7828
8303
|
const { command, rest } = splitCommand(argv);
|
|
7829
8304
|
const global = parseFlags(rest, {
|
|
8305
|
+
"api-key": { type: "string" },
|
|
7830
8306
|
config: { type: "string" },
|
|
7831
8307
|
debug: { type: "boolean" },
|
|
7832
8308
|
help: { type: "boolean" },
|