granola-toolkit 0.34.5 → 0.35.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 +1 -0
- package/dist/cli.js +503 -176
- package/package.json +1 -1
package/README.md
CHANGED
package/dist/cli.js
CHANGED
|
@@ -29,6 +29,7 @@ const granolaTransportPaths = {
|
|
|
29
29
|
meetings: "/meetings",
|
|
30
30
|
root: "/",
|
|
31
31
|
serverInfo: "/server/info",
|
|
32
|
+
syncRun: "/sync",
|
|
32
33
|
state: "/state"
|
|
33
34
|
};
|
|
34
35
|
function appendSearchParams(path, params) {
|
|
@@ -177,6 +178,9 @@ var GranolaServerClient = class GranolaServerClient {
|
|
|
177
178
|
async inspectAuth() {
|
|
178
179
|
return await this.requestJson(granolaTransportPaths.authStatus);
|
|
179
180
|
}
|
|
181
|
+
async inspectSync() {
|
|
182
|
+
return cloneValue(this.#state.sync);
|
|
183
|
+
}
|
|
180
184
|
async loginAuth(options = {}) {
|
|
181
185
|
return await this.requestJson(granolaTransportPaths.authLogin, {
|
|
182
186
|
body: JSON.stringify(options),
|
|
@@ -197,6 +201,13 @@ var GranolaServerClient = class GranolaServerClient {
|
|
|
197
201
|
method: "POST"
|
|
198
202
|
});
|
|
199
203
|
}
|
|
204
|
+
async sync(options = {}) {
|
|
205
|
+
return await this.requestJson(granolaTransportPaths.syncRun, {
|
|
206
|
+
body: JSON.stringify(options),
|
|
207
|
+
headers: { "content-type": "application/json" },
|
|
208
|
+
method: "POST"
|
|
209
|
+
});
|
|
210
|
+
}
|
|
200
211
|
async listFolders(options = {}) {
|
|
201
212
|
return await this.requestJson(granolaFoldersPath(options));
|
|
202
213
|
}
|
|
@@ -2370,7 +2381,8 @@ function defaultGranolaToolkitPersistenceLayout(options = {}) {
|
|
|
2370
2381
|
exportJobsFile: join(dataDirectory, "export-jobs.json"),
|
|
2371
2382
|
meetingIndexFile: join(dataDirectory, "meeting-index.json"),
|
|
2372
2383
|
sessionFile: join(dataDirectory, "session.json"),
|
|
2373
|
-
sessionStoreKind: targetPlatform === "darwin" ? "keychain" : "file"
|
|
2384
|
+
sessionStoreKind: targetPlatform === "darwin" ? "keychain" : "file",
|
|
2385
|
+
syncStateFile: join(dataDirectory, "sync-state.json")
|
|
2374
2386
|
};
|
|
2375
2387
|
}
|
|
2376
2388
|
//#endregion
|
|
@@ -3252,6 +3264,162 @@ function createDefaultMeetingIndexStore() {
|
|
|
3252
3264
|
return new FileMeetingIndexStore();
|
|
3253
3265
|
}
|
|
3254
3266
|
//#endregion
|
|
3267
|
+
//#region src/sync-state.ts
|
|
3268
|
+
const SYNC_STATE_VERSION = 1;
|
|
3269
|
+
const MAX_STORED_CHANGES = 50;
|
|
3270
|
+
function cloneSyncChange$1(change) {
|
|
3271
|
+
return { ...change };
|
|
3272
|
+
}
|
|
3273
|
+
function cloneSyncSummary(summary) {
|
|
3274
|
+
return summary ? { ...summary } : void 0;
|
|
3275
|
+
}
|
|
3276
|
+
function normaliseSyncState(filePath, file) {
|
|
3277
|
+
return {
|
|
3278
|
+
filePath,
|
|
3279
|
+
lastChanges: (file?.lastChanges ?? []).slice(0, MAX_STORED_CHANGES).map(cloneSyncChange$1),
|
|
3280
|
+
lastCompletedAt: file?.lastCompletedAt,
|
|
3281
|
+
lastError: file?.lastError,
|
|
3282
|
+
lastFailedAt: file?.lastFailedAt,
|
|
3283
|
+
lastStartedAt: file?.lastStartedAt,
|
|
3284
|
+
running: false,
|
|
3285
|
+
summary: cloneSyncSummary(file?.summary)
|
|
3286
|
+
};
|
|
3287
|
+
}
|
|
3288
|
+
var FileSyncStateStore = class {
|
|
3289
|
+
constructor(filePath = defaultSyncStateFilePath()) {
|
|
3290
|
+
this.filePath = filePath;
|
|
3291
|
+
}
|
|
3292
|
+
async readState() {
|
|
3293
|
+
try {
|
|
3294
|
+
const parsed = parseJsonString(await readFile(this.filePath, "utf8"));
|
|
3295
|
+
if (!parsed || parsed.version !== SYNC_STATE_VERSION) return normaliseSyncState(this.filePath);
|
|
3296
|
+
return normaliseSyncState(this.filePath, parsed);
|
|
3297
|
+
} catch {
|
|
3298
|
+
return normaliseSyncState(this.filePath);
|
|
3299
|
+
}
|
|
3300
|
+
}
|
|
3301
|
+
async writeState(state) {
|
|
3302
|
+
await mkdir(dirname(this.filePath), { recursive: true });
|
|
3303
|
+
const payload = {
|
|
3304
|
+
lastChanges: state.lastChanges.slice(0, MAX_STORED_CHANGES).map(cloneSyncChange$1),
|
|
3305
|
+
lastCompletedAt: state.lastCompletedAt,
|
|
3306
|
+
lastError: state.lastError,
|
|
3307
|
+
lastFailedAt: state.lastFailedAt,
|
|
3308
|
+
lastStartedAt: state.lastStartedAt,
|
|
3309
|
+
summary: cloneSyncSummary(state.summary),
|
|
3310
|
+
version: SYNC_STATE_VERSION
|
|
3311
|
+
};
|
|
3312
|
+
await writeFile(this.filePath, `${JSON.stringify(payload, null, 2)}\n`, {
|
|
3313
|
+
encoding: "utf8",
|
|
3314
|
+
mode: 384
|
|
3315
|
+
});
|
|
3316
|
+
}
|
|
3317
|
+
};
|
|
3318
|
+
function defaultSyncStateFilePath() {
|
|
3319
|
+
return defaultGranolaToolkitPersistenceLayout().syncStateFile;
|
|
3320
|
+
}
|
|
3321
|
+
function createDefaultSyncStateStore() {
|
|
3322
|
+
return new FileSyncStateStore();
|
|
3323
|
+
}
|
|
3324
|
+
//#endregion
|
|
3325
|
+
//#region src/sync.ts
|
|
3326
|
+
function normaliseMeeting(meeting) {
|
|
3327
|
+
return {
|
|
3328
|
+
createdAt: meeting.createdAt,
|
|
3329
|
+
folders: meeting.folders.map((folder) => ({
|
|
3330
|
+
createdAt: folder.createdAt,
|
|
3331
|
+
description: folder.description,
|
|
3332
|
+
documentCount: folder.documentCount,
|
|
3333
|
+
id: folder.id,
|
|
3334
|
+
isFavourite: folder.isFavourite,
|
|
3335
|
+
name: folder.name,
|
|
3336
|
+
updatedAt: folder.updatedAt,
|
|
3337
|
+
workspaceId: folder.workspaceId
|
|
3338
|
+
})).sort((left, right) => left.id.localeCompare(right.id)),
|
|
3339
|
+
noteContentSource: meeting.noteContentSource,
|
|
3340
|
+
tags: [...meeting.tags].sort((left, right) => left.localeCompare(right)),
|
|
3341
|
+
title: meeting.title,
|
|
3342
|
+
transcriptLoaded: meeting.transcriptLoaded,
|
|
3343
|
+
transcriptSegmentCount: meeting.transcriptSegmentCount,
|
|
3344
|
+
updatedAt: meeting.updatedAt
|
|
3345
|
+
};
|
|
3346
|
+
}
|
|
3347
|
+
function meetingChanged(previous, next) {
|
|
3348
|
+
return JSON.stringify(normaliseMeeting(previous)) !== JSON.stringify(normaliseMeeting(next));
|
|
3349
|
+
}
|
|
3350
|
+
function diffMeetingSummaries(previous, next, folderCount) {
|
|
3351
|
+
const previousById = new Map(previous.map((meeting) => [meeting.id, meeting]));
|
|
3352
|
+
const nextById = new Map(next.map((meeting) => [meeting.id, meeting]));
|
|
3353
|
+
const changes = [];
|
|
3354
|
+
let createdCount = 0;
|
|
3355
|
+
let changedCount = 0;
|
|
3356
|
+
let removedCount = 0;
|
|
3357
|
+
let transcriptReadyCount = 0;
|
|
3358
|
+
for (const meeting of next) {
|
|
3359
|
+
const previousMeeting = previousById.get(meeting.id);
|
|
3360
|
+
if (!previousMeeting) {
|
|
3361
|
+
createdCount += 1;
|
|
3362
|
+
changes.push({
|
|
3363
|
+
kind: "created",
|
|
3364
|
+
meetingId: meeting.id,
|
|
3365
|
+
title: meeting.title,
|
|
3366
|
+
updatedAt: meeting.updatedAt
|
|
3367
|
+
});
|
|
3368
|
+
if (meeting.transcriptLoaded) {
|
|
3369
|
+
transcriptReadyCount += 1;
|
|
3370
|
+
changes.push({
|
|
3371
|
+
kind: "transcript-ready",
|
|
3372
|
+
meetingId: meeting.id,
|
|
3373
|
+
title: meeting.title,
|
|
3374
|
+
updatedAt: meeting.updatedAt
|
|
3375
|
+
});
|
|
3376
|
+
}
|
|
3377
|
+
continue;
|
|
3378
|
+
}
|
|
3379
|
+
if (meetingChanged(previousMeeting, meeting)) {
|
|
3380
|
+
changedCount += 1;
|
|
3381
|
+
changes.push({
|
|
3382
|
+
kind: "changed",
|
|
3383
|
+
meetingId: meeting.id,
|
|
3384
|
+
previousUpdatedAt: previousMeeting.updatedAt,
|
|
3385
|
+
title: meeting.title,
|
|
3386
|
+
updatedAt: meeting.updatedAt
|
|
3387
|
+
});
|
|
3388
|
+
}
|
|
3389
|
+
if (!previousMeeting.transcriptLoaded && meeting.transcriptLoaded) {
|
|
3390
|
+
transcriptReadyCount += 1;
|
|
3391
|
+
changes.push({
|
|
3392
|
+
kind: "transcript-ready",
|
|
3393
|
+
meetingId: meeting.id,
|
|
3394
|
+
previousUpdatedAt: previousMeeting.updatedAt,
|
|
3395
|
+
title: meeting.title,
|
|
3396
|
+
updatedAt: meeting.updatedAt
|
|
3397
|
+
});
|
|
3398
|
+
}
|
|
3399
|
+
}
|
|
3400
|
+
for (const meeting of previous) {
|
|
3401
|
+
if (nextById.has(meeting.id)) continue;
|
|
3402
|
+
removedCount += 1;
|
|
3403
|
+
changes.push({
|
|
3404
|
+
kind: "removed",
|
|
3405
|
+
meetingId: meeting.id,
|
|
3406
|
+
previousUpdatedAt: meeting.updatedAt,
|
|
3407
|
+
title: meeting.title
|
|
3408
|
+
});
|
|
3409
|
+
}
|
|
3410
|
+
return {
|
|
3411
|
+
changes,
|
|
3412
|
+
summary: {
|
|
3413
|
+
changedCount,
|
|
3414
|
+
createdCount,
|
|
3415
|
+
folderCount,
|
|
3416
|
+
meetingCount: next.length,
|
|
3417
|
+
removedCount,
|
|
3418
|
+
transcriptReadyCount
|
|
3419
|
+
}
|
|
3420
|
+
};
|
|
3421
|
+
}
|
|
3422
|
+
//#endregion
|
|
3255
3423
|
//#region src/app/core.ts
|
|
3256
3424
|
function transcriptCount(cacheData) {
|
|
3257
3425
|
return Object.values(cacheData.transcripts).filter((segments) => segments.length > 0).length;
|
|
@@ -3271,6 +3439,16 @@ function cloneExportJob(job) {
|
|
|
3271
3439
|
function cloneFolderSummary(folder) {
|
|
3272
3440
|
return { ...folder };
|
|
3273
3441
|
}
|
|
3442
|
+
function cloneSyncChange(change) {
|
|
3443
|
+
return { ...change };
|
|
3444
|
+
}
|
|
3445
|
+
function cloneSyncState(state) {
|
|
3446
|
+
return {
|
|
3447
|
+
...state,
|
|
3448
|
+
lastChanges: state.lastChanges.map(cloneSyncChange),
|
|
3449
|
+
summary: state.summary ? { ...state.summary } : void 0
|
|
3450
|
+
};
|
|
3451
|
+
}
|
|
3274
3452
|
function cloneMeetingSummary(meeting) {
|
|
3275
3453
|
return {
|
|
3276
3454
|
...meeting,
|
|
@@ -3295,6 +3473,7 @@ function cloneState(state) {
|
|
|
3295
3473
|
transcripts: cloneExportState(state.exports.transcripts)
|
|
3296
3474
|
},
|
|
3297
3475
|
index: { ...state.index },
|
|
3476
|
+
sync: cloneSyncState(state.sync),
|
|
3298
3477
|
ui: { ...state.ui }
|
|
3299
3478
|
};
|
|
3300
3479
|
}
|
|
@@ -3328,6 +3507,11 @@ function defaultState(config, auth, surface) {
|
|
|
3328
3507
|
loaded: false,
|
|
3329
3508
|
meetingCount: 0
|
|
3330
3509
|
},
|
|
3510
|
+
sync: {
|
|
3511
|
+
filePath: defaultSyncStateFilePath(),
|
|
3512
|
+
lastChanges: [],
|
|
3513
|
+
running: false
|
|
3514
|
+
},
|
|
3331
3515
|
ui: {
|
|
3332
3516
|
surface,
|
|
3333
3517
|
view: "idle"
|
|
@@ -3357,6 +3541,14 @@ var GranolaApp = class {
|
|
|
3357
3541
|
loadedAt: this.#meetingIndex.length > 0 ? this.nowIso() : void 0,
|
|
3358
3542
|
meetingCount: this.#meetingIndex.length
|
|
3359
3543
|
};
|
|
3544
|
+
this.#state.sync = {
|
|
3545
|
+
...this.#state.sync,
|
|
3546
|
+
...cloneSyncState(deps.syncState ?? {
|
|
3547
|
+
filePath: defaultSyncStateFilePath(),
|
|
3548
|
+
lastChanges: [],
|
|
3549
|
+
running: false
|
|
3550
|
+
})
|
|
3551
|
+
};
|
|
3360
3552
|
}
|
|
3361
3553
|
getState() {
|
|
3362
3554
|
return cloneState(this.#state);
|
|
@@ -3378,16 +3570,39 @@ var GranolaApp = class {
|
|
|
3378
3570
|
resetRemoteState() {
|
|
3379
3571
|
this.#granolaClient = void 0;
|
|
3380
3572
|
this.#folders = void 0;
|
|
3573
|
+
this.#documents = void 0;
|
|
3574
|
+
this.resetDocumentsState();
|
|
3575
|
+
this.resetFoldersState();
|
|
3576
|
+
}
|
|
3577
|
+
resetDocumentsState() {
|
|
3381
3578
|
this.#documents = void 0;
|
|
3382
3579
|
this.#state.documents = {
|
|
3383
3580
|
count: 0,
|
|
3384
3581
|
loaded: false
|
|
3385
3582
|
};
|
|
3583
|
+
}
|
|
3584
|
+
resetFoldersState() {
|
|
3585
|
+
this.#folders = void 0;
|
|
3386
3586
|
this.#state.folders = {
|
|
3387
3587
|
count: 0,
|
|
3388
3588
|
loaded: false
|
|
3389
3589
|
};
|
|
3390
3590
|
}
|
|
3591
|
+
resetCacheState() {
|
|
3592
|
+
this.#cacheData = void 0;
|
|
3593
|
+
this.#cacheResolved = false;
|
|
3594
|
+
this.#state.cache = {
|
|
3595
|
+
configured: Boolean(this.config.transcripts.cacheFile),
|
|
3596
|
+
documentCount: 0,
|
|
3597
|
+
filePath: this.config.transcripts.cacheFile || void 0,
|
|
3598
|
+
loaded: false,
|
|
3599
|
+
transcriptCount: 0
|
|
3600
|
+
};
|
|
3601
|
+
}
|
|
3602
|
+
async persistSyncState() {
|
|
3603
|
+
if (!this.deps.syncStateStore) return;
|
|
3604
|
+
await this.deps.syncStateStore.writeState(this.#state.sync);
|
|
3605
|
+
}
|
|
3391
3606
|
applyAuthState(auth, options = {}) {
|
|
3392
3607
|
if (options.resetDocuments) this.resetRemoteState();
|
|
3393
3608
|
this.#state.auth = { ...auth };
|
|
@@ -3410,23 +3625,27 @@ var GranolaApp = class {
|
|
|
3410
3625
|
if (this.deps.meetingIndexStore) await this.deps.meetingIndexStore.writeIndex(this.#meetingIndex);
|
|
3411
3626
|
this.emitStateUpdate();
|
|
3412
3627
|
}
|
|
3413
|
-
async
|
|
3414
|
-
const cacheData = await this.loadCache();
|
|
3415
|
-
const documents = await this.listDocuments();
|
|
3416
|
-
const folders = await this.loadFolders();
|
|
3417
|
-
|
|
3628
|
+
async liveMeetingSnapshot(options = {}) {
|
|
3629
|
+
const cacheData = await this.loadCache({ forceRefresh: options.forceRefresh });
|
|
3630
|
+
const documents = await this.listDocuments({ forceRefresh: options.forceRefresh });
|
|
3631
|
+
const folders = await this.loadFolders({ forceRefresh: options.forceRefresh });
|
|
3632
|
+
return {
|
|
3418
3633
|
cacheData,
|
|
3419
|
-
|
|
3420
|
-
|
|
3421
|
-
|
|
3422
|
-
|
|
3423
|
-
|
|
3634
|
+
documents,
|
|
3635
|
+
folders,
|
|
3636
|
+
meetings: listMeetings(documents, {
|
|
3637
|
+
cacheData,
|
|
3638
|
+
foldersByDocumentId: this.buildFoldersByDocumentId(folders),
|
|
3639
|
+
limit: Math.max(documents.length, 1),
|
|
3640
|
+
sort: "updated-desc"
|
|
3641
|
+
})
|
|
3642
|
+
};
|
|
3424
3643
|
}
|
|
3425
3644
|
triggerMeetingIndexRefresh() {
|
|
3426
3645
|
if (this.#refreshingMeetingIndex) return;
|
|
3427
3646
|
this.#refreshingMeetingIndex = (async () => {
|
|
3428
3647
|
try {
|
|
3429
|
-
await this.
|
|
3648
|
+
await this.runSync({ foreground: false });
|
|
3430
3649
|
} catch {} finally {
|
|
3431
3650
|
this.#refreshingMeetingIndex = void 0;
|
|
3432
3651
|
}
|
|
@@ -3471,7 +3690,7 @@ var GranolaApp = class {
|
|
|
3471
3690
|
}
|
|
3472
3691
|
async loadFolders(options = {}) {
|
|
3473
3692
|
if (options.forceRefresh) {
|
|
3474
|
-
this.
|
|
3693
|
+
this.resetFoldersState();
|
|
3475
3694
|
this.emitStateUpdate();
|
|
3476
3695
|
}
|
|
3477
3696
|
if (this.#folders) return this.#folders.map((folder) => ({
|
|
@@ -3565,6 +3784,9 @@ var GranolaApp = class {
|
|
|
3565
3784
|
const auth = await this.deps.authController.inspect();
|
|
3566
3785
|
return this.applyAuthState(auth, { view: "auth" });
|
|
3567
3786
|
}
|
|
3787
|
+
async inspectSync() {
|
|
3788
|
+
return cloneSyncState(this.#state.sync);
|
|
3789
|
+
}
|
|
3568
3790
|
async loginAuth(options = {}) {
|
|
3569
3791
|
const controller = this.requireAuthController();
|
|
3570
3792
|
try {
|
|
@@ -3614,9 +3836,56 @@ var GranolaApp = class {
|
|
|
3614
3836
|
throw error;
|
|
3615
3837
|
}
|
|
3616
3838
|
}
|
|
3839
|
+
async runSync(options) {
|
|
3840
|
+
const previousMeetings = this.#meetingIndex.map((meeting) => cloneMeetingSummary(meeting));
|
|
3841
|
+
this.#state.sync = {
|
|
3842
|
+
...this.#state.sync,
|
|
3843
|
+
lastError: void 0,
|
|
3844
|
+
lastStartedAt: this.nowIso(),
|
|
3845
|
+
running: true
|
|
3846
|
+
};
|
|
3847
|
+
if (options.foreground) this.setUiState({ view: "sync" });
|
|
3848
|
+
else this.emitStateUpdate();
|
|
3849
|
+
try {
|
|
3850
|
+
const snapshot = await this.liveMeetingSnapshot({ forceRefresh: options.forceRefresh ?? true });
|
|
3851
|
+
await this.persistMeetingIndex(snapshot.meetings);
|
|
3852
|
+
const { changes, summary } = diffMeetingSummaries(previousMeetings, snapshot.meetings, snapshot.folders?.length ?? 0);
|
|
3853
|
+
this.#state.sync = {
|
|
3854
|
+
...this.#state.sync,
|
|
3855
|
+
lastChanges: changes.slice(0, 50).map(cloneSyncChange),
|
|
3856
|
+
lastCompletedAt: this.nowIso(),
|
|
3857
|
+
lastError: void 0,
|
|
3858
|
+
running: false,
|
|
3859
|
+
summary: { ...summary }
|
|
3860
|
+
};
|
|
3861
|
+
await this.persistSyncState();
|
|
3862
|
+
this.emitStateUpdate();
|
|
3863
|
+
return {
|
|
3864
|
+
changes: changes.map(cloneSyncChange),
|
|
3865
|
+
state: cloneSyncState(this.#state.sync),
|
|
3866
|
+
summary: { ...summary }
|
|
3867
|
+
};
|
|
3868
|
+
} catch (error) {
|
|
3869
|
+
this.#state.sync = {
|
|
3870
|
+
...this.#state.sync,
|
|
3871
|
+
lastError: error instanceof Error ? error.message : String(error),
|
|
3872
|
+
lastFailedAt: this.nowIso(),
|
|
3873
|
+
running: false
|
|
3874
|
+
};
|
|
3875
|
+
await this.persistSyncState();
|
|
3876
|
+
this.emitStateUpdate();
|
|
3877
|
+
throw error;
|
|
3878
|
+
}
|
|
3879
|
+
}
|
|
3880
|
+
async sync(options = {}) {
|
|
3881
|
+
return await this.runSync({
|
|
3882
|
+
forceRefresh: options.forceRefresh,
|
|
3883
|
+
foreground: true
|
|
3884
|
+
});
|
|
3885
|
+
}
|
|
3617
3886
|
async listDocuments(options = {}) {
|
|
3618
3887
|
if (options.forceRefresh) {
|
|
3619
|
-
this.
|
|
3888
|
+
this.resetDocumentsState();
|
|
3620
3889
|
this.emitStateUpdate();
|
|
3621
3890
|
}
|
|
3622
3891
|
if (this.#documents) return this.#documents;
|
|
@@ -3631,6 +3900,10 @@ var GranolaApp = class {
|
|
|
3631
3900
|
return documents;
|
|
3632
3901
|
}
|
|
3633
3902
|
async loadCache(options = {}) {
|
|
3903
|
+
if (options.forceRefresh) {
|
|
3904
|
+
this.resetCacheState();
|
|
3905
|
+
this.emitStateUpdate();
|
|
3906
|
+
}
|
|
3634
3907
|
if (this.#cacheResolved) {
|
|
3635
3908
|
if (options.required && !this.#cacheData) throw this.missingCacheError();
|
|
3636
3909
|
return this.#cacheData;
|
|
@@ -3717,28 +3990,19 @@ var GranolaApp = class {
|
|
|
3717
3990
|
source: "index"
|
|
3718
3991
|
};
|
|
3719
3992
|
}
|
|
3720
|
-
const
|
|
3721
|
-
|
|
3722
|
-
const
|
|
3723
|
-
|
|
3724
|
-
required: Boolean(options.folderId)
|
|
3725
|
-
});
|
|
3726
|
-
const meetings = listMeetings(documents, {
|
|
3727
|
-
cacheData,
|
|
3993
|
+
const snapshot = await this.liveMeetingSnapshot({ forceRefresh: options.forceRefresh });
|
|
3994
|
+
if (options.folderId && !snapshot.folders) throw new Error("Granola folder API is not configured");
|
|
3995
|
+
const meetings = listMeetings(snapshot.documents, {
|
|
3996
|
+
cacheData: snapshot.cacheData,
|
|
3728
3997
|
folderId: options.folderId,
|
|
3729
|
-
foldersByDocumentId: this.buildFoldersByDocumentId(folders),
|
|
3998
|
+
foldersByDocumentId: this.buildFoldersByDocumentId(snapshot.folders),
|
|
3730
3999
|
limit: options.limit,
|
|
3731
4000
|
search: options.search,
|
|
3732
4001
|
sort: options.sort,
|
|
3733
4002
|
updatedFrom: options.updatedFrom,
|
|
3734
4003
|
updatedTo: options.updatedTo
|
|
3735
4004
|
});
|
|
3736
|
-
await this.persistMeetingIndex(
|
|
3737
|
-
cacheData,
|
|
3738
|
-
foldersByDocumentId: this.buildFoldersByDocumentId(folders),
|
|
3739
|
-
limit: Math.max(documents.length, 1),
|
|
3740
|
-
sort: "updated-desc"
|
|
3741
|
-
}));
|
|
4005
|
+
await this.persistMeetingIndex(snapshot.meetings);
|
|
3742
4006
|
this.setUiState({
|
|
3743
4007
|
folderSearch: void 0,
|
|
3744
4008
|
meetingListSource: "live",
|
|
@@ -3938,6 +4202,9 @@ async function createGranolaApp(config, options = {}) {
|
|
|
3938
4202
|
const exportJobStore = createDefaultExportJobStore();
|
|
3939
4203
|
const exportJobs = await exportJobStore.readJobs();
|
|
3940
4204
|
const meetingIndexStore = createDefaultMeetingIndexStore();
|
|
4205
|
+
const meetingIndex = await meetingIndexStore.readIndex();
|
|
4206
|
+
const syncStateStore = createDefaultSyncStateStore();
|
|
4207
|
+
const syncState = await syncStateStore.readState();
|
|
3941
4208
|
return new GranolaApp(config, {
|
|
3942
4209
|
auth,
|
|
3943
4210
|
authController,
|
|
@@ -3945,9 +4212,11 @@ async function createGranolaApp(config, options = {}) {
|
|
|
3945
4212
|
createGranolaClient: async (mode) => await createDefaultGranolaRuntime(config, options.logger, { preferredMode: mode }),
|
|
3946
4213
|
exportJobs,
|
|
3947
4214
|
exportJobStore,
|
|
3948
|
-
meetingIndex
|
|
4215
|
+
meetingIndex,
|
|
3949
4216
|
meetingIndexStore,
|
|
3950
|
-
now: options.now
|
|
4217
|
+
now: options.now,
|
|
4218
|
+
syncState,
|
|
4219
|
+
syncStateStore
|
|
3951
4220
|
}, { surface: options.surface });
|
|
3952
4221
|
}
|
|
3953
4222
|
//#endregion
|
|
@@ -4414,10 +4683,115 @@ async function openExternalUrl(url, options = {}) {
|
|
|
4414
4683
|
}))(command.file, command.args);
|
|
4415
4684
|
}
|
|
4416
4685
|
//#endregion
|
|
4686
|
+
//#region src/web/client-state.ts
|
|
4687
|
+
function parseWorkspaceTab(value) {
|
|
4688
|
+
switch (value) {
|
|
4689
|
+
case "metadata":
|
|
4690
|
+
case "raw":
|
|
4691
|
+
case "transcript": return value;
|
|
4692
|
+
default: return "notes";
|
|
4693
|
+
}
|
|
4694
|
+
}
|
|
4695
|
+
function startupSelectionFromSearch(search) {
|
|
4696
|
+
const params = new URLSearchParams(search);
|
|
4697
|
+
return {
|
|
4698
|
+
folderId: params.get("folder")?.trim() || "",
|
|
4699
|
+
meetingId: params.get("meeting")?.trim() || "",
|
|
4700
|
+
workspaceTab: parseWorkspaceTab(params.get("tab"))
|
|
4701
|
+
};
|
|
4702
|
+
}
|
|
4703
|
+
function buildBrowserUrlPath(currentHref, selection) {
|
|
4704
|
+
const url = new URL(currentHref);
|
|
4705
|
+
if (selection.selectedFolderId) url.searchParams.set("folder", selection.selectedFolderId);
|
|
4706
|
+
else url.searchParams.delete("folder");
|
|
4707
|
+
if (selection.selectedMeetingId) url.searchParams.set("meeting", selection.selectedMeetingId);
|
|
4708
|
+
else url.searchParams.delete("meeting");
|
|
4709
|
+
if (parseWorkspaceTab(selection.workspaceTab) !== "notes") url.searchParams.set("tab", parseWorkspaceTab(selection.workspaceTab));
|
|
4710
|
+
else url.searchParams.delete("tab");
|
|
4711
|
+
return `${url.pathname}${url.search}${url.hash}`;
|
|
4712
|
+
}
|
|
4713
|
+
function exportScopeLabel(scope) {
|
|
4714
|
+
return scope && scope.mode === "folder" ? `Folder: ${scope.folderName || scope.folderId}` : "Scope: All meetings";
|
|
4715
|
+
}
|
|
4716
|
+
function currentFilterSummary(filters) {
|
|
4717
|
+
const parts = [];
|
|
4718
|
+
if (filters.selectedFolderId) {
|
|
4719
|
+
const folder = filters.folders.find((candidate) => candidate.id === filters.selectedFolderId);
|
|
4720
|
+
parts.push(`folder "${folder ? folder.name : filters.selectedFolderId}"`);
|
|
4721
|
+
}
|
|
4722
|
+
if (filters.search) parts.push(`search "${filters.search}"`);
|
|
4723
|
+
if (filters.updatedFrom) parts.push(`from ${filters.updatedFrom}`);
|
|
4724
|
+
if (filters.updatedTo) parts.push(`to ${filters.updatedTo}`);
|
|
4725
|
+
return parts.join(", ");
|
|
4726
|
+
}
|
|
4727
|
+
function selectMeetingId(meetings, selectedMeetingId) {
|
|
4728
|
+
if (selectedMeetingId && meetings.some((meeting) => meeting.id === selectedMeetingId)) return selectedMeetingId;
|
|
4729
|
+
return meetings[0]?.id ?? null;
|
|
4730
|
+
}
|
|
4731
|
+
function buildMeetingsQuery(filters, options = {}) {
|
|
4732
|
+
const params = new URLSearchParams();
|
|
4733
|
+
params.set("limit", String(options.limit ?? 100));
|
|
4734
|
+
params.set("sort", filters.sort || "updated-desc");
|
|
4735
|
+
if (filters.search) params.set("search", filters.search);
|
|
4736
|
+
if (filters.updatedFrom) params.set("updatedFrom", filters.updatedFrom);
|
|
4737
|
+
if (filters.updatedTo) params.set("updatedTo", filters.updatedTo);
|
|
4738
|
+
if (filters.selectedFolderId) params.set("folderId", filters.selectedFolderId);
|
|
4739
|
+
if (options.refresh) params.set("refresh", "true");
|
|
4740
|
+
return `?${params.toString()}`;
|
|
4741
|
+
}
|
|
4742
|
+
function buildNotesExportRequest(selectedFolderId) {
|
|
4743
|
+
return {
|
|
4744
|
+
folderId: selectedFolderId || void 0,
|
|
4745
|
+
format: "markdown"
|
|
4746
|
+
};
|
|
4747
|
+
}
|
|
4748
|
+
function buildTranscriptsExportRequest(selectedFolderId) {
|
|
4749
|
+
return {
|
|
4750
|
+
folderId: selectedFolderId || void 0,
|
|
4751
|
+
format: "text"
|
|
4752
|
+
};
|
|
4753
|
+
}
|
|
4754
|
+
function nextWorkspaceTab(currentTab, key) {
|
|
4755
|
+
const current = parseWorkspaceTab(currentTab);
|
|
4756
|
+
switch (key) {
|
|
4757
|
+
case "1": return "notes";
|
|
4758
|
+
case "2": return "transcript";
|
|
4759
|
+
case "3": return "metadata";
|
|
4760
|
+
case "4": return "raw";
|
|
4761
|
+
case "]":
|
|
4762
|
+
switch (current) {
|
|
4763
|
+
case "notes": return "transcript";
|
|
4764
|
+
case "transcript": return "metadata";
|
|
4765
|
+
case "metadata": return "raw";
|
|
4766
|
+
case "raw": return "notes";
|
|
4767
|
+
}
|
|
4768
|
+
break;
|
|
4769
|
+
case "[":
|
|
4770
|
+
switch (current) {
|
|
4771
|
+
case "notes": return "raw";
|
|
4772
|
+
case "transcript": return "notes";
|
|
4773
|
+
case "metadata": return "transcript";
|
|
4774
|
+
case "raw": return "metadata";
|
|
4775
|
+
}
|
|
4776
|
+
break;
|
|
4777
|
+
default: return;
|
|
4778
|
+
}
|
|
4779
|
+
}
|
|
4780
|
+
//#endregion
|
|
4417
4781
|
//#region src/web/client-script.ts
|
|
4418
4782
|
const granolaWebClientScript = String.raw`
|
|
4419
4783
|
const serverConfig = window.__GRANOLA_SERVER__ || { passwordRequired: false };
|
|
4420
4784
|
const workspaceTabs = ["notes", "transcript", "metadata", "raw"];
|
|
4785
|
+
${parseWorkspaceTab.toString()}
|
|
4786
|
+
${startupSelectionFromSearch.toString()}
|
|
4787
|
+
${buildBrowserUrlPath.toString()}
|
|
4788
|
+
${exportScopeLabel.toString()}
|
|
4789
|
+
${currentFilterSummary.toString()}
|
|
4790
|
+
${selectMeetingId.toString()}
|
|
4791
|
+
${buildMeetingsQuery.toString()}
|
|
4792
|
+
${buildNotesExportRequest.toString()}
|
|
4793
|
+
${buildTranscriptsExportRequest.toString()}
|
|
4794
|
+
${nextWorkspaceTab.toString()}
|
|
4421
4795
|
|
|
4422
4796
|
const state = {
|
|
4423
4797
|
appState: null,
|
|
@@ -4466,41 +4840,12 @@ const els = {
|
|
|
4466
4840
|
workspaceTabs: document.querySelectorAll("[data-workspace-tab]"),
|
|
4467
4841
|
};
|
|
4468
4842
|
|
|
4469
|
-
function parseWorkspaceTab(value) {
|
|
4470
|
-
return workspaceTabs.includes(value) ? value : "notes";
|
|
4471
|
-
}
|
|
4472
|
-
|
|
4473
|
-
function startupSelection() {
|
|
4474
|
-
const params = new URLSearchParams(window.location.search);
|
|
4475
|
-
return {
|
|
4476
|
-
folderId: params.get("folder")?.trim() || "",
|
|
4477
|
-
meetingId: params.get("meeting")?.trim() || "",
|
|
4478
|
-
workspaceTab: parseWorkspaceTab(params.get("tab")),
|
|
4479
|
-
};
|
|
4480
|
-
}
|
|
4481
|
-
|
|
4482
4843
|
function syncBrowserUrl() {
|
|
4483
|
-
const
|
|
4484
|
-
|
|
4485
|
-
|
|
4486
|
-
|
|
4487
|
-
}
|
|
4488
|
-
url.searchParams.delete("folder");
|
|
4489
|
-
}
|
|
4490
|
-
|
|
4491
|
-
if (state.selectedMeetingId) {
|
|
4492
|
-
url.searchParams.set("meeting", state.selectedMeetingId);
|
|
4493
|
-
} else {
|
|
4494
|
-
url.searchParams.delete("meeting");
|
|
4495
|
-
}
|
|
4496
|
-
|
|
4497
|
-
if (state.workspaceTab !== "notes") {
|
|
4498
|
-
url.searchParams.set("tab", state.workspaceTab);
|
|
4499
|
-
} else {
|
|
4500
|
-
url.searchParams.delete("tab");
|
|
4501
|
-
}
|
|
4502
|
-
|
|
4503
|
-
const nextPath = url.pathname + url.search + url.hash;
|
|
4844
|
+
const nextPath = buildBrowserUrlPath(window.location.href, {
|
|
4845
|
+
selectedFolderId: state.selectedFolderId,
|
|
4846
|
+
selectedMeetingId: state.selectedMeetingId,
|
|
4847
|
+
workspaceTab: state.workspaceTab,
|
|
4848
|
+
});
|
|
4504
4849
|
const currentPath = window.location.pathname + window.location.search + window.location.hash;
|
|
4505
4850
|
if (nextPath !== currentPath) {
|
|
4506
4851
|
history.replaceState(null, "", nextPath);
|
|
@@ -4515,12 +4860,6 @@ function escapeHtml(value) {
|
|
|
4515
4860
|
.replaceAll('"', """);
|
|
4516
4861
|
}
|
|
4517
4862
|
|
|
4518
|
-
function exportScopeLabel(scope) {
|
|
4519
|
-
return scope && scope.mode === "folder"
|
|
4520
|
-
? "Folder: " + (scope.folderName || scope.folderId)
|
|
4521
|
-
: "Scope: All meetings";
|
|
4522
|
-
}
|
|
4523
|
-
|
|
4524
4863
|
function setStatus(label, tone = "idle") {
|
|
4525
4864
|
els.stateBadge.textContent = label;
|
|
4526
4865
|
els.stateBadge.dataset.tone = tone;
|
|
@@ -4534,29 +4873,6 @@ function syncFilterInputs() {
|
|
|
4534
4873
|
els.updatedTo.value = state.updatedTo;
|
|
4535
4874
|
}
|
|
4536
4875
|
|
|
4537
|
-
function currentFilterSummary() {
|
|
4538
|
-
const parts = [];
|
|
4539
|
-
|
|
4540
|
-
if (state.selectedFolderId) {
|
|
4541
|
-
const folder = state.folders.find((candidate) => candidate.id === state.selectedFolderId);
|
|
4542
|
-
parts.push("folder " + (folder ? '"' + folder.name + '"' : '"' + state.selectedFolderId + '"'));
|
|
4543
|
-
}
|
|
4544
|
-
|
|
4545
|
-
if (state.search) {
|
|
4546
|
-
parts.push('search "' + state.search + '"');
|
|
4547
|
-
}
|
|
4548
|
-
|
|
4549
|
-
if (state.updatedFrom) {
|
|
4550
|
-
parts.push("from " + state.updatedFrom);
|
|
4551
|
-
}
|
|
4552
|
-
|
|
4553
|
-
if (state.updatedTo) {
|
|
4554
|
-
parts.push("to " + state.updatedTo);
|
|
4555
|
-
}
|
|
4556
|
-
|
|
4557
|
-
return parts.join(", ");
|
|
4558
|
-
}
|
|
4559
|
-
|
|
4560
4876
|
function renderWorkspaceTabs() {
|
|
4561
4877
|
for (const button of els.workspaceTabs) {
|
|
4562
4878
|
button.dataset.selected = button.dataset.workspaceTab === state.workspaceTab ? "true" : "false";
|
|
@@ -4730,7 +5046,13 @@ function renderMeetingList() {
|
|
|
4730
5046
|
state.selectedMeeting = null;
|
|
4731
5047
|
state.selectedMeetingBundle = null;
|
|
4732
5048
|
syncBrowserUrl();
|
|
4733
|
-
const filterSummary = currentFilterSummary(
|
|
5049
|
+
const filterSummary = currentFilterSummary({
|
|
5050
|
+
folders: state.folders,
|
|
5051
|
+
search: state.search,
|
|
5052
|
+
selectedFolderId: state.selectedFolderId,
|
|
5053
|
+
updatedFrom: state.updatedFrom,
|
|
5054
|
+
updatedTo: state.updatedTo,
|
|
5055
|
+
});
|
|
4734
5056
|
const message = filterSummary
|
|
4735
5057
|
? "No meetings match " + filterSummary + "."
|
|
4736
5058
|
: "No meetings yet. Try Refresh.";
|
|
@@ -4739,10 +5061,7 @@ function renderMeetingList() {
|
|
|
4739
5061
|
return;
|
|
4740
5062
|
}
|
|
4741
5063
|
|
|
4742
|
-
|
|
4743
|
-
if (!state.selectedMeetingId || !visibleIds.has(state.selectedMeetingId)) {
|
|
4744
|
-
state.selectedMeetingId = state.meetings[0]?.id || null;
|
|
4745
|
-
}
|
|
5064
|
+
state.selectedMeetingId = selectMeetingId(state.meetings, state.selectedMeetingId);
|
|
4746
5065
|
syncBrowserUrl();
|
|
4747
5066
|
|
|
4748
5067
|
els.list.innerHTML = state.meetings
|
|
@@ -4886,34 +5205,6 @@ async function fetchJson(path, init) {
|
|
|
4886
5205
|
return payload;
|
|
4887
5206
|
}
|
|
4888
5207
|
|
|
4889
|
-
function buildMeetingsQuery(limit = 100, refresh = false) {
|
|
4890
|
-
const params = new URLSearchParams();
|
|
4891
|
-
params.set("limit", String(limit));
|
|
4892
|
-
params.set("sort", state.sort);
|
|
4893
|
-
|
|
4894
|
-
if (state.search) {
|
|
4895
|
-
params.set("search", state.search);
|
|
4896
|
-
}
|
|
4897
|
-
|
|
4898
|
-
if (state.updatedFrom) {
|
|
4899
|
-
params.set("updatedFrom", state.updatedFrom);
|
|
4900
|
-
}
|
|
4901
|
-
|
|
4902
|
-
if (state.updatedTo) {
|
|
4903
|
-
params.set("updatedTo", state.updatedTo);
|
|
4904
|
-
}
|
|
4905
|
-
|
|
4906
|
-
if (state.selectedFolderId) {
|
|
4907
|
-
params.set("folderId", state.selectedFolderId);
|
|
4908
|
-
}
|
|
4909
|
-
|
|
4910
|
-
if (refresh) {
|
|
4911
|
-
params.set("refresh", "true");
|
|
4912
|
-
}
|
|
4913
|
-
|
|
4914
|
-
return "?" + params.toString();
|
|
4915
|
-
}
|
|
4916
|
-
|
|
4917
5208
|
async function loadFolders(options = {}) {
|
|
4918
5209
|
const refresh = options.refresh === true;
|
|
4919
5210
|
|
|
@@ -4953,7 +5244,22 @@ async function loadMeetings(options = {}) {
|
|
|
4953
5244
|
|
|
4954
5245
|
try {
|
|
4955
5246
|
state.listError = "";
|
|
4956
|
-
const payload = await fetchJson(
|
|
5247
|
+
const payload = await fetchJson(
|
|
5248
|
+
"/meetings" +
|
|
5249
|
+
buildMeetingsQuery(
|
|
5250
|
+
{
|
|
5251
|
+
search: state.search,
|
|
5252
|
+
selectedFolderId: state.selectedFolderId,
|
|
5253
|
+
sort: state.sort,
|
|
5254
|
+
updatedFrom: state.updatedFrom,
|
|
5255
|
+
updatedTo: state.updatedTo,
|
|
5256
|
+
},
|
|
5257
|
+
{
|
|
5258
|
+
limit: 100,
|
|
5259
|
+
refresh,
|
|
5260
|
+
},
|
|
5261
|
+
),
|
|
5262
|
+
);
|
|
4957
5263
|
state.meetings = payload.meetings || [];
|
|
4958
5264
|
state.meetingSource = payload.source || "live";
|
|
4959
5265
|
|
|
@@ -5063,10 +5369,7 @@ async function syncAuthState() {
|
|
|
5063
5369
|
async function exportNotes() {
|
|
5064
5370
|
setStatus(state.selectedFolderId ? "Exporting folder notes…" : "Exporting notes…", "busy");
|
|
5065
5371
|
await fetchJson("/exports/notes", {
|
|
5066
|
-
body: JSON.stringify(
|
|
5067
|
-
folderId: state.selectedFolderId || undefined,
|
|
5068
|
-
format: "markdown",
|
|
5069
|
-
}),
|
|
5372
|
+
body: JSON.stringify(buildNotesExportRequest(state.selectedFolderId)),
|
|
5070
5373
|
headers: { "content-type": "application/json" },
|
|
5071
5374
|
method: "POST",
|
|
5072
5375
|
});
|
|
@@ -5079,10 +5382,7 @@ async function exportTranscripts() {
|
|
|
5079
5382
|
"busy",
|
|
5080
5383
|
);
|
|
5081
5384
|
await fetchJson("/exports/transcripts", {
|
|
5082
|
-
body: JSON.stringify(
|
|
5083
|
-
folderId: state.selectedFolderId || undefined,
|
|
5084
|
-
format: "text",
|
|
5085
|
-
}),
|
|
5385
|
+
body: JSON.stringify(buildTranscriptsExportRequest(state.selectedFolderId)),
|
|
5086
5386
|
headers: { "content-type": "application/json" },
|
|
5087
5387
|
method: "POST",
|
|
5088
5388
|
});
|
|
@@ -5386,50 +5686,15 @@ document.addEventListener("keydown", (event) => {
|
|
|
5386
5686
|
return;
|
|
5387
5687
|
}
|
|
5388
5688
|
|
|
5389
|
-
const
|
|
5390
|
-
if (
|
|
5391
|
-
state.workspaceTab =
|
|
5392
|
-
syncBrowserUrl();
|
|
5393
|
-
renderMeetingDetail();
|
|
5394
|
-
return;
|
|
5395
|
-
}
|
|
5396
|
-
|
|
5397
|
-
if (event.key === "2") {
|
|
5398
|
-
state.workspaceTab = "transcript";
|
|
5399
|
-
syncBrowserUrl();
|
|
5400
|
-
renderMeetingDetail();
|
|
5401
|
-
return;
|
|
5402
|
-
}
|
|
5403
|
-
|
|
5404
|
-
if (event.key === "3") {
|
|
5405
|
-
state.workspaceTab = "metadata";
|
|
5406
|
-
syncBrowserUrl();
|
|
5407
|
-
renderMeetingDetail();
|
|
5408
|
-
return;
|
|
5409
|
-
}
|
|
5410
|
-
|
|
5411
|
-
if (event.key === "4") {
|
|
5412
|
-
state.workspaceTab = "raw";
|
|
5413
|
-
syncBrowserUrl();
|
|
5414
|
-
renderMeetingDetail();
|
|
5415
|
-
return;
|
|
5416
|
-
}
|
|
5417
|
-
|
|
5418
|
-
const currentIndex = tabs.indexOf(state.workspaceTab);
|
|
5419
|
-
if (event.key === "]") {
|
|
5420
|
-
state.workspaceTab = tabs[(currentIndex + 1) % tabs.length];
|
|
5421
|
-
syncBrowserUrl();
|
|
5422
|
-
renderMeetingDetail();
|
|
5423
|
-
}
|
|
5424
|
-
|
|
5425
|
-
if (event.key === "[") {
|
|
5426
|
-
state.workspaceTab = tabs[(currentIndex + tabs.length - 1) % tabs.length];
|
|
5689
|
+
const nextTab = nextWorkspaceTab(state.workspaceTab, event.key);
|
|
5690
|
+
if (nextTab) {
|
|
5691
|
+
state.workspaceTab = nextTab;
|
|
5427
5692
|
syncBrowserUrl();
|
|
5428
5693
|
renderMeetingDetail();
|
|
5429
5694
|
}
|
|
5430
5695
|
});
|
|
5431
5696
|
|
|
5432
|
-
const initialSelection =
|
|
5697
|
+
const initialSelection = startupSelectionFromSearch(window.location.search);
|
|
5433
5698
|
state.selectedFolderId = initialSelection.folderId || null;
|
|
5434
5699
|
state.selectedMeetingId = initialSelection.meetingId || null;
|
|
5435
5700
|
state.workspaceTab = initialSelection.workspaceTab;
|
|
@@ -6294,12 +6559,14 @@ async function startGranolaServer(app, options = {}) {
|
|
|
6294
6559
|
exports: true,
|
|
6295
6560
|
folders: true,
|
|
6296
6561
|
meetingOpen: true,
|
|
6562
|
+
sync: true,
|
|
6297
6563
|
webClient: enableWebClient
|
|
6298
6564
|
},
|
|
6299
6565
|
persistence: {
|
|
6300
6566
|
exportJobs: true,
|
|
6301
6567
|
meetingIndex: true,
|
|
6302
|
-
sessionStore: defaultGranolaToolkitPersistenceLayout().sessionStoreKind
|
|
6568
|
+
sessionStore: defaultGranolaToolkitPersistenceLayout().sessionStoreKind,
|
|
6569
|
+
syncState: true
|
|
6303
6570
|
},
|
|
6304
6571
|
product: "granola-toolkit",
|
|
6305
6572
|
protocolVersion: 2,
|
|
@@ -6395,6 +6662,11 @@ async function startGranolaServer(app, options = {}) {
|
|
|
6395
6662
|
sendJson(response, await app.inspectAuth(), { headers: originHeaders });
|
|
6396
6663
|
return;
|
|
6397
6664
|
}
|
|
6665
|
+
if (method === "POST" && path === granolaTransportPaths.syncRun) {
|
|
6666
|
+
const body = await readJsonBody(request);
|
|
6667
|
+
sendJson(response, await app.sync({ forceRefresh: typeof body.forceRefresh === "boolean" ? body.forceRefresh : void 0 }), { headers: originHeaders });
|
|
6668
|
+
return;
|
|
6669
|
+
}
|
|
6398
6670
|
if (method === "POST" && path === granolaTransportPaths.authLock) {
|
|
6399
6671
|
sendJson(response, { ok: true }, { headers: {
|
|
6400
6672
|
...originHeaders,
|
|
@@ -6624,6 +6896,7 @@ function printWebRoutes() {
|
|
|
6624
6896
|
console.log(" POST /exports/notes");
|
|
6625
6897
|
console.log(" POST /exports/jobs/:id/rerun");
|
|
6626
6898
|
console.log(" POST /exports/transcripts");
|
|
6899
|
+
console.log(" POST /sync");
|
|
6627
6900
|
}
|
|
6628
6901
|
async function runGranolaWebWorkspace(app, options) {
|
|
6629
6902
|
const server = await startGranolaServer(app, {
|
|
@@ -7052,6 +7325,7 @@ const serveCommand = {
|
|
|
7052
7325
|
console.log(" POST /exports/notes");
|
|
7053
7326
|
console.log(" POST /exports/jobs/:id/rerun");
|
|
7054
7327
|
console.log(" POST /exports/transcripts");
|
|
7328
|
+
console.log(" POST /sync");
|
|
7055
7329
|
console.log(`Attach: granola attach ${server.url.href}`);
|
|
7056
7330
|
if (password) console.log("Attach password: add --password <value>");
|
|
7057
7331
|
await waitForShutdown(async () => await server.close());
|
|
@@ -7059,6 +7333,57 @@ const serveCommand = {
|
|
|
7059
7333
|
}
|
|
7060
7334
|
};
|
|
7061
7335
|
//#endregion
|
|
7336
|
+
//#region src/commands/sync.ts
|
|
7337
|
+
function syncHelp() {
|
|
7338
|
+
return `Granola sync
|
|
7339
|
+
|
|
7340
|
+
Usage:
|
|
7341
|
+
granola sync [options]
|
|
7342
|
+
|
|
7343
|
+
Options:
|
|
7344
|
+
--cache <path> Path to Granola cache JSON
|
|
7345
|
+
--timeout <value> Request timeout, e.g. 2m, 30s, 120000 (default: 2m)
|
|
7346
|
+
--supabase <path> Path to supabase.json
|
|
7347
|
+
--debug Enable debug logging
|
|
7348
|
+
--config <path> Path to .granola.toml
|
|
7349
|
+
-h, --help Show help
|
|
7350
|
+
`;
|
|
7351
|
+
}
|
|
7352
|
+
function pluralise(count, singular, plural = singular) {
|
|
7353
|
+
return `${count} ${count === 1 ? singular : plural}`;
|
|
7354
|
+
}
|
|
7355
|
+
const syncCommand = {
|
|
7356
|
+
description: "Refresh the local meeting index and sync state",
|
|
7357
|
+
flags: {
|
|
7358
|
+
cache: { type: "string" },
|
|
7359
|
+
help: { type: "boolean" },
|
|
7360
|
+
timeout: { type: "string" }
|
|
7361
|
+
},
|
|
7362
|
+
help: syncHelp,
|
|
7363
|
+
name: "sync",
|
|
7364
|
+
async run({ commandFlags, globalFlags }) {
|
|
7365
|
+
const config = await loadConfig({
|
|
7366
|
+
globalFlags,
|
|
7367
|
+
subcommandFlags: commandFlags
|
|
7368
|
+
});
|
|
7369
|
+
debug(config.debug, "using config", config.configFileUsed ?? "(none)");
|
|
7370
|
+
debug(config.debug, "supabase", config.supabase);
|
|
7371
|
+
debug(config.debug, "cacheFile", config.transcripts.cacheFile || "(none)");
|
|
7372
|
+
debug(config.debug, "timeoutMs", config.notes.timeoutMs);
|
|
7373
|
+
const app = await createGranolaApp(config);
|
|
7374
|
+
debug(config.debug, "authMode", app.getState().auth.mode);
|
|
7375
|
+
const result = await app.sync();
|
|
7376
|
+
console.log(`✓ Synced ${pluralise(result.summary.meetingCount, "meeting", "meetings")} across ${pluralise(result.summary.folderCount, "folder", "folders")} (${pluralise(result.summary.createdCount, "created")}, ${pluralise(result.summary.changedCount, "updated")}, ${pluralise(result.summary.removedCount, "removed")}, ${pluralise(result.summary.transcriptReadyCount, "transcript ready", "transcripts ready")})`);
|
|
7377
|
+
const lines = result.changes.slice(0, 10).map((change) => {
|
|
7378
|
+
return ` ${change.kind.padEnd(16)} ${change.title} (${change.meetingId})`;
|
|
7379
|
+
});
|
|
7380
|
+
for (const line of lines) console.log(line);
|
|
7381
|
+
if (result.changes.length > lines.length) console.log(` ...and ${result.changes.length - lines.length} more change(s)`);
|
|
7382
|
+
if (result.state.lastCompletedAt) debug(config.debug, "syncCompletedAt", result.state.lastCompletedAt);
|
|
7383
|
+
return 0;
|
|
7384
|
+
}
|
|
7385
|
+
};
|
|
7386
|
+
//#endregion
|
|
7062
7387
|
//#region src/commands/tui.ts
|
|
7063
7388
|
function tuiHelp() {
|
|
7064
7389
|
return `Granola tui
|
|
@@ -7195,6 +7520,7 @@ const commands = [
|
|
|
7195
7520
|
meetingCommand,
|
|
7196
7521
|
notesCommand,
|
|
7197
7522
|
serveCommand,
|
|
7523
|
+
syncCommand,
|
|
7198
7524
|
tuiCommand,
|
|
7199
7525
|
transcriptsCommand,
|
|
7200
7526
|
{
|
|
@@ -7321,6 +7647,7 @@ Global options:
|
|
|
7321
7647
|
Examples:
|
|
7322
7648
|
granola attach http://127.0.0.1:4123
|
|
7323
7649
|
granola folder list
|
|
7650
|
+
granola sync
|
|
7324
7651
|
granola notes --supabase "${granolaSupabaseCandidates()[0] ?? "/path/to/supabase.json"}"
|
|
7325
7652
|
granola transcripts --cache "${granolaCacheCandidates()[0] ?? "/path/to/cache-v3.json"}"
|
|
7326
7653
|
`;
|